1.字典的实现
Redis中字典使用的哈希表结构
typedef struct dictht {
// 哈希表数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值
// 总是等于 size - 1
unsigned long sizemask;
// 该哈希表已有节点的数量
unsigned long used;
} dictht;
table是一个数组,存放指向entry键值对节点的指针
size则是数组长度。used则是数组中已有entry的数量。
Redis中哈希表节点的结构(dictEntry)
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
key键值对中键
v为键值对中的值。v可以为指针也可以为一个整数。
next设计为当出现hash冲突时,冲突的hashentry将放置在当前已存在entry的前面,采用头插法。
这里与HashMap里的hash结构类似,数组+链表的结构,除了hash冲突插入方法有所不同以外,其他很相似。
Redis中字典的结构 (dict)
typedef struct dict {
// 类型特定函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表
dictht ht[2];
// rehash 索引
// 当 rehash 不在进行时,值为 -1
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
} dict;
哈希表的dictht的上层结构又封装了一层,这就是字典了。
字典结构的用处就是:
拥有一个*type用来区分不同类型的键值对,为多态字典保留。privdata也是为了多态字典而准备,用于需要传给那些类型特定函数的可选参数。
拥有存放dictht的一个数组,只包含两个哈希表。初始时ht[0]哈希表被具体使用,当rehash时,ht[0]的数据会被rehash到ht[1]。
Redis中dictType结构
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;
由此结构可见,本结构中具体封装了拥有不同类型的entry的哈希表所对应拥有的不同类型的方法函数。为多态字典准备。
到这里我们知道了从外层到内层的结构为:
dict 字典->dictht 哈希表 -> dictEntry数组table -> dictEntry具体的hash节点(单向链表) ->k,v具体的键值对
2.Redis的rehash操作
很多语言中的hashmap的结构都有rehash操作。redis中操作步骤如下:
1.为hash表重新分配空间
- 如果执行的是扩展操作, 那么
ht[1]
的大小为第一个大于等于ht[0].used * 2
的 (2
的n
次方幂); - 如果执行的是收缩操作, 那么
ht[1]
的大小为第一个大于等于ht[0].used
的 。
如何理解呢,如果是扩容,我们把每个扩容等级想象成上台阶,每一级台阶上可以存放的entry数量为1,2,4,8,16,32...。每次扩容比如entry数量为10时,那么扩容就会扩容到10*2=20,第一个大于等于20的 2的n次方的值就是32,这里大于20的2的n次方的值有32,64,128.....扩容为第一个就行,记得entry的数量要*2
如果是缩容,那么直接缩到当前一个大于等于当前entry数量的一个2的n次方的值就行。比如当前entry为3,那么4就是第一个大于等于3的一个2的n次方的值。
3.Redis的rehash时机
3.1 当以下条件中的任意一个被满足时, 程序会自动开始对哈希表执行扩展操作:
服务器目前没有在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 1
服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 5
3.2
当哈希表的负载因子小于 0.1
时, 程序自动开始对哈希表执行收缩操作。
3.3 负载因子
# 负载因子 = 哈希表已保存节点数量 / 哈希表大小
load_factor = ht[0].used / ht[0].size
4.渐进式rehash
渐进式rehash的意义是为了避免 rehash 对服务器性能造成影响, 服务器不是一次性将 ht[0]
里面的所有键值对全部 rehash 到 ht[1]
, 而是分多次、渐进式地将 ht[0]
里面的键值对慢慢地 rehash 到 ht[1]
步骤:
4.1 为 ht[1]
分配空间, 让字典同时持有 ht[0]
和 ht[1]
两个哈希表。
4.2 在字典中维持一个索引计数器变量 rehashidx
, 并将它的值设置为 0
, 表示 rehash 工作正式开始。
4.3 在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0]
哈希表在 rehashidx
索引上的所有键值对 rehash 到 ht[1]
, 当 rehash 工作完成之后, 程序将 rehashidx
属性的值增一。
4.4 随着字典操作的不断执行, 最终在某个时间点上, ht[0]
的所有键值对都会被 rehash 至 ht[1]
, 这时程序将 rehashidx
属性的值设为 -1
, 表示 rehash 操作已完成。
rehash过程中,增删改查操作有以下特点:
增加只会在ht[1]表中增加,这一措施保证了 ht[0]
包含的键值对数量会只减不增, 并随着 rehash 操作的执行而最终变成空表。
删除,修改,查找,都会在两张表执行。