Redis intset 如何省下内存

Feng 10 阅读 数据库

Redis 整数集合(intset)是 Redis 中用于高效存储整数值的底层数据结构,具有内存紧凑和升级灵活的特点。本文深入分析 intset 的内存布局、升级机制和适用边界,帮助开发者在实际应用中做出合理选择。

intset 基本概念与内存布局

intset 是 Redis 中用于存储整数值的集合实现,当集合只包含整数值时,Redis 会使用 intset 作为底层实现。intset 的特点是内存紧凑,能够根据存储的整数大小动态调整编码方式,从而节省内存空间。

intset 的内存布局遵循以下结构:

typedef struct intset {
    uint32_t encoding; // 编码方式:INTSET_ENC_INT16, INTSET_ENC_INT32, INTSET_ENC_INT64
    uint32_t length;   // 元素个数
    int8_t contents[]; // 柔性数组,存储整数元素
} intset;

encoding 字段决定了整数的编码方式,有三种可能的取值:

  • INTSET_ENC_INT16:16位有符号整数(-32768 到 32767),每个元素占 2 字节
  • INTSET_ENC_INT32:32位有符号整数(-2147483648 到 2147483647),每个元素占 4 字节
  • INTSET_ENC_INT64:64位有符号整数(-9223372036854775808 到 9223372036854775807),每个元素占 8 字节

length 字段存储当前 intset 中的元素个数。contents 是一个柔性数组,实际存储整数元素,元素按从小到大的顺序排列,使得二分查找成为可能。

// intset 创建示例
intset *is = intsetNew();    // 创建一个空的 intset
intsetAdd(is, 1000, NULL);   // 添加元素 1000
intsetAdd(is, 2000, NULL);   // 添加元素 2000
intsetAdd(is, 3000, NULL);   // 添加元素 3000
// 此时 encoding 为 INTSET_ENC_INT16,因为所有元素都可以用 16 位整数表示

intset 的这种内存布局设计使其在存储小整数时非常高效,但也会受到编码方式的限制。当存储的整数超出当前编码范围时,intset 需要进行升级操作。

intset 升级机制详解

intset 的升级机制是其核心特性之一,它允许 intset 根据存储的整数大小动态调整编码方式,从而在保持有序性的同时最大化内存效率。

升级触发条件

intset 升级会在以下情况下触发:

  1. 当添加的整数无法被当前编码表示时
  2. 当添加的整数会导致当前编码无法表示所有元素时

例如,如果一个 intset 当前使用 INTSET_ENC_INT16 编码,当添加一个大于 32767 或小于 -32768 的整数时,intset 将升级为 INTSET_ENC_INT32。

升级过程

升级过程可以分为以下几个步骤:

  1. 根据新元素的值确定目标编码方式
  2. 创建一个新的、更大尺寸的 intset
  3. 将原有元素和新增元素合并排序后存入新 intset
  4. 释放旧 intset 的内存
// intset 升级示例(伪代码)
void intsetUpgradeAndAdd(intset *is, int64_t value) {
    // 1. 确定目标编码
    int8_t encoding = intsetGetEncodingForValue(value);

    // 2. 创建新 intset
    intset *new_is = intsetNew();
    new_is->encoding = encoding;
    new_is->length = is->length + 1;

    // 3. 分配新内存
    new_is->contents = malloc(new_is->length * intsetElementSize(encoding));

    // 4. 复制元素并合并排序
    int i = 0;
    while (i < is->length && intsetGet(is, i) < value) {
        intsetSet(new_is, i, intsetGet(is, i));
        i++;
    }
    intsetSet(new_is, i, value);
    while (i < is->length) {
        intsetSet(new_is, i + 1, intsetGet(is, i));
        i++;
    }

    // 5. 释放旧 intset 并替换为新 intset
    free(is);
}

升级带来的性能影响

升级操作的时间复杂度为 O(N),其中 N 是 intset 中的元素个数。不过,由于 intset 通常用于存储少量整数,且升级操作不频繁,这种性能影响通常可以忽略不计。

升级后的内存占用会增加:

  • 从 INTSET_ENC_INT16 升级到 INTSET_ENC_INT32:内存使用增加 2 倍
  • 从 INTSET_ENC_INT32 升级到 INTSET_ENC_INT64:内存使用增加 2 倍

intset 适用场景与边界

适用场景

  1. 存储少量整数:元素数量较少时,intset 的内存效率优势明显
  2. 整数值范围跨度不大:当所有整数可以用较小的编码表示时
  3. 需要有序性:intset 中的元素始终有序,适合有序操作

性能边界

  1. 元素数量:当元素数量超过一定阈值(通常 512),Redis 会转换为更高效的数据结构
  2. 整数范围:整数范围过大时会导致频繁升级,影响性能
  3. 查找操作:由于是有序结构,查找操作的时间复杂度为 O(log N)

与其他数据结构的比较

特性 intset hashtable skiplist
内存占用 低(紧凑存储) 中(需要哈希表结构) 高(需要多层指针)
查找速度 O(log N) O(1) O(log N)
插入速度 O(N)(升级时) O(1) O(log N)
有序性 天然有序 无序 天然有序

intset 在内存占用方面具有明显优势,但频繁的升级操作会影响插入性能。在选择数据结构时,需要根据具体场景进行权衡。

实践示例与注意事项

最小可运行示例

# 创建一个整数集合
> sadd myset 1 2 3 4 5
(integer) 5

# 查看集合类型
> object encoding myset
intset

# 添加一个小整数
> sadd myset 6
(integer) 1
> object encoding myset
intset

# 添加一个大整数,触发升级
> sadd myset 32768
(integer) 1
> object encoding myset
hashtable  # 升级为 hashtable

使用注意事项

  1. 避免频繁升级:尽量避免在 intset 中混合使用跨度极大的整数
  2. 合理设置初始大小:预估可能存储的整数范围,避免不必要的升级
  3. 监控内存使用:对于可能包含大量元素的整数集合,考虑使用其他数据结构替代
  4. 利用有序性:intset 的天然有序性适用于需要有序遍历的场景,如排行榜等
  5. 避免删除操作:intset 的删除操作可能导致降级(虽然 Redis 当前未实现),频繁删除和添加可能影响性能
Feng
这位作者很神秘,还没有填写简介。