Redis数据结构——字典

字典是一种用来保存键值对的数据结构。
在字典中,一个key与一个value相对应,字典中的key是唯一的。
在Redis中字典使用哈希表作为底层实现,用数组来表示一个哈希表,每个元素都是一对key-value

同样,在Redis中字典由三部分组成:

  • 哈希节点,保存一对key-value
  • 哈希表,用来爆粗整个哈希表以及相关信息
  • 字典用来封装哈希表

字典的实现

哈希节点

typeof struct dictEntry{
	void *key; // 键key 

	union v ;	// 值value 
	
	struct dictEntry *next; // 指向下一个哈希节点,形成链表 
} dictEntry;

哈希节点中的这三个属性没啥说的,看过HashMap源码的,应该都懂。

哈希表

哈希表的结构体:

typeof struct dictht {
	dictEntry **table; // 哈希表数组
	
	unsigned long size; //哈希表大小
	
	unsigned long sizemask; // 掩码,用来计算索引
	
	unsigned long used; // 哈希表中已使用的节点数量 
} dictht;

还是来说一下这个结构:

  • table属性是一个数组,数组中的每个元素都是指向dictEntry哈希表节点的指针
  • sizemask用来快速计算一个哈希节点的索引

字典

字典的底层实现是哈希表,字典结构体还保存着整体的信息,还字典的结构体:

typeof struct dict {
	// 哈希表
	dictht ht[2];
	
	// 类型特定函数,针对不同类型的key-value操作的一系列函数,不必关心 
	dictType *type; 
	
	// 私有数据,不必关心 
	void *private; 
	
	// rehash索引 
	int rehashidx; 
} dict;

在字典的结构体中,有两个属性比较重要:

  • dictht ht[2],这是两个哈希表。我们前面讲解了哈希表结构体dictht,一个字典中竟然封装了两个哈希表,主要用来rehash,接下来说。
  • int rehashidx 在rehas时指向旧哈希表的一个指针,关于rehash接下来就说。

关于哈希表节点、哈希表、字典的关系,看这张图梳理一下:

hash冲突

谈到哈希表,那么hash冲突是绕不开的一个点。
哈希冲突:当两个不同的key,通过哈希运算,被分配到了统一索引上,这就是哈希冲突。

那么Redis中是如何解决哈希冲突的呢?
Redis中的哈希表与Java中的HashMap的原理相似,都是通过链表法来解决key冲突。
我们知道每个链表节点dictEntry中,都有一个next属性,这个属性用来构成单向链表,把分配到同一个索引上的节点通过此属性连接起来
因为在dictEntry中只有一个next指针,所以为了性能考虑,Redis哈希表采用了头插法,时间复杂度是O(1),排在其已有节点的前面。

Redis中使用了MurmurHash算法,即使对于相同的输入,也能够保证很好的随机分布性,而且计算速度非常快。

rehash

rehash也是哈希表绕不开的一个点。
随着操作的不断进行,哈希表中的键值对数量会逐渐增多或减少,为了让哈希表的负载因子load factor(负载因子,用来衡量哈希表满的程度)维持在一个合理的范围,当哈希表中保存的键值对的数量太多或太少时,就会触发rehash,来对哈希表进行适应的扩容或收缩。

负载因子 = 哈希表中已保存的数量 / 哈希表容量

我们前文提到,在字典中定义了两个哈希表,即dictht ht[2],在正常使用时,我们是把键值对分配到第一个哈希表中,也就是ht[0]中,当rehash时,第二个哈希表ht[1]就发挥作用了。
rehash的具体过程:

  1. 首先为ht[1]分配空间,ht[1]的大小取决于原先的ht[0]中键值对的数量:
    • 如果是扩容操作,那么ht[1] 的大小是第一个大于等于ht[0].used*2的2^n。
    • 如果是收缩操作,那么ht[1]的大小是第一个等于ht[0].used的2^n
  2. ht[1]分配空间完成后,就是键值对迁移的过程了。将ht[0]中的所有键值对再次hash放到ht[1]中,rehash就是重新计算哈希值和索引值,然后放到ht[1]的指定位置上。
  3. ht[0]中的键值对全部迁移到了ht[1]之后,释放ht[0],将ht[1]作为ht[0],并在ht[1]创建一个新的空白的哈希表,留着下一次rehash使用

何时对哈希表进行扩容操作?满足以下任意一个条件

  • redis服务器目前没有执行BGSAVE(rdb持久化)命令或BGREWRITEAOF(AOF文件重写)命令,并且此时哈希表的复杂因子大于等于1.
  • 服务器正在执行BGSAVE(rdb持久化)命令或BGREWRITEAOF(AOF文件重写)命令,并且哈希表的复杂因子大于等于5

当负载因子小于0.1时,会自动进行收缩操作

渐进式rehash

刚才说了rehash的基本流程,对于第二个过程,将ht[0]中的键值对rehash到ht[1]上这一个过程,并不是一次完成的。
因为考虑到哈希表中的键值对数量过多,一次性完成可能会占用非常长的时间,由于redis是单线程的,同时还要对外提供服务,如果rehash是一次性完成的,那么在rehash期间,Redis必须停掉服务。
因此,为了避免rehash对服务器性能造成的影响,服务器并不是一次性将ht[0]中的键值对迁移到ht[1]中的,而是分多次、渐进式、慢慢地rehash到ht[1]上的

渐进式rehash的详细过程:

  1. ht[1]分配空间,字典同时持有ht[0]ht[1]两个哈希表
  2. 在字典内部维护一个索引计数器变量,就是前文提到的rehashidx,将它设置为0(即ht[0]中第一个键值对的索引),表示rehash工作开始。
  3. 在rehash期间,每次对字典进行增、删、改、查操作时,还会顺带着将ht[0]哈希表在rehashidx指向的键值对rehash到ht[1]中,当这一次操作完成后,rehashidx + 1,指向下一个需要rehash的键值对。
  4. 随着对字典的操作不断执行,最终ht[0]中所有的键值对都会迁移到ht[1]中,这时rehashidx为-1,表示rehash已经全部完成。

在渐进式rehash的过程中,对字典的新增操作,是直接在ht[1]新的哈希表上进行,而删除、查找、更新操作是在这两个哈希表上进行的
例如,当查找一个元素时,先去ht[0]上查找,如果没有找到,再去ht[1]上查找。
随着rehash的进行,ht[0]最终变成空表。

总结

在这一篇中,说了很多,大致总结一下:

  • 字典是一种存储键值对数据的结构
  • 在Redis中,字典是由哈希表实现的,字典相关的结构体有三个:
    • dictEntry哈希表中的一个节点,就是一个键值对
    • dictht哈希表,有多个dictEntry组成的数组来表示哈希表
    • dict字典,其中保存了两个哈希表ht[2],在rehash时使用
  • 哈希表的rehash操作并不是一次性完成的,而是渐进式地,伴随着每次对哈希表的操作,顺带迁移一个键值对,直至完成。

参考文章

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值