一、前言:Redis 的"心脏"------Dict
如果说 SDS 是 Redis 的基石,那么 Dict(字典) 就是其跳动的心脏 。它是 Redis 实现几乎所有高级数据结构的核心引擎:
- 数据库本身:就是一个巨大的 Dict,Key 是用户定义的键,Value 是各种数据类型对象。
- Hash 类型:底层直接使用 Dict 存储 field-value 对。
- Set 类型:当元素非整数或数量过大时,底层转为 Dict(Value 为 NULL)。
- ZSet 类型:Dict 用于存储 member 到 score 的映射,实现 O(1) 查询。
💡 核心价值 :
Dict 是 Redis 高性能读写的基石,其精妙的哈希表实现和"渐进式 Rehash"机制,解决了大数据量下扩容导致的服务卡顿问题!
本文将带你:
- 拆解 Dict 的三层嵌套结构
- 揭秘"渐进式 Rehash"如何做到无感扩容
- 分析 Redis 如何在持久化期间智能控制内存增长
二、Dict 是什么?三层结构大起底
Redis 的 Dict 并非一个简单的哈希表,而是一个包含双哈希表、支持渐进式 Rehash 的复杂结构。
2.1 核心组件
Dict 由三个核心结构体组成:
1. dictEntry:哈希节点
cpp
typedef struct dictEntry {
void *key; // 键
union { // 值(支持多种类型)
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next; // 链表指针,解决哈希冲突
} dictEntry;
- 作用:存储单个键值对。
- 冲突解决 :采用链地址法(Separate Chaining) ,
next指针构成单向链表。
2. dictht:哈希表
cpp
typedef struct dictht {
dictEntry **table; // 哈希桶数组(指针数组)
unsigned long size; // 哈希表大小(桶的数量,总是2的幂)
unsigned long sizemask; // 掩码 = size - 1,用于计算索引
unsigned long used; // 哈希表中已有节点的数量
} dictht;
- 关键点 :
size总是 2 的幂 ,使得index = hash & sizemask可以代替取模运算,极大提升性能。
3. dict:字典主体
cpp
typedef struct dict {
dictType *type; // 类型特定函数(如哈希函数、键比较函数等)
void *privdata; // 私有数据,传递给 type 中的函数
dictht ht[2]; // **两个哈希表!用于渐进式 Rehash**
long rehashidx; // Rehash 索引,-1 表示未在 Rehash
unsigned long iterators; // 当前正在运行的安全迭代器数量
} dict;
- 灵魂所在 :
ht[2]和rehashidx共同实现了渐进式 Rehash。
2.2 内存布局图
+------------------+
| dict |
| +--------------+ | +------------------+ +------------------+
| | ht[0] | |---->| dictht (旧) |---->| dictEntry 数组 |
| +--------------+ | +------------------+ +------------------+
| | ht[1] | |---->| dictht (新) |---->| dictEntry 数组 |
| +--------------+ | +------------------+ +------------------+
| rehashidx: n |
+------------------+
三、Dict 的两大核心机制
3.1 机制 1:渐进式 Rehash(Incremental Rehashing)
问题 :传统哈希表在扩容/缩容时,需要一次性将所有键值对迁移到新表,这在大数据量下会导致服务长时间阻塞。
Redis 的解决方案 :分而治之!
- 准备阶段 :为
ht[1]分配新空间(通常是ht[0].used * 2)。 - 迁移阶段 :不一次性迁移 ,而是将迁移操作分散到后续的每一次 Dict 操作 (增删改查)中。
- 每次操作时,顺带将
ht[0]中rehashidx位置上的所有键值对迁移到ht[1]。 - 同时
rehashidx自增。
- 每次操作时,顺带将
- 完成阶段 :当
ht[0]所有桶都迁移完毕,释放ht[0],将ht[1]赋值给ht[0],并重置rehashidx = -1。
✅ 优势:
- 无感扩容:避免了服务卡顿。
- 平滑过渡 :查询时会同时在
ht[0]和ht[1]中查找,保证数据一致性。
3.2 机制 2:智能的扩容/缩容策略
Dict 的扩容/缩容并非简单地基于 used/size(负载因子),而是结合了服务器当前状态:
触发条件
- 扩容 :
- 服务器没有 在执行
BGSAVE或BGREWRITEAOF(即无子进程):- 负载因子 ≥ 1 时扩容。
- 服务器正在 执行
BGSAVE或BGREWRITEAOF:- 禁止自动扩容!(防止写时复制 COW 导致大量内存页复制)
- 除非 负载因子 ≥ 5(危险阈值),此时强制扩容以防性能急剧下降。
- 服务器没有 在执行
- 缩容 :
- 负载因子 < 0.1 时,进行缩容。
相关配置 (硬编码在
dict.c中)
cppstatic int dict_can_resize = 1; // BGSAVE/BGREWRITEAOF 时置为 0 static unsigned int dict_force_resize_ratio = 5; // 强制扩容阈值
⚠️ 设计哲学 :在内存效率和性能稳定性之间取得精妙平衡。
四、Dict 在 Redis 中的应用场景
| 应用场景 | Key | Value | 说明 |
|---|---|---|---|
| 数据库 | 用户定义的 Key | redisObject |
整个 Redis DB 就是一个 Dict |
| Hash 类型 | Field | Value | 直接使用 Dict 作为底层 |
| Set 类型 | Member | NULL | 当 Set 不满足 intset 条件时 |
| ZSet 类型 | Member | Score (double) |
用于 O(1) 查询成员分数 |
五、动手实验:观察 Dict 的行为
5.1 查看 Hash 的底层编码
bash
# 创建一个小 Hash
> HSET smallhash name "Alice" age "25"
(integer) 2
# 查看编码(小 Hash 可能用 ziplist)
> OBJECT ENCODING smallhash
"ziplist"
# 插入更多字段,使其转为 Dict
> HSET smallhash f1 v1 f2 v2 ... f512 v512
(integer) 512
# 再次查看(应变为 hashtable)
> OBJECT ENCODING smallhash
"hashtable"
5.2 观察 Rehash 过程(需开启 MONITOR)
虽然无法直接看到 rehashidx,但可以通过 INFO 命令间接感知:
bash
# 在大量写入后,观察 used_memory 的变化趋势
# 如果内存是平滑增长而非突增,说明 Rehash 是渐进式的
> INFO memory
六、结语
感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!