Redis是一个键值数据库而键与值的映射关系,就是通过Dict实现的。
Dict的组成
- 哈希表(DictHashTable)
- 哈希节点(DictEntry)
- 字典(Dict)
当我们向Dict添加键值对时,Redis首先根据key算出hash值(h),然后利用 h & sizemask(掩码,固定是Dict的数组长度-1)来计算元素应该储存到数组中的哪个索引位置。为什么是求与运算?(因为掩码刚刚好是数组长度 - 1,而此时求&运算恰好等价于求余)
Dict处理hash冲突存值问题
Dict采用了拉链法的实现,而且是头插法,这样做避免了key不同,但hash值相同时导致的数据覆盖问题
Dict的扩容
对于用哈希表构成的Dict,扩容问题是我们必须要面对的,由于Dict采用了拉链法,这个"拉链"是通过链表实现的,当存入的key非常多时,必然会有大量的哈希冲突,并且由于key不同,必然会存入"拉链",当链表长度过长时,必然会导致查询效率大大降低,解决这个问题就只有扩容哈希表(DictHashTable)这个方法。
Dict在每次新增键值对时都会检查负载因子(LoadFactor = used(当前键值对的数量) / size(哈希表的大小)),满足以下两种情况时会触发哈希表扩容:
- 哈希表的负载因子 >= 1,且服务器没有执行BGSAVE或者BGREWRITAOF等后台进程(这些后台进程比较吃性能,可能会导致扩容满而出现主进程阻塞)
- 哈希表的负载因子 > 5(此时肯定出现了对某些哈希槽位的性能影响)
Dict的收缩
Redis对于内存方面非常的"偏执",在每次删除元素时,也会对负载因子做检查,当LoadFactor < 0.1 时,会进行哈希表收缩。
rehash
Dict扩容和收缩中,对于性能影响最大的就在于rehash。以下是Redis的rehash实现思路(渐进式rehash)
- 计算新hash表的realeSize,值取决于当前要做的是扩容还是收缩
- 扩容:新size为第一个大于等于used + 1的
- 收缩:新size为第一个大于等于used的(不得小于4)
- 按照新的realeSize申请内存空间,创建dictht(新的哈希表),并赋值给dict.ht[1] (dict有两个哈希表,0是使用的,1是用来做rehash的)
- 设置dict.rehashidx = 0,标志rehash正在进行
- 每次crud,都检查一下dict.rehashidx是否大于-1,如果是则将dict.ht[0].table[rehashidx]的entry链表rehash到dict.ht[1]中,并且rehashidx++。直到所有数据都迁移完成。(分步完成rehash,避免因为一次rehash的数量过多,速度慢,阻塞了主线程,保障了高性能。原哈希表有多长就需要rehash多少次)
- 将dict.ht[1]赋值给dict.ht[0],给dict.ht[1]初始化为空,释放原来的dict.ht[0]的内存
在操作4中,有一段时间是部分数据在0号位置,部分在1号中,此时除了插入直接插到ht[1]中,其他都需要从两个哈希表中寻找。