Redis数据结构之Set结构详解

 更新时间:2026年05月09日 09:55:44   作者:難釋懷  
本文介绍了Redis中Set集合的数据结构,主要其Int种两种编码方式IntSet和Dict以及如何根据数据特征自动切换,IntSet通过紧凑的整数布局实现极致内存节省;Dict则提供高效的唯一性校验,两种编码方式和阈值机制共同实现了了Set的内存效率和操作性能的间的平衡

一、前言:无序、唯一、高效的集合

在 Redis 的五大数据类型中,Set(集合) 是一个非常独特且强大的存在。它天生具备两个核心特性:

  • 元素唯一性:自动去重,确保集合内不存在重复元素。
  • 无序性:元素没有固定的顺序(尽管 SMEMBERS 返回的顺序是稳定的)。

基于这两个特性,Set 被广泛应用于:

  • 标签系统(用户兴趣标签、文章分类)
  • 共同好友/关注SINTER 交集运算)
  • 抽奖池SRANDMEMBER 随机抽取)
  • 全局去重(如爬虫 URL 去重)

但你是否想过,Redis 是如何在底层高效地实现“唯一性”和“快速查找”的?答案就藏在它的两种精巧数据结构中。

核心价值

Redis Set 的底层会根据数据特征,在 IntSet(整数集合)和 Dict(字典)之间智能切换,以实现内存效率与操作性能的最佳平衡

本文将带你:

  • 拆解 IntSet 的紧凑内存布局
  • 揭秘 Dict 如何实现 O(1) 的唯一性校验
  • 理解编码转换背后的阈值逻辑

二、Set 的双重身份:IntSet 与 Dict

Redis Set 并非只有一种底层实现,而是拥有两种编码(encoding),由 redisObject 的 encoding 字段决定:

编码 (encoding)底层数据结构适用场景
OBJ_ENCODING_INTSETIntSet (整数集合)所有元素都是整数,且数量较少
OBJ_ENCODING_HTDict (字典/哈希表)元素包含非整数,或整数数量过多

这种设计体现了 Redis “因地制宜” 的优化哲学:对简单、规则的数据用最省空间的结构;对复杂、庞大的数据用最高效的结构。

三、编码一:IntSet - 整数的极致压缩

3.1 诞生背景

当一个 Set 中的所有元素都是整数时,使用通用的哈希表(Dict)来存储显得有些“大材小用”。因为哈希表需要为每个元素存储一个完整的 dictEntry 结构(包含 key, value, next 指针等),内存开销较大。

为了极致节省内存,Redis 引入了 IntSet

3.2 源码结构

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

关键特性

  • 内存连续:所有整数紧密排列在 contents 数组中,无任何指针开销
  • 有序存储:内部元素按从小到大排序,为二分查找提供可能。
  • 类型升级encoding 字段决定了每个整数占用的字节数(2/4/8字节)。

3.3 类型升级机制(核心!)

IntSet 最精妙的设计在于其动态类型升级能力。

  • 初始状态:插入第一个整数 5encoding = INTSET_ENC_INT16,每个元素占 2 字节。
  • 插入更大整数:当插入一个超出当前 encoding 范围的整数(如 70000,超过了 int16 的最大值 32767)时,IntSet 会自动将整个集合升级到 INTSET_ENC_INT32

过程

  • 申请一块新的、更大的内存空间。
  • 将原有所有元素按新类型(如 int32)重新写入新空间。
  • 更新 encoding 和 length 字段。
  • 释放旧内存。

注意:这个过程需要 O(N) 的时间复杂度和额外的内存,但只会在必要时发生一次,之后的插入操作又恢复 O(log N)(二分查找+插入)。

3.4 内存优势

假设一个 Set 包含 1000 个 int16 范围内的整数:

  • IntSet8 (header) + 1000 * 2 = 2008 bytes
  • Dict:每个 dictEntry 至少需要 8(key)+8(value)+8(next) = 24字节,加上哈希表本身的桶数组,总内存轻松超过 24000+ bytes

内存节省高达 90% 以上!

四、编码二:Dict - 通用的高性能解决方案

一旦 Set 不再满足 IntSet 的苛刻条件(出现非整数,或整数太多),Redis 会立即将其转换为 Dict(字典)

4.1 为什么是 Dict?

Dict 是 Redis 的基石数据结构之一,它是一个哈希表,天然具备以下特性:

  • O(1) 平均时间复杂度:用于添加 (SADD)、删除 (SREM)、查找 (SISMEMBER) 操作。
  • 天然去重:哈希表的 key 本身就是唯一的,完美契合 Set 的“元素唯一”要求。

4.2 Dict 在 Set 中的特殊用法

在 Set 的场景下,Dict 的使用非常巧妙:

  • Key:存储 Set 的元素(字符串或序列化后的整数)。
  • Value统一设置为 NULL 指针
