底层数据结构
// 字典本身数据结构
struct dict {
dictType *type;
void *privdata;
dictht ht[2]; // 2个哈希表,真正存储数据的地方;正常情况只会用其中一个,另一个在渐进式扩容时使用
int rehashidx; // 渐进式hash的进度,其他情况下为-1
}
// 单个哈希表数据结构
struct dictht {
dictEntry** table; // dictEntry指针列表
long size; // 第一维数组的长度
long used; // hash表中的元素个数
}
// 单个哈希表中元素的数据结构
struct dictEntry {
void* key; // 当前元素的key值
void* val; // 当前元素的value值
dictEntry* next; // 指向下一个元素的指针
}
redis底层hashtable使用分桶的方式解决hash冲突。第一维是数组,第二维是链表,其hash结构大致如图:
渐进式rehash
渐进式rehash的原因
扩容/缩容的操作需要重新申请一块更大/更小的内存空间,然后将hash中的所有key一个个复制到新的hash中,这是一个O(n)的操作,如果遇到比较大的字典,整个复制过程耗时很久;redis又是一个单线程服务,如果线程陷于字典的复制,则不能处理其他的请求,所以需要把hash复制的操作分散开,尽量不影响redis单线程对外提供服务
rehash过程
redis的dict有两个哈希map: ht[2]
, 假设ht[0]
正在使用,ht[1]
为空,此时需要扩容,其过程如下:
- 为ht[1]分配空间,让dict字典同时持有 ht[0] 和 ht[1] 两个哈希表。
- 在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash工作正式开始。
- 在rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将ht[0]哈希表在 rehashidx索引(table[rehashidx]桶上的链表)上的所有键值对rehash到ht[1]上,当rehash工作完成之后,将rehashidx属性的值增一,表示下一次要迁移链表所在桶的位置。
- 随着字典操作的不断执行,最终在某个时间点上,ht[0]的所有桶对应的键值对都会被rehash至ht[1],这时程序将rehashidx属性的值设为-1,表示rehash操作已完成。
定时rehash
上述rehash过程依赖于后续客户端对字典的增删改查,如果在rehash刚发生后,客户端停止请求这个map,这种情况会导致dict一致保持两个hashtable吗?不会,此时定时任务会对字典进行主动搬迁
关于定时任务,其执行时机可以在前一篇笔记io模型中查到踪迹;大致情形是,redis工作线程在每次事件循环执行完read和write后,会执行一些其他任务,其中就包括定时任务执行
注:知识点之间的连线很重要,可以让我们对其有更系统的了解,这里算是有所体会
rehash条件
redis的hashtable是基于分桶式链表解决hash冲突的,当哈希表中数据量过大是,桶链表很长,这样查询过程会变得很慢,此时需要对哈希表扩容;相反,如果哈希表中的元素非常少,过大的一维数组会浪费内存空间,此时会触发哈希表缩容;redis中对哈希表的扩容和缩容条件都有比较明确的标准
扩容条件
- 当hash表中元素个数等于第一维数组的长度时,会触发扩容,扩容的新数组是原数组两倍
- 当遇到Redis正在做rdb快照bgsave时,redis为了减少内存页复制(COW),会尽量不做扩容
- 但,当元素个数超过第一维数组长度的5倍时,redis会强制哈希表扩容
缩容条件
- 当hash表元素个数小于第一维数组10%,redis触发hash缩容,缩容不考虑bgsave
Dict数据结构的使用
-
面向用户的hash结构,其实现即为一个
dict
-
面向用户的set结构,其底层实现也是
dict
,只不过val = NULL -
面向用户的zset结构,其底层实现是
dict
和zskiplist
的结合,其中dict
存储value和score值的映射关系struct zset { dict *dict; // all values. value => socre zskiplist *zsl; // 跳表 }
-
整个Redis数据库的所有key/value也是一个全局字典,还有一个记录所有带过期时间的key与过期时间的映射也是使用dict
struct RedisDb { dict* dict; // all keys. key => value dict* expires; // all expired keys. key => timestamp }