结构
最外层封装了dictht,结构如下
table指向了实际存储的hash结构dictEntry。size是哈希表大小,也就是说dictEntry有多少空间。sizemask是掩码,为固定值size-1,然后元素的index就应该是元素哈希值&sizemask。used代表dictEntry里面多少个坑位已经被用上了。假设index冲突了,用拉链法(头插,避免尾插法遍历到尾巴这一O(n)的时间开销)解决。
扩容机制
触发扩容机制有两种情况,一种是负载因子>=1且没有在执行BGSAVE或BGREWRITEAOF命令,另一种是负载因子>=5。负载因子就是used/size
分别说明如下:
- 负载因子>=1,说明此时基本dictEntry上每个坑位都有一个链表了,查找时候时间复杂度达不到O(1)了,假设没在执行BGSAVE或者BGREWRITEAOF就进行扩容。因为扩容是主进程推进的,这俩机制会创建子进程,假设此时扩容,相当于主进程在写入,子进程还在复制,内存开销很大
- 负载因子>=5,那此时哈希表已经不堪重负了,无论如何顶着压力也得扩容。
那么具体扩容机制是啥呢?其实是叫做渐进扩容,就是不是说一口气把旧表迁移过去新表,这样数据多的话会阻塞。实际的思想是一种顺带着做的感觉,就是每次增删改查哈希表时候顺带着迁移一下(注意,这里意思是,步骤3hashtable里有[a, b, c, d]我可能rehashindex是0,那我此时要迁移的是a,时机是我要增删改查一个元素(可能是b或者c都有可能)了)。
以下是哈希表渐进式 rehash 的详细步骤:
- 为 ht[1] 分配空间, 让字典同时持有 ht[0] 和 ht[1] 两个哈希表。
- 在字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始。
- 在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增一。
- 随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。
注意
因为在进行渐进式 rehash 的过程中, 字典会同时使用 ht[0] 和 ht[1] 两个哈希表, 所以在渐进式 rehash 进行期间, 字典的删除(delete)、查找(find)、更新(update)等操作会在两个哈希表上进行: 比如说, 要在字典里面查找(删除和修改本质也需要先找到)一个键的话, 程序会先在 ht[0] 里面进行查找(还没被迁移走), 如果没找到的话(已经被迁移走了), 就会继续到 ht[1] 里面进行查找。
另外, 在渐进式 rehash 执行期间, 新添加到字典的键值对一律会被保存到 ht[1] 里面, 而 ht[0] 则不再进行任何添加操作: 这一措施保证了 ht[0] 包含的键值对数量会只减不增, 并随着 rehash 操作的执行而最终变成空表。