// 伪代码示意
dict *d = dictCreate(&setDictType, NULL);
dictAdd(d, "element1", NULL);
dictAdd(d, "element2", NULL);

✅ 优势:这样既利用了 Dict 的高效哈希和唯一性保证,又省去了 Value 的内存开销。

4.3 渐进式 Rehash

Dict 本身也有一套精妙的扩容/缩容机制(渐进式 rehash),确保在数据量巨大时,单次操作的延迟依然很低。这部分内容在此不展开,但它保证了即使 Set 包含百万级元素,性能依然稳定。

五、编码转换:阈值与触发条件

Redis 通过两个配置项来控制 Set 何时从 intset 转换为 hashtable

配置项默认值说明
set-max-intset-entries512当 Set 中的整数元素数量超过此值时,即使全是整数,也会转换为 Dict。
隐式条件-当尝试向一个 intset 编码的 Set 中插入一个非整数值(如字符串)时,会立即触发转换。

设计考量

  • 512 这个阈值:是内存效率和操作性能的平衡点。超过 512 个元素后,IntSet 的 O(log N) 查找和 O(N) 的插入(因需移动内存)开销开始显现,而 Dict 的 O(1) 优势则愈发明显。
  • 即时转换:保证了数据模型的一致性。一旦数据不再是“纯整数”,就必须切换到通用模型。

六、动手实验:观察 Set 的编码变化

6.1 验证 IntSet

# 添加纯整数
> SADD myset 1 2 3 100 200
(integer) 5

# 查看编码
> OBJECT ENCODING myset
"intset"

6.2 触发转换:插入非整数

# 插入一个字符串
> SADD myset "hello"
(integer) 1

# 编码已变为 hashtable
> OBJECT ENCODING myset
"hashtable"

6.3 触发转换:超过阈值

# 创建一个脚本,添加513个整数
> for i in {1..513}; do redis-cli SADD big_intset $i; done

# 查看编码(应为 hashtable)
> OBJECT ENCODING big_intset
"hashtable"

七、总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • Redis配置只读账号实现方式

    Redis配置只读账号实现方式

    本文介绍了Redis的ACL系统,用于精细化管理不同用户对Redis的访问权限,从创建只读账户、设置命令控制、键空间控制到权限类别等,详细阐述了Redis6.0及以上版本的ACL配置方法,并提供了配置示例和管理命令,适用于提高数据安全性与管理效率
    2026-04-04
  • Redis慢日志的实现示例

    Redis慢日志的实现示例

    慢查询日志是Redis提供的一个用于观察系统性能的功能,本文主要介绍了Redis慢日志的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2024-04-04
  • redis中的数据结构和编码详解

    redis中的数据结构和编码详解

    本文主要和大家分享几种Redis数据结构详解,希望文中的案例和代码,能帮助到大家。
    2020-03-03
  • Redis 过期键删除策略的实现示例

    Redis 过期键删除策略的实现示例

    Redis的过期数据删除策略主要有三种,包括定时删除、惰性删除和定期删除,本文主要介绍了Redis 过期键删除策略的实现示例,具有一定的参考价值,感兴趣的可以了解一下
    2024-03-03
  • Redis实现商品秒杀的示例代码

    Redis实现商品秒杀的示例代码

    本文主要介绍了Redis实现商品秒杀的示例代码,详细介绍了Redis的List、Set和Hash类型,以及使用Redis事务保证原子性的方式,具有一定的参考价值,感兴趣的可以了解一下
    2024-02-02
  • redisTemplate.opsForValue().get()获取值失败的解决方案

    redisTemplate.opsForValue().get()获取值失败的解决方案

    文章讨论了在使用RedisTemplate时遇到get()方法返回null的问题,并分析了原因,作者建议使用@Autowired注解进行依赖注入,特别是推荐通过构造函数注入,以避免类型无法分辨的问题,文章最后总结了这些经验,并鼓励读者参考和使用
    2026-03-03
  • Redis中Hash类型相关命令介绍

    Redis中Hash类型相关命令介绍

    Redis hash是一个String类型的field和value的映射表,hash特别适合用于存储对象,这篇文章主要介绍了Redis中Hash类型相关命令的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2025-12-12
  • Redis实现短信验证码登录的示例代码

    Redis实现短信验证码登录的示例代码

    本文主要介绍了基于Redis如何实现短信验证码登录功能,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-06-06
  • Redis 常用命令之基础、进阶与场景化实战案例

    Redis 常用命令之基础、进阶与场景化实战案例

    Redis常用命令全解析,涵盖基础、进阶和场景化实战,包括字符串、哈希、列表、集合、有序集合等数据类型,以及发布订阅、分布式锁、事务等高级功能,本文给大家介绍Redis常用命令之基础、进阶与场景化实战案例,感兴趣的朋友一起看看吧
    2026-01-01
  • redis stream 实现消息队列的实践

    redis stream 实现消息队列的实践

    本文主要介绍了redis stream 实现消息队列的实践,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-08-08

最新评论