前言
上一章把Redis基础类型介绍完了,更深的问题便会问:哈希表会有什么缺点?或者你了解hash吗?它是怎么解决冲突的?Redis渐进式rehash的原理是什么?
下面就来深入的解析这些问题。
一、字典
字典是Redis中存在最广泛的一种数据结构不仅在哈希对象,集合对象和有序结合对象中都有使用,而且Redis所有的Key,Value都是存在db->dict这张字典中的。
Redis 的字典使用哈希表作为底层实现。
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2]; // 包含一堆 dictEntry 即下面的结构体
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
unsigned long iterators; /* number of iterators currently running */
} dict;
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
// Redis 的哈希表使用链地址法(separate chaining)来解决键冲突:
// 每个哈希表节点都有一个 next 指针, 多个哈希表节点可以用 next 指针构成一个单向链表,
// 被分配到同一个索引上的多个节点可以用这个单向链表连接起来, 这就解决了键冲突的问题。
struct dictEntry *next;
} dictEntry;
// 这2个结构体要好好记住理解。
rehash操作
rehash解决了什么问题:通过扩容解决hash冲突,解决字典的性能问题。
扩容操作
哈希表的负载因子(used/size)
a. 如果没有进行bgsave 元素数量达到hash长度时就会扩容(负载因子大于等于 1)
b. 如果进行bgsave,元素数量达到hash长度的5倍会进行扩容(负载因子大于等于 5)
收缩操作
当哈希表的负载因子小于 0.1时, 程序自动开始对哈希表执行收缩操作。
负载因子是个动态值,满足一定条件的时候会触发rehash,rehash包括扩容和缩容
rehash步骤:
- 在字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始;
- 为 ht[1] 分配空间, 让字典同时持有 ht[0] 和 ht[1] 两个哈希表
若是扩展操作,那么ht[1]的大小为>=ht[0].used*2的2^n
若是收缩操作,那么ht[1]的大小为>=ht[0].used的2^n
- 将保存在 ht[0] 中的所有键值对 rehash 到 ht[1] 上
- 当 ht[0] 包含的所有键值对全部迁移到了 ht[1] 之后,释放 ht[0] ,将 ht[1] 设置为 ht[0],并重置 ht[1] ,最好将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。
rehashidx还有一个作用:记录的是ht[0]的下标位置的位置,下次rehash就从该位置继续进行。
//部分代码
if (d->ht[0].used == 0) {
// 释放ht[0]哈希表
zfree(d->ht[0].table);
// 将原来的ht[1]号哈希表设置为新的ht[0]哈希表
d->ht[0] = d->ht[1