找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3479

积分

0

好友

450

主题
发表于 11 小时前 | 查看: 8| 回复: 0

今天我们来聊点不一样的。不再重复那些老生常谈的“Redis为什么快”,也不罗列数据类型。我们来直面一个更硬核的问题:如果现在让你打开编辑器,从第一行代码开始,实现Redis的哈希表,你能写得出来吗?

相信大多数人的回答是:不能。这很正常,因为“看懂”和“写出来”之间,隔着一条巨大的实践鸿沟。

一、一个真实的面试拷问

前段时间,有朋友分享了一次面试经历。面试官问:“Redis的哈希表扩容时,为什么不会卡顿?”
他答出了“渐进式rehash”,面试官点点头,接着追问:
“那rehash期间,读写操作怎么处理?rehashidx是什么意思?迁移是按什么顺序进行的?”
然后,他就卡壳了。

他说自己看过很多讲解,感觉都懂了,但被深挖细节时,才发现理解仅停留在表面。你是否也有过这种感受?看文章时恍然大悟,合上之后脑中一片空白。根本原因就在于:从未亲手实现过。

二、我的探索:从源码到实践

过去这段时间,我专注于两件事:一是深入研究 Redis 7.x 的核心源码(超9万行),二是将这些复杂晦涩的原理,转化为能让人真正动手学会的实践课程。

目前,在完成SDS和Adlist两个模块后,Dict哈希表Intset整数集合这两个核心模块也已全部实现。我认为,它们是Redis数据结构中最具学习价值的:

  • Dict:其渐进式rehash是Redis高性能与稳定性的基石,不理解它,对Redis的理解就是不完整的。
  • Intset:虽然小巧,却极致体现了Redis对内存优化的追求,其编码升级机制非常精妙。

下面,我将为你拆解这两个模块的核心设计思想。

三、Dict:为什么需要两张哈希表?

首先思考:Redis的哈希表,与你大学数据结构课上学到的,最大的区别是什么?
答案是:Redis使用了两张哈希表,而非一张
为什么?

3.1 单哈希表的扩容之痛

假设你有一张初始大小为16的哈希表。随着数据增多,到达负载因子阈值后必须扩容,比如扩到32。
扩容意味着什么?所有键值对都需要重新计算哈希值,并分配到新的槽位中
如果你有100万个键,这个瞬间就要执行100万次操作。在此期间,Redis服务将完全卡住,无法响应请求。这对一个要求高可用的系统来说是灾难性的。

3.2 Redis的解决方案:双表渐进式迁移

Redis的解法聪明而优雅:双哈希表 + 渐进式迁移

扩容前:
dict
  ht[0]  size=16  used=13    ← 正在使用的哈希表
  ht[1]  = NULL
  rehashidx = -1             (未在rehash)

触发扩容后:
dict
  ht[0]  size=16  used=13    ← 旧表,数据逐步迁出
  ht[1]  size=32  used=0     ← 新表,数据逐步迁入
  rehashidx = 0              (从第0个桶开始迁移)

核心机制:每次对字典进行增、删、改、查操作时,顺便将ht[0]rehashidx索引指向的桶(bucket)内的所有条目迁移到ht[1]
这样一来,100万次迁移操作被拆散到100万次用户请求中逐步完成,用户完全感知不到卡顿。这就是“渐进式rehash”的精髓。

3.3 Rehash期间的读写规则

这是一个重要的面试考点:

  • 写操作:新数据只写入ht[1],绝不写入ht[0]。这保证了ht[0]的数据只减不增,迁移最终总会完成。
  • 读操作:同时查找ht[0]ht[1](先查ht[0],未找到再查ht[1])。
查找 key "name" 的流程(rehash进行中):

step1: 在 ht[0] 中找 → 未找到(已迁移走)
        ↓
step2: 在 ht[1] 中找 → 找到!返回结果

说明:如果 rehashidx=5,意味着 ht[0] 的 0~4 号桶已迁完
      这些桶里的 key 已经在 ht[1] 里了,ht[0] 里自然找不到

3.4 Dict模块:6天实现路径

  1. 第1天:基础哈希表 实现dictEntry、链地址法解决冲突,完成基础增删改查,编写一个简化的哈希函数,跑通首个测试用例。
  2. 第2天:扩容机制 弄清负载因子计算与触发条件,先实现一次性全量rehash以理解原理。
  3. 第3天:渐进式Rehash(核心) 引入双表结构,实现rehashidx标记与逐步迁移逻辑,将迁移嵌入日常操作。
  4. 第4天:迭代器与扫描 实现安全/非安全迭代器,重点攻克dictScan的反向二进制位迭代算法,这是rehash期间遍历不遗漏的关键。
  5. 第5天:高级特性 实现无值模式、整数内联存储、两阶段删除等,体会Redis极致的性能优化哲学。
  6. 第6天:完整测试与评估 进行百万级数据压力测试,对比渐进式与一次性rehash性能,整理代码并交付。

