文章目录
关于字典这个数据结构的内容就稍微的有那么一点多了,redis数据库就可以看成是一个字典,那我们就来看看字典的内部究竟是如何实现的吧~
1 字典的实现
Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对。接下来我们一个一个的来说。
1.1 哈希表
首先,哈希表的结构大概长下边这个鬼样子:
- table: dictEntry类型的数组
- size:哈希表的大小
- sizemask:用于计算索引的掩码,永远等于size-1,和哈希值一起决定一个键放到table的哪个位置
- used:已经有的节点数量
举个实际的例子呢,就长下边这个样子:
1.2 哈希表节点(dictEntry)
哈希表中的每个节点是dictentry类型,这是个什么类型呢,他大概长下边这个鬼样子:
- key属性:保存着键值对中的键
- v属性:保存着键值对中的值,其中键值对的值可以是一个指针,或者是一个uint64_ t整数,又或者是一个int64_ t整数。
- next属性:next属性是指向另一个哈希表节点的指针,这个指针可以将多个哈希值相同的键值对连接在一起,以此来解决键冲突( collision)的问题。举个例子就是下边这个情况:
1.3 字典结构
终于,铺垫了半天大哥终于来了,让我们一睹真容吧:
- type属性:一个指向dictType结构的指针,每个dictType结构保存了一簇用于操作特定类型键值对的函数,Redis 会为用途不同的字典设置不同的类型特定函数。至于dictType大概长这个样子:
- privdata属性:则保存了需要传给那些类型特定函数的可选参数。
- ht属性:是一个包含两个项的数组,数组中的每个项都是一个dictht哈希表,一般情况下,字典只使用ht[0]哈希表,ht[1]哈希表只会在对ht[0]哈希表进行rehash时使用。rehash的事情之后再说。
- rehashidx:它记录了rehash目前的进度,如果目前没有在进行rehash,那么它的值为-1。
老规矩看个整体图吧!
2 哈希算法
这个算法有啥用呢? 简单来说我们现在要想字典里放键值对,放哪?我们要解决的就是放哪的问题,首先呢字典具有内置哈希函数会根据当前的键值对计算哈希值,接下来再根据和哈希值和刚才我们介绍的sizemask来计算我们哈希表的索引,也就是来放这个键值对的具体地方。具体来说就是如下语句:
hash = dict->type->hashFunction(k0);
index = hash & dict->ht[0].sizemask
3 解决键冲突
首先,啥叫冲突呢?就是两个键值对经过计算都想放到同一个索引(index)这就是冲突。Redis的解决办法就是链地址法,就是大家都想在这就在这,排队呗,通过next指针形成一个链表。就是下边这样子:
4 rehash
这是刚才没讲的一个知识点,字面理解就是重新做hash,为啥要重新做呢?原因有二:
- 哈希表用的太少了,浪费空间,重新搞得的时候搞小点。
- 哈希表用的太多了,都要满了,再插入新的就得通过next指针排队,那就会影响哈希表的性能了。
如此一来。我们就必须在特定情况下做rehash,那问题来了啥时候搞呢?
4.1 扩展时机
当以下条件中的任意一个被满足时,程序会自动开始对哈希表执行扩展操作:
- 服务器目前没有在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于1。
- 服务器目前正在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于5。
负载因子计算方式如下:
// 负载因子 = 哈希表已保存节点数量/哈希表大小
load_factor = ht[0].used /ht[0].size
注意: 根据BGSAVE命令或BGREWRITEAOF命令是否正在执行,服务器执行扩展操作所需的负载因子并不相同,这是因为在执行BGSAVE命令或BGREWRITEAOF命令的过程中,Redis需要创建当前服务器进程的子进程,而大多数操作系统都采用写时复制(copy-on-write)技术来优化子进程的使用效率,所以在子进程存在期间,服务器会提高执行扩展操作所需的负载因子,从而尽可能地避免在子进程存在期间进行哈希表扩展操作,这可以避免不必要的内存写人操作,最大限度地节约内存。
4.2 收缩时机
当哈希表的负载因子小于0.1时,程序自动开始对哈希表执行收缩操作。
4.3 rehash过程
时机知道了,那具体咋办呢??
- 为字典的ht[1]哈希表分配空间,这个哈希表的空间大小取决于要执行的操作,以及ht[0]当前包含的键值对数量(也即是ht [0].used属性的值):
- 如果执行的是扩展操作,那么ht[1]的大小为第一个大于等于ht [0].used*2的2^n(2的n次方幂);
- 如果执行的是收缩操作,那么ht[1]的大小为第一个大于等于ht [0] .used的2^n。
- 将保存在ht[0]中的所有键值对rehash到ht[1]上面: rehash指的是重新计
算键的哈希值和索引值,然后将键值对放置到ht[1]哈希表的指定位置上。 - 当ht[0]包含的所有键值对都迁移到了ht[1]之后(ht[0]变为空表),释放ht[0],将ht[1]设置为ht[0],并在ht[1]新创建一个空白哈希表,为下一次rehash做准备。
4.4 渐进rehash
这个简单,就是如果有很多东西需要搬家,一次是搬不完的。我们平时还要送外卖,我要是光搬家,就甭干别的了没饭吃了。所以说rehash的过程也是每次搞一点,搞完为止。具体流程如下:
- 1)为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表。
- 2)在字典中维持- 一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash工作正式开始。
- 3)在rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1],当rehash工作完成之后,程序rehashidx属性的值增一。
- 4)随着字典操作的不断执行,最终在某个时间点上,ht[0]的所有键值对都会被rehash至ht[1],这时程序将rehashidx属性的值设为-1,表示rehash操作已完成。
5 字典常用API
好了,今天又分享知识了哦~