字典哈希表的实现原理_Redis设计与实现字典

1、字典的定义以及作用

1.1、什么是字典?

字典,又称为符号表(symbol table)、关联数组(associative array)或映射(map),是一种用于保存键值对的抽象数据结构,即,一个键(key)可以和一个值(value)进行关联,将键映射为值。

1.2、有什么作用?

字典在Redis中的应用非常广泛,比如Redis的数据库就是使用字典来作为底层实现的,对数据库的增删改查操作等,还有哈希对象。

2、字典的实现

Redis的字典使用哈希表作为底层实现,一个哈希表里可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对。

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

table属性是一个数组,每一个dictEntry结构都保存着一个键值对。掩码如何和hash值一起决定一个键应该被放到table数组的哪个索引上面?

fc47b9c2d957b2ccbfee3c393193c0eb.png

2.2、哈希表节点
typedef struct dictEntry{  void *key;  union{    void *val;    uint64_tu64;    int64_ts64;  }v;  //指向下一个哈希表节点,形成链表  struct dictEntry *next;}dictEntry;

next属性是指向另一个哈希表节点的指针,这个指针可以将多个哈希值相同的键值对连接在一起,以此解决键冲突的问题。

key属性保存着键值对中的键,而v属性则保存着键值对中的值,可以是一个指针,也可以是整数。

308c1af9f00bc4cc38194d91435fe91e.png

2.3、字典
typedef struct dict{  //类型特定函数  dictType *type;  //私有数据  void *privdata;  //哈希表  dictht ht[2];  //rehash索引、当rehash不再进行时,值为-1  int rehashidx;}dict;
typedef struct dictType{  //计算哈希值的函数  unsigned int (*hashFunction)(const void *key);  //复制键的函数  void *(*keyDup)(void *privdata,const void *key);  //复制值的函数  void *(*valDup)(void *privdata,const void *obj);  //对比键的函数  int (*keyCompare)(void *privdata,const void *key);  //销毁键的函数  void (*keyDestructor)(void *privdata,void *key);  //销毁值的函数  void (*valDestructor)(void *privdata,void *obj);}dictType

ht是一个哈希表数组,长度为2,一般情况下字典只使用ht[0]哈希表,ht[1]哈希表只会对ht[0]哈希表rehash时使用。而rehashidx则记录了rehash目前的进度,如果目前没有在进行rehash,它的值为-1;

  • type属性是一个指向dictType结构的指针,每个dictType结构保存了一簇用于操作特定类型键值对的函数,Redis会为用途不同的字典设置不同的类型特定函数。

  • privdata属性则保存了需要传给不确定类型特定函数的可选参数

type属性和privdata属性是针对不同类型的键值对,为创建多态字典而设置的(就是说支持多种数据类型吧,此时还是不确定的类型):

1f30911c903c5daabdb7c3bdecd7b796.png

3、解决冲突

Redis用的hash算法是murmurhash2,即使输入的键是有规律的,算法仍能给出一个很好的随机分布性,并且算法的计算速度也非常快。

Redis用链地址法来解决键冲突。但是因为dictEntry节点组成的链表是没有指向链表表尾的指针,所以为了速度考虑,采用的是头插法。

3.1、rehash

随着操作的不断执行,哈希表保存的键值对会逐渐地增多或者减少,为了让哈希表的负载因子(load factor)维持在一个合理的范围,会对哈希表进行相应的扩展或者收缩。

(1)为字典的ht[1]哈希表分配空间,这个值取决于是要扩展还是收缩。以及当前ht[0].used的数量

  • 如果是扩展,那么ht[1]的大小为第一个大于等于ht[0].used*2的2^n;

  • 如果是收缩,那么ht[1]的大小为第一个大于等于ht[0].used的2^n;

ps:这里的扩缩容原理应该和hashmap的差不多,而且还用到了掩码,即公式hash%len=hash&(len-1)其中len要为2^n.

(2)将保存在ht[0]上的键值对,通过重新计算散列值和数组下标,rehash到ht[1]上,然后ht[0]的值被释放,ht[0]设置为ht[1],为下一次rehash作准备。

bfcb31ff9e26c020e0cf89ad07bd4ca6.png

rehash需要满足以下条件:

  • 服务器目前没有执行BGSAVE(rdb持久化)命令或者BGWRITEAOF(AOF文件重写)命令,且哈希表的负载因子大于等于1

  • 服务器目前正在执行BGSAVE或者BGWRITEAOF命令,且哈希表的负载因子大于等于5

  • 当负载因子小于等于0.1时,程序自动执行收缩操作

根据BG类命令是否正在执行,服务器扩展操作所需的负载因子并不相同,这是因为在执行BG类命令时,Redis需要创建当前服务器进程的子进程,而大多数操作系统采用写时复制(copy-on-write)来优化子进程的使用效率,所以在子进程存在期间,服务器会提高执行扩展操作所需的负载因子,尽可能地避免在子进程存在期间进行哈希表扩展操作。

3.2渐进式rehash

rehash动作并不是一次性、集中式地完成的,而是分多次、渐进式地完成的,用rehashidx判断是否正在rehash。

对于rehash我们思考一个问题如果散列表当前大小为 1GB,要想扩容为原来的两倍大小,那就需要对 1GB 的数据重新计算哈希值,并且从原来的散列表搬移到新的散列表。这种情况听着就很耗时,而生产环境中甚至会更大。为了解决一次性扩容耗时过多的情况,可以将扩容操作穿插在插入操作的过程中,分批完成。当负载因子触达阈值之后,只申请新空间,但并不将老的数据搬移到新散列表中。当有新数据要插入时,将新数据插入新散列表中,并且从老的散列表中拿出一个数据放入到新散列表。每次插入一个数据到散列表,都重复上面的过程。经过多次插入操作之后,老的散列表中的数据就一点一点全部搬移到新散列表中了。这样没有了集中的一次一次性数据搬移,插入操作就都变得很快了。

   Redis为了解决这个问题采用渐进式rehash方式。以下是Redis渐进式rehash的详细步骤:

  1. ht[1] 分配空间, 让字典同时持有 ht[0]ht[1] 两个哈希表。

  2. 在字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 ,表示 rehash 工作正式开始。

  3. 在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增1。

  4. 随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。

说明:

1.因为在进行渐进式 rehash 的过程中,字典会同时使用 ht[0] 和 ht[1] 两个哈希表,所以在渐进式 rehash 进行期间,字典的删除(delete)、查找(find)、更新(update)等操作会在两个哈希表上进行。

2. 在渐进式 rehash 执行期间,新添加到字典的键值对一律会被保存到 ht[1] 里面,而 ht[0] 则不再进行任何添加操作:这一措施保证了 ht[0] 包含的键值对数量会只减不增,并随着 rehash 操作的执行而最终变成空表。

4、字典API

0787e273569f0bb4d84f8b2b6cc72d2c.png

04105e93394437a07497468d675de7ab.png

参考来源:https://www.cnblogs.com/hunternet/p/9989771.html

参考书籍:Redis设计与实践

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值