Redis 字典rehash过程

一、字典简介

Redis数据库本身就是一个大的字典,也就是保存了一个一个的键值对。例如直接执行SET命令就是在Redis的字典中插入一个键值对

redis> SET msg "hello world"
OK

这里插入了一个key为msg,value为"hello world"的键值对。

字典同样也是Redis常用数据结构HashTable的实现之一。例如执行命令:

127.0.0.1:6379>  HMSET user name "zhangsan" school "abc school" age 20 
OK
127.0.0.1:6379>  HGETALL user
1) "name"
2) "school"
3) "age"

通过HMSET向user这个HashTable中插入了多个键值对。通过HGETALL获取user中所有key的名称。

1. 哈希表(HashTable)的定义

Redis中的HashTable通常使用字典作为底层实现。但是在同时满足下面两个条件时,会使用ziplist(压缩列表)作为底层实现:

  • 哈希中元素数量小于512个;
  • 哈希中所有键值对的键和值字符串长度都小于64字节。

哈希表的数据结构定义:

typedef struct dictht{
	//哈希表数组
	dictEntry **table;
	//哈希表大小
	unsigned long size;
	//哈希表大小掩码,用于计算索引值。总是等于size-1
	unsigned long sizemask;
	//哈希表已有节点的数量
	unsigned long used;
} dictht;

table是一个dictEnty数组,数组中的每个位置都是一个dictEntry元素(也有可能是Null),dictEntry其实就是一个键值对的数据接口,下面细说。然后size表示哈希表的大小,也就是table数组的长度。used表示字典中所有dictEntry节点的数量。sizemask则使用时size-1,它其实是和key的哈希值一起决定这个key会存放到table中的哪个位置。
ht
哈希表节点dictEntry的数据结构定义:

typedef struct dictEntry{
	//键
	void *key;
	//值
	union {
		void *val;
		uint64_tu64;
		int64_ts64;
	}v;
	//指向下一个节点的指针
	struct dictEntry *next;
} dictEntry;
2、字典的定义

字典的数据结构如下:

typedef struct dict{
	//类型特定函数
	dictType *type;
	//私有数据
	void *privdata;
	//哈希表数组
	dictht ht[2];
	//rehash索引。-1表示不在进行rehash操作。
	int trehashidx;
} dict;

这里字典中的哈希表为什么是两个呢?其实一般情况下只会使用第一个哈希表ht[0],但是如果要进行rehash操作的时候,就会使用到第二个哈希表ht[1]了。下图展示了一个普通状态(不在进行rehash操作)的字典:
dict

二、字典的rehash过程

1. hash过程

有rehash过程必然就有hash过程,hash过程就是第一次插入键值对的过程。如果现在要向字典中插入一个键值对,那么插入哪个位置呢?这就需要哈希算法来判断了。Redis中根据键计算出一个哈希值和一个索引值,再根据这个索引值放到对应的哈希表数组的指定索引上,其实就是计算出一个位置,放到哈希表数组对应的位置上。Redis计算哈希值和索引值的算法如下:

  • 哈希值:hash=dict->type->hashFunction(key);
  • 索引值:index=hash & dict->ht[x].sizemask;根据情况不同ht[x]可能是hx[0]或hx[1]

另外对键冲突的处理方法,Redis很显然就是通过链地址法来解决的,多个键计算得到的索引相同,就通过next指针连接它们,插入到链表尾部。

2. rehash过程

假设一开始字典中的哈希表大小只有4,但是随着数据的不断插入,数据量越来越多。不仅冲突会频繁发生,查找性能也会降低。因此需要对将字典中的哈希表的负载因子(load factor)保持在一个合理范围之内。也就是需要rehash操作了。同理,当数据减少的时候,也需要进行rehash。

Redis触发rehash条件

  • 扩容:服务器目前没有在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于或等于1。服务端目前正在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于或等于5。
  • 缩容:当哈希表的负载因子小于0.1时,redis会自动开始对哈希表进行缩容操作。

Redis对字典中哈希表的reshah步骤如下:
(1)为字典中的哈希表ht[1]分配空间,这个分配的空间大小取决于是扩容还是缩容以及当前used的值:

  • 如果是扩容,那么ht[1]的大小就是第一个大于等于ht[0].used*2 2 n 2^n 2n(大于等于2倍used的2的n次方幂);
  • 如果是缩容,那么ht[1]的大小就是第一个大于等于ht[0].used 2 n 2^n 2n(大于等于used的2的n次方幂);
    (2)为ht[1]分配完空间之后,将ht[0]中的所有dictEntry根据ht[1]的大小重新计算哈希值和索引值,然后移动到ht[1]对应的索引位置上。
    (3)当ht[0]上的所有键值对rehash完成之后,将ht[1]变为新的ht[0],并且释放原始的ht[0]的空间,同时将原始的ht[0]变为ht[1]

Redis的这个rehash过程有点类似于CopyOnWriteArrayList的思想了。

3. 渐进式rehash过程

因为实际场景中Redis中保存的数据量可能及其大,如果想要一次性完成rehash过程可能非常影响性能。所以Redis在执行rehash操作的时候实际上是分多次、渐进式完成的。渐进式rehash的过程:

(1)为ht[1]分配空间
(2)将字典中的变量trehashidx修改为0,表示rehash操作开始执行
(3)在rehash执行期间,如果发生了对字典的增删改查操作的话,会这样执行:

  • 新增操作:直接将键值对插入到ht[1]上,保证ht[0]的结点不会增加
  • 删除操作:同时在ht[0]和ht[1]两个哈希表上执行,避免漏删;
  • 修改操作:同时在ht[0]和ht[1]两个哈希表上执行,避免漏改;
  • 查找操作:先从ht[0]查,查不到的话再去ht[1]查;
    (4)在执上述操作的时候,还会顺带将ht[0]中trehashidx索引位置上的所有键值对rehash到ht[1]上,每次执行完这个子步骤都会讲trehashidx自增1`
    (5)等到ht[0]上的所有键值对都rehash到ht[1]上之后,将trehashidx修改为-1,表示rehash过程结束。

这种渐进式rehash的过程采取了分而治之的方式,将rehash的整个大过程分摊到了每次增删改查操作上,避免了集中rehash带来的庞大计算量。


THE END.

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值