最终成果:约1678行完整实现,附带详尽文档与测试用例。

四、Intset:三天读懂Redis的“内存抠门”艺术

如果说Dict是“大工程”,Intset就是“小心思”。它目标纯粹:高效存储一组整数。但Redis将其做到了极致。

4.1 为什么不用简单数组?

假设存储 [1, 2, 3, 100, 200]。如果用int32数组,每个占4字节,共20字节。
但如果只存[1, 2, 3]呢?用int32就浪费了,int16足矣。
Intset的设计正在于此:根据集合中最大整数的值,动态选择最节省空间的编码方式

内存布局:

 encoding(4B)   length(4B)    contents[]
 ──────────────────────────────────────────
 │   编码类型  │  元素个数  │  有序整数数组  │
 ──────────────────────────────────────────

encoding 的三种值:
  INTSET_ENC_INT16 → 每个元素 2 字节,范围 -32768 ~ 32767
  INTSET_ENC_INT32 → 每个元素 4 字节,范围约 ±21亿
  INTSET_ENC_INT64 → 每个元素 8 字节,范围超大整数

关键点:数据在contents中是有序存储的,因此查找时可以使用二分搜索,达到 O(log n) 复杂度。

4.2 编码升级:精彩的“动态扩军”

假设当前intset为[1, 2, 3],编码为INT16,占6字节。
现在要插入70000(超出INT16范围),必须将编码升级为INT32

升级前(INT16,每元素2字节):
  contents: [1][2][3]   共6字节

升级步骤:
  1. 重新分配内存:(3+1) × 4 = 16 字节
  2. 从后往前迁移!(避免覆盖未迁移的数据)
       先移动 3 → 放到新位置
       再移动 2 → 放到新位置
       再移动 1 → 放到新位置
       最后插入 70000(因有序,大数必然在末尾)

升级后(INT32,每元素4字节):
  contents: [1][2][3][70000]   共16字节

重要特性编码只能升级,不能降级。即使后续删除了70000,编码仍保持INT32。这是因为降级需要遍历检查所有元素,代价较高。Redis在此选择了以少许空间换取实现简洁与稳定,这是一种典型的工程权衡思维。

4.3 Intset模块:3天实现路径

  1. 第1天:基础建设 设计数据结构,厘清内存布局,实现字节序转换模块,完成创建、获取长度等基础API。
  2. 第2天:核心算法 实现二分查找intsetSearch,完成最精彩的编码升级函数intsetUpgradeAndAdd,实现完整的增删逻辑。
  3. 第3天:收尾完善 实现随机获取元素、集合完整性校验等功能,编写完整测试套件,交付项目。

最终成果:约734行完整实现,附带详尽文档与测试用例。

五、实践出真知:看懂与实现的差距

一个真实的例子:有学员在学SDS前说:“这不就是个动态字符串嘛,挺简单。”实现之后他反馈:“原来之前理解全是错的。返回指针为什么指向buf而不是header?为什么有5种header?预分配为什么是翻倍?”。

看懂了以为懂了,动手写才发现不懂。 我的课程设计正是为了填补这个鸿沟:每天从最小可运行版本开始,增量编码,最终得到一个完整、可用、高性能的实现。你不是在复制粘贴,而是在理解每一行代码为何如此编写。

六、你能收获什么?

  • Dict模块(6天,1678行):你将真正吃透渐进式rehash,能在面试中清晰阐述rehashidx的作用、读写流程、以及dictScan算法的奥秘,而非泛泛而谈。
  • Intset模块(3天,734行):你会掌握Redis极致内存优化的思路,理解编码升级背后的工程权衡。这种“按需分配、动态升级”的设计思想,适用于任何系统设计场景。

两个模块共计9天,代码量2412+行,配套完整文档与测试用例。

最后

学习Redis源码,目的不是为了夸夸其谈,而是为了在未来设计系统时,能自然而然地运用这些精妙的思想:“这里可以用渐进式迁移平滑扩容”,“这里可以用编码升级节省内存”。这才是深入源码的核心价值。

希望这篇对Redis Dict和Intset的拆解,能为你打开一扇从理解到实践的大门。如果你对这类深入底层、动手实学的技术内容感兴趣,欢迎在云栈社区与我们交流探讨。




上一篇:Clawdbot 多 IM 平台统一接入架构拆解:插件化设计思路与核心实现模式
下一篇:机器人核心技术深度解析:从精密减速器到端侧AI芯片的产业链与国产化进程
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-1 21:19 , Processed in 0.383006 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表