Redis 底层数据结构之 Dict(字典)
Redis 中的字典(Dict)是一种非常重要的数据结构,广泛用于实现数据库的键空间、哈希键等功能。它类似于 Java 中的 HashMap 或 Python 中的字典,是一种键值对存储结构,支持高效的增删改查操作。
一、Dict 的基本结构
Redis 的字典由三个主要结构组成:
- dict:整个字典的顶层结构
- dictType:字典的类型特定函数(多态支持)
- dictht:哈希表结构(实际存储键值对的地方)
- dictEntry:哈希表中的每个键值对节点
结构定义(简化版)
c
运行
// 字典类型函数
typedef struct dictType {unsigned int (*hashFunction)(const void *key);void *(*keyDup)(void *privdata, const void *key);void *(*valDup)(void *privdata, const void *obj);int (*keyCompare)(void *privdata, const void *key1, const void *key2);void (*keyDestructor)(void *privdata, void *key);void (*valDestructor)(void *privdata, void *obj);
} dictType;// 哈希表节点
typedef struct dictEntry {void *key; // 键union { // 值(联合体)void *val;uint64_t u64;int64_t s64;double d;} v;struct dictEntry *next; // 链表指针,处理哈希冲突
} dictEntry;// 哈希表
typedef struct dictht {dictEntry **table; // 哈希表数组unsigned long size; // 哈希表大小unsigned long sizemask; // 掩码,用于计算索引(size-1)unsigned long used; // 已使用节点数量
} dictht;// 字典
typedef struct dict {dictType *type; // 类型函数void *privdata; // 私有数据dictht ht[2]; // 两个哈希表,用于渐进式 rehashlong rehashidx; // rehash 索引,-1 表示未在进行 rehashunsigned long iterators; // 当前运行的迭代器数量
} dict;
二、哈希表的实现原理
哈希计算:
- 对键进行哈希计算:
hash = dict->type->hashFunction(key)
- 计算索引:
index = hash & dict->ht[x].sizemask
(利用位运算高效计算)
- 对键进行哈希计算:
解决哈希冲突:
- Redis 使用链地址法解决哈希冲突
- 当多个键哈希到同一个索引时,通过链表将这些键值对连接起来
- 新节点会被添加到链表的头部(O (1) 操作)
三、扩容(Rehash)机制
当哈希表中的键值对数量过多或过少时,Redis 会触发 rehash 操作来调整哈希表大小,以保证操作效率。
触发条件
扩容(expand):
- 服务器没有执行 BGSAVE 或 BGREWRITEAOF 时,负载因子(used/size)> 1
- 服务器正在执行 BGSAVE 或 BGREWRITEAOF 时,负载因子 > 5
- 负载因子 = 哈希表已使用节点数 / 哈希表大小
缩容(shrink):
- 负载因子 < 0.1 时,触发缩容
渐进式 Rehash 过程
为了避免一次性 rehash 带来的性能冲击,Redis 采用渐进式 rehash:
- 为
ht[1]
分配合适的大小(扩容通常是ht[0].used*2
的第一个大于等于该值的 2^n) - 设置
rehashidx = 0
,表示开始 rehash - 每次对字典执行增删改查时,除了操作本身,还会将
ht[0]
中rehashidx
索引的所有键值对迁移到ht[1]
- 迁移完成后,
rehashidx
递增 - 当所有键值对迁移完成,
rehashidx
设为 -1,释放ht[0]
,ht[1]
成为新的ht[0]
,并创建新的空ht[1]
渐进式 Rehash 期间的操作处理
- 查找:先在
ht[0]
查找,如未找到则到ht[1]
查找 - 插入:直接插入到
ht[1]
- 删除:在
ht[0]
和ht[1]
中查找并删除 - 更新:在
ht[0]
中查找并更新,如不存在则到ht[1]
中操作
四、增删改查操作的实现
1. 插入操作(dictAdd)
c
运行
int dictAdd(dict *d, void *key, void *val) {// 尝试查找键是否已存在dictEntry *entry = dictAddRaw(d, key, &existing);if (!entry) return DICT_ERR; // 键已存在// 设置值dictSetVal(d, entry, val);return DICT_OK;
}dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing) {int index;dictEntry *entry;dictht *ht;// 如果正在 rehash,先执行一步 rehashif (dictIsRehashing(d)) _dictRehashStep(d);// 计算键的索引if ((index = _dictKeyIndex(d, key, existing)) == -1)return NULL;// 确定要插入的哈希表(rehash 期间插入到 ht[1])ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];entry = zmalloc(sizeof(*entry));// 将新节点插入到链表头部entry->next = ht->table[index];ht->table[index] = entry;ht->used++;// 设置键dictSetKey(d, entry, key);return entry;
}
2. 查找操作(dictFind)
c
运行
dictEntry *dictFind(dict *d, const void *key) {dictEntry *he;unsigned int h, idx, table;// 如果哈希表为空,直接返回 NULLif (dictSize(d) == 0) return NULL;// 如果正在 rehash,先执行一步 rehashif (dictIsRehashing(d)) _dictRehashStep(d);// 计算哈希值h = dictHashKey(d, key);// 在两个哈希表中查找for (table = 0; table <= 1; table++) {idx = h & d->ht[table].sizemask;he = d->ht[table].table[idx];// 遍历链表查找while (he) {if (dictCompareKeys(d, key, he->key))return he;he = he->next;}// 如果不在 rehash,不需要检查第二个表if (!dictIsRehashing(d)) break;}return NULL;
}
3. 删除操作(dictDelete)
c
运行
int dictDelete(dict *d, const void *key) {// ... 省略部分代码// 计算哈希值和索引h = dictHashKey(d, key);// 在两个哈希表中查找并删除for (table = 0; table <= 1; table++) {idx = h & d->ht[table].sizemask;he = d->ht[table].table[idx];prev = NULL;while (he) {if (dictCompareKeys(d, key, he->key)) {// 从链表中移除节点if (prev)prev->next = he->next;elsed->ht[table].table[idx] = he->next;// 释放键值对dictFreeKey(d, he);dictFreeVal(d, he);zfree(he);d->ht[table].used--;return DICT_OK;}prev = he;he = he->next;}// 如果不在 rehash,不需要检查第二个表if (!dictIsRehashing(d)) break;}return DICT_ERR; // 未找到要删除的键
}
4. 更新操作
Redis 中没有专门的更新函数,更新操作通常是先查找(dictFind),找到后直接修改值:
c
运行
dictEntry *entry = dictFind(d, key);
if (entry) {dictSetVal(d, entry, newValue);// 释放旧值(如果需要)// ...
}
五、Dict 的特点与优化
高效的哈希算法:
- 默认使用 MurmurHash2 算法,具有良好的分布性和计算速度
- 对字符串、整数等不同类型的键有专门的优化
渐进式 rehash:
- 避免了一次性 rehash 带来的性能波动
- 保证了 Redis 在高负载下的响应性
内存优化:
- 哈希表大小总是 2 的幂,便于使用位运算计算索引
- 采用链表处理冲突,避免了开放地址法的聚集问题
多态支持:
- 通过 dictType 结构体支持不同类型的键值对
- 可以自定义哈希函数、比较函数、复制函数等
六、总结
Redis 的字典(Dict)是一个高效、灵活的键值对存储结构,通过哈希表实现,并使用链地址法解决哈希冲突。其核心特性是渐进式 rehash 机制,这使得 Redis 能够在处理大量数据时保持高性能。
Dict 不仅是 Redis 数据库键空间的实现基础,也是哈希键、有序集合等数据类型的底层实现之一,理解 Dict 的工作原理对于深入掌握 Redis 内部机制至关重要。