那些年背过的题:Redis 整数集合的设计与实现

165 阅读6分钟

在 Redis 中,整数集合(Intset)主要用作集合数据类型(Set)的底层实现之一。具体来说,当一个集合的数据量较小,并且其中的所有元素都是整数时,Redis 会选择使用整数集合来存储这些数据。这是为了优化内存使用和操作效率。

集合数据类型(Set)

集合数据类型是 Redis 提供的一种无序的、元素唯一的数据结构,适用于存储不重复的元素集合。这个集合类型支持丰富的操作,如添加元素、删除元素、检查元素是否存在、获取集合大小等。

Redis 内部对集合数据类型的实现有两种主要方式:

  1. 整数集合(Intset)

    • 当集合中的所有元素都是整数,并且元素数量较少时(默认情况下是小于 512 个,但可以通过配置文件调整),Redis 会使用整数集合来实现。
    • 这种方式非常节省内存,因为整数集合采用了紧凑的字节数组来存储整数,并且根据需要进行自动升级编码。
  2. 哈希表(Hashtable)

    • 当集合中的元素不是整数,或者元素数量增加到一定程度(超过默认阈值)时,Redis 会切换为使用哈希表来实现集合。
    • 哈希表虽然占用更多内存,但能够更好地处理大规模数据并保持高效的操作性能。

使用示例

以下是一个简单的示例,展示如何使用集合数据类型以及内部可能采用的整数集合实现:

# 创建一个新的集合并添加一些整数元素
SADD myset 1 2 3 4 5

# 检查某个元素是否存在于集合中
SISMEMBER myset 3  # 返回 1 表示存在

# 获取集合中的所有元素
SMEMBERS myset  # 返回 [1, 2, 3, 4, 5]

# 删除某个元素
SREM myset 3

# 再次获取集合中的所有元素
SMEMBERS myset  # 返回 [1, 2, 4, 5]

在上述示例中,如果 myset 集合中的元素数量较少,并且全部是整数,Redis 会自动选择整数集合来存储这些数据。

整数集合应用场景

  1. 小范围整数数据

    • 当集合中的所有元素都是整数且范围较小时(例如用户 ID、积分等),整数集合能够提供极高的内存效率和操作速度。
  2. 高效的空间利用

    • 在需要节省内存的情况下,通过使用整数集合,可以显著减少内存占用,因为它针对小整数数据进行了优化。
  3. 低延迟要求的操作

    • 由于整数集合在插入、删除和查找操作上都具有非常高的效率,因此适用于对延迟敏感的应用场景。
  4. 频繁变化的小集合

    • 适用于那些元素数量较少但频繁变动的集合,例如在线用户 ID 列表、活跃会话 ID 列表等。

特点

  1. 紧凑存储

    • 整数集合采用紧凑的字节数组来存储整数,按需使用 16 位、32 位或 64 位编码,最大化地节省了内存。
  2. 自动升级

    • 整数集合支持自动升级机制。当新加入的整数超过当前编码范围时,整数集合会自动进行升级,使得既可以保持高效的存储,又能容纳更大范围的数据。
  3. 有序且唯一

    • 存储的整数是有序的,并且每个整数在集合中是唯一的。这使得查找操作可以使用二分查找法,从而进一步提升了查找效率。
  4. 高效的基本操作

    • 插入、删除和查找等基本操作都被设计为在 O(N) 时间复杂度内完成,但由于集合通常较小,实际操作时的平均时间复杂度常常接近 O(1)。
  5. 轻量级

    • 相比于其他数据结构(如哈希表、跳跃表等),整数集合更加轻量级,适合处理小规模的整数数据集。

设计与实现

整数集合的实现主要包括以下几个方面:

  1. 数据结构 整数集合在内部由一个 intset 结构表示。该结构包含三个属性:

    typedef struct intset {
        uint32_t encoding; // 编码方式
        uint32_t length;   // 集合中元素的数量
        int8_t contents[]; // 实际存储元素的数组
    } intset;
    
  2. 编码方式 Redis 的整数集合支持三种不同的编码方式,分别是:

    • INTSET_ENC_INT16 (2 字节,每个元素)
    • INTSET_ENC_INT32 (4 字节,每个元素)
    • INTSET_ENC_INT64 (8 字节,每个元素)

    编码方式取决于集合中元素的大小。如果添加新的元素导致需要更大范围的编码,那么整个集合都会进行升级,以适应新的元素。

  3. 升级机制 当需要将一个新元素插入到整数集合中,且该元素超过了当前编码范围时,整数集合会进行升级。例如,如果当前编码为 INTSET_ENC_INT16,但新元素需要 INTSET_ENC_INT32,那么整个集合将被转换为 INTSET_ENC_INT32

    升级过程包括:

    • 分配新的存储空间,以适应新的编码方式。
    • 将已有的元素复制到新的存储空间,并进行适当的转换。
    • 插入新的元素。
  4. 操作 整数集合支持多种操作,包括插入、删除、查找等。这些操作均在 O(N) 时间复杂度下完成,但由于整数集合通常较小,因此实际操作时性能非常高。

    • 插入:确保新元素按照升序排列,必要时进行升级。
    • 删除:找到待删除元素的位置,移除元素并调整剩余元素的位置。
    • 查找:通过二分查找算法快速定位元素。

示例代码

以下是一个简单的整数集合插入操作伪代码:

// 插入操作的伪代码
intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
    uint32_t pos;
    if (intsetSearch(is, value, &pos)) {
        // 元素已存在
        if (success) *success = 0;
    } else {
        // 需要扩展内存分配并插入元素
        is = intsetResize(is, intrev32ifbe(is->length + 1));
        if (pos < intrev32ifbe(is->length))
            memmove(&is->contents[pos+1],&is->contents[pos],(intrev32ifbe(is->length)-pos)*intrev32ifbe(is->encoding));
        is->contents[pos] = value;
        is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
        if (success) *success = 1;
    }
    return is;
}