Redis中的Set与Intset
1435597771 · Jul 23, 2026
什么是 Set? Set 是 Redis 的基本数据结构之一,它是一个 无序且唯一 的键值集合(集合中的元素不能重复,且不会按照插入的先后顺序排序)。 在一个 Set 集合中,理论上最多可以存储 $2^{32}-1$(约 42.9 亿)个元素。 内部实现原理 Set 的底层数据结构主要由 IntSet(整数集合) 和 Dict(哈希表) 两种结构实现: IntSet(整数集合) :当集合中的元素 全都是整数 ,且元素个数 不超过 512 个 (默认值,可通过 set-max-intset-entries 修改)时,Redis 会优先使用 IntSet 作为底层结构。 Dict(哈希表) :当集合元素不满足上述条件(混入了非整数,或数量超过了阈值)时,Redis 会自动升级为 Dict。在 Dict 中,Set 的值存放在 Dict 的 key 中,而 Dict 的 value 则统一被设为 null 。 深度拆解:IntSet 结构 我们着重介绍一下 IntSet 这个省内存的底层结构。它的 C 语言结构体定义如下: typedef struct intset { // 编码方式(决定元素是 16位、32位 还是 64位 整数) uint32_t encoding; // 集合包含的元素数量 uint32_t length; // 保存元素的数组 int8_t contents[]; } intset; 注意 : contents 声明为 int8_t 并不意味着它只能存 1 字节的整数。它只是一个 原始字节数组占位符 ,每个元素实际占用多少位,完全由 encoding 动态决定。 1. 假有序与无序的权衡 因为 IntSet 在物理内存里是一块连续数组,且 Redis 在 IntSet 中查找元素使用的是 二分查找 ,这意味着在 IntSet 状态下,数据在物理上是 严格有序 的。 那么,为什么官方依然定义 Set 是“无序集合”? 因为 IntSet 只是一个阶段性的内存优化手段。一旦元素超过 512 个,底层就会立马从 IntSet 搬家(转换)为 Dict。而 Dict 通过 Hash 函数打散存储, 顺序会瞬间被打乱 。所以,Redis 必须在 API 层面保证“无序”的语义,IntSet 带来的“有序”反而是一种次要的副作用。 2. 为什么要用 IntSet?(极致节约内存) 答案只有两个字: 省内存 。 我们拿存储 100 个 32 位整数 为例: 如果用 Dict 存储 : 每一个节点都需要 dictEntry (24B) + redisObject (16B) + SDS 字符串封装 (约 10~15B) + 哈希桶指针 (8B),100 个节点加上内存碎片,至少需要 5800 字节 以上的空间。 如果用 IntSet 存储 : Header ( encoding + length ) 占用 8 字节 + 100 个 32 位整数($100 \times 4\text{B} = 400\text{B}$),总共只需要 408 字节 ! 内存开销整整相差了 14 倍 !这就是 Redis 宁可承受二分查找的开销也要引入 IntSet 的原因。 IntSet 的关键机制 1. IntSet 内部升维(Encoding 升级) 当向 IntSet 插入一个新的整数,且这个整数的范围超出了当前 encoding 所能表示的最大范围时(例如向 16 位数组中插入了一个 32 位整数),就会触发 Encoding 升级 : 扩容 :根据新元素的位数重新计算需要的内存。假设原有 3 个 16 位整数,新入 1 个 32 位整数,总容量由 $3 \times 16 = 48\text{ bit}$ 扩容至 $4 \times 32 = 128\text{ bit}$(新增 80 位空间)。 迁移(重排) :在原数组内存上从后往前,将原来的 16 位整数依次重新分配并转换为 32 位格式放置。 添加新数据 :最后把引发升级的新整数放入预留的末尾空间,并将 encoding 修改为 32 位。 为什么要升级? 默认保持最小的 16 位编码,只有遇到大数才升级,能最大限度地避免内存浪费。 2. IntSet 转换为 Dict 当元素数量突破 512 个,或插入了字符串时,转换流程如下: 创建 :创建一个全新的 Dict 字典,并预分配好 Bucket 空间; 遍历 :遍历 IntSet 中的所有旧整数; 迁移 :将整数转换为字符串 SDS,计算 Hash 值后逐个插入新 Dict,Value 设为 null ; 替换 :将 Set 的 encoding 修改为 OBJ_ENCODING_HT ,指针指向新 Dict,并释放原 IntSet 内存。 3. 支持降级吗? 不支持 。无论是 Encoding 降级,还是 Dict 降级回 IntSet,Redis 都不支持。 如果支持降级,在临界点(如频繁在 512 上下插入删除)会导致系统频繁地发生内存重分配与全量重排,产生严重的 CPU 震荡,反而极大地拖垮性能。 常用命令 Shell # 往集合 key 中存入元素,元素存在则忽略,若 key 不存在则新建 SADD key member [member...] # 从集合 key 中删除元素 SREM key member [member...] # 获取集合 key 中所有元素 SMEMBERS key # 获取集合 key 中的元素个数 SCARD key # 判断 member 元素是否存在于 key 集合中 SISMEMBER key member # 从集合 key 中随机选出 count 个元素,元素不从 key 中删除 SRANDMEMBER key [count] # 从集合 key 中随机选出 count 个元素,元素从 key 中删除 SPOP key [count] # 集合运算(交集、并集、差集) SINTER key [key ...] SUNION key [key ...] SDIFF key [key ...] 典型使用场景 点赞 / 签到 / 抽奖活动 :利用 Set 的唯一性,防止用户重复点赞,或者利用 SPOP / SRANDMEMBER 进行无放回/有放回抽奖。 社交关系(共同关注 / 共同好友) :利用 SINTER 计算两个用户的关注列表交集。 黑名单 / 标签系统 :利用 SISMEMBER 极速判断某个 ID 是否在黑名单中。 注意 :Set 的交集、并集、差集运算复杂度较高(通常为 $O(N)$)。在海量数据场景下,直接在 Redis 主线程中执行复杂的集合运算可能导致 Redis 阻塞,建议在客户端进行计算或使用从库计算。