Redis 字典
基本语法
字典是Redis中的一种数据结构,底层使用哈希表实现,一个哈希表中可以存储多个键值对,它的语法如下,其中KEY为键,field和value为值(也是一个键值对):
1 | HSET key field value |
根据Key和field获取value:
1 | HGET key field |
哈希表
数据结构
dictht
dictht是哈希表的数据结构定义:
- table:哈希表数组,数组中的元素是dictEntry类型的
- size:哈希表数组的大小
- sizemask:哈希表大小掩码,一般等于size-1
- used:已有节点的数量(存储键值对的数量)
1 | typedef struct dictht { |
dictEntry
dictEntry是哈希表节点的结构定义:
- key:键值对中的键
- v:键值对中的值
- next:由于会出现哈希冲突,所以next是指向下一个节点的指针
1 | typedef struct dictEntry { |
dict
dict是Redis中字典的结构定义:
- type:指向dictType的指针
- privdata
- ht[2]:一个dictht类型的数组,数组大小为2,保存了两个哈希表,rehash时使用
- rehashidx:记录了当前rehash的进度
- pauserehash:rehash暂停标记,大于0表示没有进行rehash
1 | typedef struct dict { |
哈希冲突
一个键值对放入哈希表的时候,会根据key的值,计算一个hash值,然后根据hash值与哈希表大小掩码做与运算得到一个索引值,索引值决定元素放入哪个哈希桶中(落入哈希表数组哪个索引位置处)。
1 | // 计算hash值 |
在进行哈希计算的时候,不可避免会出现哈希冲突,出现哈希冲突的时候,Redis采用链式哈希解决冲突,也就是落入同一个桶中的元素,使用链表将这些冲突的元素链起来(dictEntry中的next指针)。
rehash
由于Redis采用链式哈希解决冲突,那么在冲突频繁的场景下,链表会变得越来越长,这种情况下查找效率是比较低下的,需要遍历链表对比KEY的值来获取数据,为了处理效率低下的问题,需要对哈希表进行扩容,扩容的过程称为rehash。
在dict结构替中ht保存了两个哈希表,ht[0]用于数据正常的增删改查,ht[1]用于rehash:
(1)正常情况下,所有的增删改查操作都在ht[0]中进行;
(2)需要进行rehash时,会使用ht[1]建立新的哈希表,并将ht[0]中的数据迁移到ht[1]中;
(3)迁移完成后,ht[0]的空间被释放,然后将ht[1]地址赋给ht[0],ht[1]的大小被设为0,ht[0]重新接收正常的请求,回到了第(1)步的状态;
rehash的触发条件
1 | /* 判断是否需要扩容 */ |
d->ht[0].used/d->ht[0].size : 节点数量与哈希表数组大小的比例,称作负载因子。
dict_force_resize_ratio 的默认值是 5。
- ht[0]的大小为0,此时哈希表是空的,相当于对哈希表做一个初始化的操作。
- 如果哈希表中存储的节点数量大于或者等于哈希表数组的大小,并且哈希表可以扩容或者负载因子大于dict_force_resize_ratio(默认值为5),根据字典的类型判断允许分配内存,满足这三个条件开始扩容。
dict_can_resize
dict_can_resize用来判断哈希表是否可以扩容,有两种状态,值分别为1和0,1代表可以扩容,0代表禁用扩容:
1 | void dictEnableResize(void) { |
updateDictResizePolicy中对dict_can_resize的状态进行了控制,当前没有RDB子进程并且也没有AOF子进程时设置dict_can_resize状态为可扩容:
1 |
|
扩容大小
从代码中可以看到,扩容后哈希表数组的大小为已经存储的节点数量+1:
1 | // 进行扩容 |
一些旧版本中扩容后的大小为已存储节点数量的2倍:
1 | dictExpand(d, d->ht[0].used*2); |
渐进式hash
当哈希表存储节点内容比较多时,需要将原来的节点一个一个拷贝到新的哈希表中,此时Redis主线程无法执行其他请求,造成阻塞,影响性能,为了解决这个问题,引入了渐进式hash。
渐进式hash并不会一次把旧节点全部拷贝到新的哈希表中,而是分多次渐进式的完成拷贝,其中rehashidx记录了迁移进度,每一次迁移的过程中会更新rehashidx的值,下一次进行数据迁移的时候,从rehashidx的位置开始迁移,在dictRehash中可以看到迁移的处理:
- 方法传入了一个参数n,代表本次需要迁移几个哈希桶
- 根据需要迁移哈希桶的数量,循环处理每一个哈希桶:
- 如果当前哈希桶中为空,继续下一个桶的处理rehashidx++
- 如果当前哈希桶不为空,将当前桶中的所有节点迁移到新的哈希表中,然后更新rehashidx的值继续处理下一个桶
- 如果已经处理够了n个桶,或者哈希表的所有数据已经迁移完毕,则结束迁移。
1 | int dictRehash(dict *d, int n) { |
_dictRehashStep
_dictRehashStep中可以看到调用dictRehash时,每次迁移哈希桶的数量为1:
1 | static void _dictRehashStep(dict *d) { |
总结
Redis字典底层使用哈希表实现。
键值对放入哈希表的时候,会根据key的值,计算hash值,出现哈希冲突的时候,Redis采用链式哈希解决冲突,使用链表将这些冲突的元素链起来。
由于Redis采用链式哈希解决冲突,那么在冲突频繁的场景下,链表会变得越来越长,这种情况下查找效率是比较低下的,需要遍历链表对比KEY的值来获取数据,为了处理效率低下的问题,需要对哈希表进行扩容,扩容的过程称为rehash。
当哈希表存储节点内容比较多时,进行rehas的时候主线程无法执行其他请求,造成阻塞,影响性能,所以采用了渐进式hash,渐进式hash并不会一次把旧节点全部拷贝到新的哈希表中,而是分多次渐进式的完成拷贝。
参考
黄健宏《Redis设计与实现》
Redis版本:redis-6.2.5