Redis intset整数集合是如何编码的?

来源:CSS教程作者:桃子头衔:草根站长
导读:本期聚焦于桃子创作的《Redis intset整数集合是如何编码的?》,敬请观看详情。一条整数集合数据在 Redis 内部可能并不是用哈希表保存的,当所有成员都是整数且数量没有超过阈值时,Redis 会启用紧凑的 intset 结构。这个结构用 encoding 字段记录每个元素占用字节数,取值分别为 2、4、8,对应 16 位、32 位和 64 位有符号整数。所有元素按从小到大顺序存放在连续数组中,读取时根据编码宽度直接定位。遇到超出当前范围的整数,intset 会触发编码升级,重新分配空间并把旧元素转换到新宽度;升级只发生在范围不足时,不会因为删除元素而反向降级。由于没有指针、链表和哈希桶的开销,intset 的内存占用远低于哈希表,非常适合小规模整数集合。理解这套编码逻辑,有助于分析集合类型在写入、查询和内存回收时的行为差异。

Redis 的集合类型在保存元素时,底层并不只依赖哈希表。当一个集合对象全部由整数组成,并且元素数量没有超过 set-max-intset-entries 配置的阈值时,内部编码会采用 intset,也就是整数集合。它的核心思路是:用一段连续内存保存有序整数,去掉哈希表节点中的指针、键和值元数据,从而在小集合场景下大幅降低内存占用。

Redis intset整数集合是如何编码的?

intset 的结构与编码字段

intset 在 Redis 源码中的定义非常精简,通常可以概括为一个包含编码类型、长度和柔性数组的结构体。字段 encoding 并不直接存储枚举名称,而是存储每个元素占用的字节数,因此 INTSET_ENC_INT16、INTSET_ENC_INT32、INTSET_ENC_INT64 分别对应数值 2、4 和 8。

典型结构如下:

typedef struct intset {
    uint32_t encoding;
    uint32_t length;
    int8_t contents[];
} intset;

contents 声明为 int8_t 只是为了表示柔性数组的起始地址,真实元素宽度由 encoding 决定。例如编码为 4 时,第 0 个元素占据 contents[0] 到 contents[3],第 1 个元素从 contents[4] 开始。这样设计的好处是访问元素时只需要根据索引乘以编码宽度即可定位,不依赖每种整数类型单独的数组。

为了保证检索效率,intset 中的所有元素都会按照从小到大顺序排列,并且不允许重复。长度字段 length 记录当前包含的元素个数,而不是字节长度。后续插入、删除和查找都依赖这个有序特性,以及编码宽度快速计算偏移量。

插入与编码升级机制

intset 创建时默认采用 16 位整数编码。插入一个整数时,Redis 会先调用 _intsetValueEncoding 判断这个值适合 16 位、32 位还是 64 位。如果新值适合的编码不高于当前编码,就会直接按有序数组插入;如果新值超出了当前编码的范围,就需要触发一次编码升级。

编码升级并不是直接把新元素写进去那么简单。假设当前 intset 使用 16 位编码保存了 1、3、5 三个元素,现在要插入 40000,由于 40000 超出 int16 的最大值 32767,整数集合必须升级为 32 位。Redis 会先根据新编码重新分配更大的连续内存,然后从旧数组的尾部开始向前迁移元素,把它们逐项转换成 32 位后写入新位置。这个从后往前的迁移顺序非常关键:如果从前往后写,较早的目标区域会覆盖尚未读取的旧数据,导致元素丢失。

static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
    uint8_t curenc = intrev32ifbe(is->encoding);
    uint8_t newenc = _intsetValueEncoding(value);
    int length = (int)intrev32ifbe(is->length);
    uint32_t origlen = (uint32_t)length;
    int prepend = value < 0 ? 1 : 0;

    is->encoding = intrev32ifbe(newenc);
    is = intsetResize(is, length + 1);

    while (length--) {
        _intsetSet(is, length + prepend,
                   _intsetGetEncoded(is, length, curenc));
    }

    if (prepend) {
        _intsetSet(is, 0, value);
    } else {
        _intsetSet(is, origlen, value);
    }
    is->length = intrev32ifbe(origlen + 1);
    return is;
}

在上面的简化代码里,prepend 用来判断新插入的值是否为负数。由于 intset 保持升序,负数通常会排到数组头部。如果新值是负数,旧元素迁移时需要整体向后移动一位,给新值留出下标 0 的位置;如果是正数,则直接追加到数组末尾。升级完成后,encoding 被更新为新的宽度,后续插入同范围内整数就不再触发升级。

需要特别注意的是,intset 的升级是单向的。即使之后删除了那个导致升级的大整数,集合仍然会保持高编码宽度,不会自动降级。这样设计避免了频繁重编码带来的 CPU 和内存抖动,代价是一旦集合中短暂出现过大整数,所有后续存储都会按更大宽度分配。

查找、删除与有序性保障

有序数组是 intset 实现高效操作的基础。查找函数 intsetSearch 会在连续内存上做二分查找:它先取中间位置,根据编码宽度读出对应整数,再与目标值比较,决定继续在左半区还是右半区查找。得益于元素宽度固定,按索引取值的复杂度为 O(1),整体查找复杂度为 O(log N)。

插入时同样会先通过二分查找定位 position。如果目标值已经存在,函数直接返回,不会产生重复元素;如果不存在,Redis 会把 position 之后的所有元素向后移动一个槽位,再写入新值。删除操作则相反:找到元素后,把后面的元素整体向前移动,覆盖被删元素,最后通过 intsetResize 缩小内存。

int pos;
if (intsetSearch(is, value, &pos)) {
    uint32_t len = intrev32ifbe(is->length);
    int enc = intrev32ifbe(is->encoding);

    if (pos < len - 1) {
        memmove(is->contents + pos * enc,
                is->contents + (pos + 1) * enc,
                (len - pos - 1) * enc);
    }
    is = intsetResize(is, len - 1);
    is->length = intrev32ifbe(len - 1);
}

这里使用 memmove 而不是 memcpy,因为源地址和目标地址可能重叠。删除中间元素时,目标区域和源区域都位于同一个连续缓冲区中,memmove 能正确处理重叠拷贝。由于元素编码宽度一致,移动长度可以通过剩余元素数量乘以编码宽度快速计算。

与哈希表、listpack 的对比及使用边界

在小集合场景下,intset 的主要优势是紧凑。哈希表的每一个节点都需要维护指针、键、值以及哈希链冲突信息,即使只存一个整数,也要付出大量元数据开销。而 intset 只保留编码、长度和连续整数缓冲区,既省内存又提升缓存局部性。对批量读取和范围扫描来说,连续内存也更容易命中 CPU 缓存。

不过 intset 的适用边界非常清晰。Redis 通过 set-max-intset-entries 参数控制最大元素数量,默认是 512。超过这个数量,intset 会被转换成其他结构,例如旧版本会转成哈希表,较新版本可能采用 listpack。即便元素数量很少,只要插入了一个非整数元素,集合也会放弃 intset,因为 intset 只能表达有符号整数。

综合来看,intset 适合读多写少、元素均为整数且规模稳定的业务场景。理解它的编码升级、有序数组和单向升级策略,有助于预测内存占用变化,避免因为一次超大整数插入或集合规模超限而引发底层结构切换,从而影响请求延迟。

Redis intset整数集合编码原理修改时间:2026-09-23 01:58:54

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0923/60708.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。