字典又称
符号表
,关联数组
,映射
, 是一种用于保存键值对的抽象数据结构Redis构建了自己的字典实现, eg. set msg ‘test’会构建
key
为msg
,value
为test
的键值对.除了表示数据库之外, 字典还是hash键的底层实现之一(另外一种是ziplist)
字典实现
hash表
Redis字典所使用的哈希表由 dict.h/dictht 结构定义
1 | typedef struct dictht { |
hash表节点
1 | typedef struct dictEntry { |
key
属性保存着键值对中的键、而v
属性则保存着键值对中的值, 它可以是一个指针
, 或者是uint64_t
类型的整数, 或者是 int64_t
的整数
next
属性是指向另一个节点的指针, 这个指针可以将多个hash值相同的键值对连接在一起, 解决键冲突问题
字典
Redis中的字典由 dict.h/dict
结构定义
1 | typedef struct dict { |
type
属性是一个指向 dictType
结构的指针, 每个 dictType结构保存了一簇用于操作特定类型键值对的函数, Redis会为不同类型的字典设置不同类型的特定函数
privdata
属性则保存了需要传给那些特定类型函数的可选参数
type
和privdata
是针对不同类型的键值对, 为创建多态字典设置的
1 | typedef struct dictType { |
ht
属性是一个包含两个项的数组, 数组中的每个项都是一个dictht哈希表, 一般只使用ht[0], ht[1]只会在对ht[0] 进行rehash
时使用.
rehashidx
记录当前rehash
的进度, 若没有在进行rehash
, 则值为 -1
完整的dict结构
hash算法
Redis计算hash值的方式如下:
1 | hash = dict->type->hashFunction(key); // 使用hash字典设置的hash函数、计算key的hash值 |
解决键冲突
当有两个或以上数量的键被分配到hash表数组同一个索引上时, 称为键冲突 collision
, Redis的hash表使用 链地址法
(separate chaining
)来解决冲突, 每个hash表节点上有一个next指针, 多个hash表节点使用next指针构成单链表
dictEntry
总是将新增节点添加到链表头部(时间复杂度为 O(1)), 因为没有tail指针.
rehash
随着操作的不断进行、hash表维护的键值对会增加或减少, 为了让hash表的负载因子(load factor)维持在一个合理的额范围, 当hash表保存键值对数量太大或太小时, 程序需要对hash表扩容或缩容, 扩容或缩容通过rehash
进行, Rehash
步骤如下:
- 为字典的ht[1]hash表分配空间, hash表的空间大小取决于要执行的操作, 及ht[0]当前包含的键值对的数量(即: ht[0].used 属性值), 若为扩容, ht[1] 为第一个大于等于 (ht[0].used
*
2)d的*2n; 若为缩容, ht[1] 为第一个大于等于 (ht[0].used) 的 2n - 将ht[0]中的所有键值对rehash到ht[1]上,
rehash
是指: 重新计算键的hash值和索引值, 然后将键值对放到ht[1]hash表的对应位置 - 当ht[0]包含的所有键值对迁移到ht[1]之后(ht[0]变为空表), 释放ht[0], 将ht[1]设置为 ht[0], 并在 ht[1]新创建一个空白hash表, 为下一次rehash做准备
1 | eg. 扩容操作: |
hash表的扩展与收缩
满足下述任意条件时, 会自动扩展:
server 当前未执行
bgsave
或者bgrewriteaof
, 且hash表的负载因子大于等于1server 当前正在执行
bgsave
或者bgrewriteaof
, 且 hash表的负载因子大于等于 5(持久化时、优先持久化操作)
1 | load_factor = ht[0].used / ht[0].size; // 负载因子 = hash表已保存节数量 / hash表大小 |
若bgsave
或者bgrewriteaof
执行时, Redis需要创建当前服务器进程的子进程, 大多数操作系统使用写时复制(copy-on-write
)优化子进程使用效率, 所以子进程存在期间, Server会提高rehash操作的负载因子, 尽可能避免此时rehash, 避免不必要的内存写入操作, 最大限度的节省内存.
另外:
hash表的负载因子小于0.1
时, 程序会自动缩容
渐进式rehash
其实, hash表的rehash操作、不是一次集中完成的, 而是多次迭代进行的. 因为若 ht[0]里保存的key有百万计, 甚至更多, 一次rehash导ht[1], 庞大的计算量可能会导致Server一段时间的停止服务.
渐进式rehash步骤如下:
- 为ht[1]分配空间, 让字典同时持有ht[0] 和 ht[1]两个hash表
- 在字典中维护一个索引计数器变量
rehashidx
, 设置为0, 表示rehash
已经开始 - 在
rehash
期间, 每次对字典add
,del
,find
orupdate
操作, 程序除了执行指定操作外, 还会顺带将 ht[0] hash表在rehashidx
索引上的所有键值对 rehash 到 ht[1], rehash完成、将rehashidx++
. - 随字典操作的不断执行、某个时间点, ht[0]的所有键值对rehash到ht[1], rehash完成、将
rehashidx
设为-1
.
渐进式rehash过程中会维护 ht[0] 和 ht[1] 两个hash表, 字典的删除、查找、更新等操作都会在两个hash表上进行, eg. 查找: 会先在 ht[0] 上查找、若未找到则 到 ht[1] 上查找
但: 插入操作只会在 ht[1] 上操作
1 | ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0]; |
字典API
函数 | 作用 | 时间复杂度 |
---|---|---|
dictCreate | 创建一个新的字典 | O(1) |
dictAdd | 将给定键值对添加到字典 | O(1) |
dictReplace | 添加or替换 | O(1) |
dictFetchValue | 返回给定键的值 | O(1) |
dictGetRandomKey | 随机返回一个键值对 | O(1) |
dictRelease | 释放给定字典, 及字典中包含的所有键值对 | O(N), N为值的数量 |
dict总结
- 广泛用于实现Redis的各种功能, 包括数据库和hash键
- 作为hash表的底层实现, 每个字典有两个hash表、一个平时使用, 一个 rehash 时使用
- 使用
Murmurhash2
计算key的hash值 - 使用链地址法解决键冲突、同一个索引上的多个键值对连接成一个单链表
- rehash是渐进式完成、非一次性工作