Redis Dict(字典/哈希表)技术文档
一、概述
Dict(字典)是 Redis 中最核心、使用最广泛 的数据结构。它是 Redis 的哈希表实现,用于实现键值对存储,支持 O(1) 的查找、插入、删除操作。
1.1 为什么叫 Dict?
Redis 对外暴露的 API 是 Hash(如 HSET、HGET),但底层实现叫做 dict。这是因为:
- Hash:对外的数据结构接口
- Dict:底层的哈希表实现
1.2 使用场景
| 场景 | 说明 |
|---|---|
| 全局键空间 | 存储所有数据库键值对(db->dict) |
| Hash 类型 | 实现 Redis Hash 数据类型 |
| Set 类型 | 底层编码(当元素为字符串时) |
| 模块系统 | 模块内部数据结构 |
| 集群模式 | 存储节点信息、槽位映射 |
| 发布订阅 | 存储频道订阅关系 |
| Lua 脚本 | 脚本缓存 |
二、核心设计思想
2.1 三大特性
- 渐进式 Rehash:使用两个哈希表,逐步迁移数据,避免一次性迁移阻塞主线程
- 链表法解决冲突:每个桶是一个链表,冲突时插入链表
- 指针标记优化:对 Set(无 value)优化,直接存储 key 指针,减少内存分配
2.2 整体架构
lua
+-------------------+
| dict |
| +-------------+ |
| | dictType | | -- 类型配置(哈希函数、比较函数等)
| +-------------+ |
| | ht_table[0] | | -- 当前使用的主哈希表
| | ht_table[1] | | -- 渐进式 Rehash 时的目标表
| +-------------+ |
| | rehashidx | | -- Rehash 进度(-1 表示不在 rehash)
| | pauserehash | | -- Rehash 暂停标记
| +-------------+ |
+-------------------+
三、数据结构详解
3.1 dict 结构体
c
struct dict {
dictType *type; // 类型配置
dictEntry **ht_table[2]; // 两个哈希表
unsigned long ht_used[2]; // 已使用的节点数
long rehashidx; // rehash 进度,-1 表示未在 rehash
unsigned pauserehash; // rehash 暂停标记
signed char ht_size_exp[2]; // 大小指数(size = 1 << exp)
int16_t pauseAutoResize; // 禁止自动扩容
void *metadata[]; // 额外元数据
};
关键字段解析:
| 字段 | 类型 | 说明 |
|---|---|---|
ht_table[0] |
dictEntry** |
当前使用的主哈希表 |
ht_table[1] |
dictEntry** |
渐进式 Rehash 时的目标表 |
ht_used[2] |
unsigned long |
两个表的已用节点数 |
rehashidx |
long |
-1 表示不在 rehash,>=0 表示当前进度 |
pauserehash |
unsigned |
>0 表示暂停 rehash(迭代器使用) |
ht_size_exp[2] |
signed char |
哈希表大小的指数(size = 1 << exp) |
3.2 dictEntry(哈希节点)
c
typedef struct dictEntry {
void *key; // 键
union { // 值(联合体)
void *val;
uint64_t u64;
int64_t i64;
double d;
} v;
struct dictEntry *next; // 链表指针(解决冲突)
} dictEntry;
内存布局:
lua
+------------+-------------+-------------+
| key | val/u64/ | next |
| (void*) | i64/d | (dictEntry*)
+------------+-------------+-------------+
3.3 dictType(类型配置)
c
typedef struct dictType {
// 哈希函数
uint64_t (*hashFunction)(const void *key);
// 键复制/比较/销毁
void *(*keyDup)(dict *d, const void *key);
int (*keyCompare)(dictCmpCache *cache, const void *key1, const void *key2);
void (*keyDestructor)(dict *d, void *key);
// 值复制/销毁
void *(*valDup)(dict *d, const void *obj);
void (*valDestructor)(dict *d, void *obj);
// 扩展/收缩许可
int (*resizeAllowed)(size_t moreMem, double usedRatio);
// 元数据
void *userdata;
// 标志
unsigned int no_value:1; // 无 value 优化(Set 使用)
unsigned int keys_are_odd:1; // key 地址为奇数优化
unsigned int force_full_rehash:1; // 强制完整 rehash
} dictType;
四、哈希算法
4.1 哈希函数选择
Redis 使用 SipHash(一种加密安全的哈希算法):
c
uint64_t dictGenHashFunction(const void *key, size_t len) {
return siphash(key, len, dict_hash_function_seed);
}
为什么选 SipHash?
- 防止哈希碰撞攻击(Hash DoS)
- 良好的随机分布性
- 高性能
4.2 桶索引计算
c
index = hash(key) & mask
// 等价于
index = hash(key) % size
由于哈希表大小始终是 2 的幂次,所以可以用位运算代替取模:
c
#define DICTHT_SIZE_MASK(exp) (((unsigned long)1 << (exp)) - 1)
// 如 exp=4: mask = 0b1111 = 15,size = 16
五、渐进式 Rehash(核心机制)
5.1 为什么需要 Rehash?
当哈希表中的元素越来越多,负载因子(used / size)会增大,导致链表变长,查询效率下降。
Redis 通过 rehash(重新哈希)来扩容或缩容,保持效率。
5.2 什么是渐进式 Rehash?
一次性 rehash 的问题:
- 如果有 100 万个键,全部重新哈希需要很长时间
- Redis 是单线程的,会阻塞所有操作
渐进式 Rehash 解决方案:
- 每次操作(查找、插入、删除)时,迁移一小部分数据
- 把大任务拆成多个小任务,分散到多次请求中
5.3 Rehash 流程
ini
阶段 1: 准备阶段
+------------------+ +------------------+
| ht_table[0] | | ht_table[1] |
| [桶0][桶1][...] | | [NULL][NULL]... | ← 新分配更大的表
+------------------+ +------------------+
阶段 2: 渐进迁移
+------------------+ +------------------+
| ht_table[0] | | ht_table[1] |
| [桶0→][桶1→] | --> | [新桶0][新桶1] |
| [桶2→][...] | | [新桶2]... |
+------------------+ +------------------+
↑ 每次操作迁移一个桶
阶段 3: 完成
+------------------+ +------------------+
| ht_table[0] | | ht_table[1] |
| [NULL][NULL]... | | [完整数据] |
+------------------+ +------------------+
交换 ht_table[0] 和 ht_table[1]
5.4 代码示例
c
// 每次操作时执行 rehash
int dictRehash(dict *d, int n) {
// 如果不在 rehash,直接返回
if (!dictIsRehashing(d)) return 0;
// 迁移 n 个桶
while(n--) {
dictEntry *de, *nextde;
// 检查当前桶是否为空
if ((de = d->ht_table[0][d->rehashidx]) == NULL) {
d->rehashidx++;
continue;
}
// 迁移当前桶的所有节点
while(de) {
nextde = de->next;
// 计算新索引并插入 ht[1]
uint64_t h = dictHashKey(d, de->key) & DICTHT_SIZE_MASK(d->ht_size_exp[1]);
de->next = d->ht_table[1][h];
d->ht_table[1][h] = de;
d->ht_used[1]++;
d->ht_used[0]--;
de = nextde;
}
d->ht_table[0][d->rehashidx] = NULL;
d->rehashidx++;
}
return 1;
}
5.5 触发时机
| 操作 | 行为 |
|---|---|
dictAdd / dictReplace |
插入前检查,触发一次 rehash |
dictFind / dictDelete |
查找/删除前检查,触发一次 rehash |
dictRehashMicroseconds |
定时任务,在空闲时执行 |
5.6 负载因子阈值
c
// 扩容阈值:负载因子 >= 1 时扩容
if (used >= size && can_resize) {
dictExpand(d, used * 2);
}
// 缩容阈值:负载因子 < 0.1 时缩容
if (size > DICT_HT_INITIAL_SIZE && used * 100 / size < HASHTABLE_MIN_FILL) {
dictShrink(d);
}
六、冲突解决:链表法
6.1 冲突处理
当两个 key 哈希到同一个桶时,使用链表连接:
yaml
桶索引 0: [key1=100] -> [key2=200] -> NULL
桶索引 1: [key3=300] -> NULL
桶索引 2: NULL
6.2 查找过程
c
dictEntry *dictFind(dict *d, const void *key) {
uint64_t h = dictHashKey(d, key);
// 如果正在 rehash,先迁移当前桶
if (dictIsRehashing(d)) _dictRehashStep(d);
// 遍历链表
dictEntry *he = d->ht_table[0][h & mask0];
while(he) {
if (dictCompareKeys(d, key, he->key))
return he;
he = he->next;
}
return NULL;
}
6.3 平均查找复杂度
| 情况 | 平均查找次数 | 最坏情况 |
|---|---|---|
| 理想情况 | 1 次 | 1 次 |
| 一般情况 | 1-2 次 | O(n)(所有 key 都冲突) |
| 扩容后 | < 1.5 次 | O(1) |
七、指针标记优化(Set 优化)
7.1 背景
当 dict 用作 Set (无 value)时,每个 dictEntry 只需要存储 key。
但标准 dictEntry 结构体包含 key、val、next,有内存浪费。
7.2 优化方案
方案 A:标准模式(Hash)
css
桶 -> [dictEntry] -> [dictEntry] -> NULL
↓ ↓
[key+val] [key+val]
方案 B:优化模式(Set)
css
桶 -> [key_ptr] -> [dictEntry] -> NULL
↓
[key+next]
7.3 指针标记(Pointer Tagging)
利用指针地址的最低 3 位(由于 8 字节对齐,这 3 位始终为 0)来存储元数据:
指针值: 0x7FFF_0000_0000_0XXX
↑
低 3 位(标记位)
标记含义:
| 标记值 | 含义 |
|---|---|
000 |
普通 dictEntry 指针 |
010 |
偶地址 key 指针 |
XX1 |
奇地址 key 指针 |
7.4 代码实现
c
// 检查指针类型
#define ENTRY_PTR_NORMAL 0 // dictEntry 指针
#define ENTRY_PTR_IS_EVEN_KEY 2 // 偶地址 key
#define ENTRY_PTR_IS_ODD_KEY 1 // 奇地址 key(任何奇数)
// 判断是否为 dictEntry
static inline int entryIsNormal(const dictEntry *de) {
return ((uintptr_t)de & 7) == ENTRY_PTR_NORMAL;
}
// 判断是否为直接 key 指针
static inline int entryIsKey(const dictEntry *de) {
return !entryIsNormal(de);
}
// 获取实际的 dictEntry 或 key
dictEntry *decodeEntry(dictEntry *de) {
if (entryIsNormal(de)) {
return de; // 直接返回
} else {
// 从 key 指针创建 dictEntry(临时)
void *key = (void *)((uintptr_t)de & ~7);
// ... 构建临时 entry
}
}
7.5 优化效果
| 场景 | 每个元素内存 |
|---|---|
| 标准 dictEntry | 24 字节(key + val + next) |
| 优化后(单个 key) | 8 字节(仅 key 指针) |
| 优化后(链表) | 16 字节(dictEntry 无 val) |
八、迭代器设计
8.1 安全 vs 非安全迭代器
c
typedef struct dictIterator {
dict *d;
long index;
int table, safe;
dictEntry *entry, *nextEntry;
unsigned long long fingerprint;
} dictIterator;
| 类型 | 特点 | 使用场景 |
|---|---|---|
| 安全迭代器 | 可以修改 dict(暂停 rehash) | HSCAN、HKEYS |
| 非安全迭代器 | 只读,不暂停 rehash | 遍历但不修改 |
8.2 使用示例
c
// 安全迭代器
dictIterator *iter = dictGetSafeIterator(d);
dictEntry *entry;
while ((entry = dictNext(iter)) != NULL) {
void *key = dictGetKey(entry);
void *val = dictGetVal(entry);
// 可以安全地删除或修改
}
dictReleaseIterator(iter);
九、核心 API 速查
9.1 创建与销毁
c
dict *dictCreate(dictType *type); // 创建字典
void dictRelease(dict *d); // 销毁字典
void dictEmpty(dict *d, void(callback)(dict*)); // 清空字典
9.2 增删改查
c
// 添加
int dictAdd(dict *d, void *key, void *val); // 添加键值对
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing);
int dictReplace(dict *d, void *key, void *val); // 替换或添加
// 删除
int dictDelete(dict *d, const void *key); // 删除
// 查找
dictEntry *dictFind(dict *d, const void *key); // 查找键
void *dictFetchValue(dict *d, const void *key); // 查找值
9.3 扩展与收缩
c
int dictExpand(dict *d, unsigned long size); // 手动扩展
int dictExpandIfNeeded(dict *d); // 按需扩展
int dictShrinkIfNeeded(dict *d); // 按需收缩
int dictRehash(dict *d, int n); // 执行 n 步 rehash
9.4 随机获取
c
dictEntry *dictGetRandomKey(dict *d); // 随机获取一个键
dictEntry *dictGetFairRandomKey(dict *d); // 公平随机(分布均匀)
unsigned int dictGetSomeKeys(dict *d, dictEntry **des, unsigned int count);
9.5 宏工具
c
dictSize(d) // 获取元素数量
dictIsEmpty(d) // 判断是否为空
dictIsRehashing(d) // 是否在 rehash
dictPauseRehashing(d) // 暂停 rehash
dictResumeRehashing(d) // 恢复 rehash
十、性能分析
10.1 时间复杂度
| 操作 | 平均复杂度 | 最坏情况 |
|---|---|---|
插入(dictAdd) |
O(1) | O(n)(所有 key 冲突) |
删除(dictDelete) |
O(1) | O(n) |
查找(dictFind) |
O(1) | O(n) |
| Rehash(单步) | O(1) | O(n)(一个桶的链表很长) |
10.2 空间复杂度
- 负载因子(used / size)控制在 0.5 ~ 1.0 之间
- 最小表大小 :4(
DICT_HT_INITIAL_SIZE = 1<<2 = 4) - 扩展策略:翻倍
- 收缩策略:负载因子 < 12.5% 时缩容
十一、设计亮点总结
11.1 渐进式 Rehash
思想:"把大任务拆成小任务,分散到多次请求中"
- 避免一次性迁移导致阻塞
- 保证服务的持续可用性
- 利用请求的间隙时间完成迁移
11.2 指针标记优化
思想:"利用内存对齐的空闲位,存储额外信息"
- 减少 Set 类型的内存占用
- 不需要额外的元数据结构
- 对性能几乎无影响
11.3 安全迭代器
思想:"读写分离,保证遍历安全"
- 遍历时暂停 rehash,避免数据丢失
- 提供指纹校验,检测非法修改
- 支持安全删除
十二、与 Java HashMap 对比
| 特性 | Redis Dict | Java HashMap |
|---|---|---|
| 冲突解决 | 链表法 | 链表法 / 红黑树 |
| 扩容策略 | 渐进式 Rehash | 一次性迁移(阻塞) |
| 负载因子 | 0.5 ~ 1.0 | 0.75 |
| 哈希算法 | SipHash | 自定义(基于对象 hashCode) |
| 线程安全 | 单线程(外部保证) | 非线程安全(ConcurrentHashMap 是) |
| Set 优化 | 指针标记 | 无(HashSet 独立实现) |
| 缩容 | 支持 | 不支持 |
附录:关键宏定义
c
#define DICT_HT_INITIAL_EXP 2 // 初始大小指数
#define DICT_HT_INITIAL_SIZE (1 << DICT_HT_INITIAL_EXP) // 初始大小 = 4
#define HASHTABLE_MIN_FILL 8 // 最小填充率 = 12.5%
#define DICT_OK 0 // 操作成功
#define DICT_ERR 1 // 操作失败
参考源码 :Redis 8.8.2 (src/dict.h, src/dict.c)