场景
最近在读《redis设计与实现》,记录下第4章字典这个常用数据结构。
定义
字典又叫关联数组或者映射,是一种用于保存键值对的数据结构。
用途
Redis的数据库就是使用字典作为底层实现,例如执行set msg "hello world"
,在数据库中会保存为一个key为’msg‘,value为’hello world‘键值对。
实现
Redis字典底层采用哈希表实现,一个hash表有多个hash节点,每个hash节点就保存了字典中的一个键值对。
typedef struct dict{
// 类型特定函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表
// 包含2个hashTable的数组,ht[0]平常使用,ht[1]rehash时会用到
dictht ht[2];
// rehash索引,也标志着rehash的进度
// rehash不进行时为-1
int rehashidx;
} dict;
随着字典的键值对的使用,Redis为了让哈希表的负载因子维持在一个合理范围内,会对字典进行rehash操作(包括扩展和收缩)
rehash时机
什么时候进行rehash操作呢?
当满足以下任意一个条件时,程序自动进行rehash(扩展)操作:
- 如果Redis服务器没有在执行BGSAVE命令(RDB)或者BGREWRITEAOF(AOF)命令且哈希表负载因子大于等于1
- 如果Redis服务器正在执行BGSAVE命令(RDB)或者BGREWRITEAOF(AOF)命令且哈希表负载因子大于等于5
为什么当Redis在进行RDB或者AOF持久化时,需要负载因子大于1呢?最终目标是为了节省内存,Redis在进行持久化时会创建一个子进程执行任务,而大多数操作系统会采用写时复制(copy on write)来提高子进程使用效率,负载因子提高会避免在子进程存在期间进行rehash操作,避免不必要的内存写入操作。
rehash(收缩):hash表负载因子小于等于0.1。
rehash流程:
当满足rehash时机时,程序会进行rehash操作,因为Redis是单线程(epoll+事件提高效率),采用一次性把所有的键值对重写计算和放置的话,势必会有stop the world,Redis采用渐进式rehash解决了长时间的阻塞问题。
具体流程:
- 为ht[1]分配空间size,其大小取决于是扩展还是收缩以及ht[0]实际使用数量 used
扩展:size = 第一个大于等于used * 2的2的n次幂
收缩:size = 第一个大于等于used的2的n次幂
当n为2的次幂时 (n - 1) & size = n % size,即可以用与运算替换取模运算,为什么要替换,因为速度快啊,为什么快啊,可以通过查看汇编指令观察二者的CPU周期总数,前者比后者少的多,所以更快
- rehash所有ht[0]键值对,把元素放到ht[1]上
- 释放ht[0],将ht[1]设置为ht[0],在ht[1]新建一个空白hash表,为下一次rehash做准备
在渐进式rehash期间,rehashidx = 0 标志着rehash正式开始,在对字典执行删除、查找或者更新时,程序除了执行指定的操作外,会顺带将ht[0]在rehashidx索引上的所有键值对rehash至ht[1],当rehash完成后rehashidx加一,当所有ht[0]都被移至ht[1]后rehashidx置为-1。执行添加时,程序只在ht[1]添加,使得ht[0]的哈希表是逐渐减少的。
在此期间删除、查找、更新操作需要同时对ht[0]和ht[1]进行操作,以免命令不能被正确执行。