Redis 使用哈希表作为其底层数据结构之一来存储键值对。为了保证高效的查找性能,Redis 会根据哈希表中的条目数量动态调整哈希表的大小,这个过程称为 rehash(重新散列)。
为什么需要 Rehash?
- 减少哈希冲突:当哈希表中的元素过多时,哈希冲突的概率会上升,这会导致链式或开放寻址等解决冲突的方法效率降低。
- 保持高效性:通过维持一个较低的负载因子(load factor),即哈希表中元素数量与桶数量的比例,可以确保操作如插入、删除和查找能够保持在 O(1) 的时间复杂度。
Rehash 的触发条件
- 扩容:当哈希表的负载因子超过一定阈值(默认为 0.7,可以通过配置项
hash-max-zipmap-entries和hash-max-zipmap-value来调整)时,Redis 会创建一个新的更大的哈希表,并将旧表中的所有元素迁移到新表中。 - 缩容:当哈希表中的元素数量低于某个阈值(通常是当前容量的 10%),并且当前容量大于最小容量时,Redis 也会进行缩容操作,以节省内存。
Rehash 的过程
Rehash 是一个渐进的过程,这意味着它不会一次性完成,而是分多次逐步完成。这是为了避免在哈希表很大时,一次性的 rehash 操作导致服务器阻塞。
- Redis 会在每次处理客户端请求之后,执行一次小批量的数据迁移。
- 它会计算出一个批次大小,然后从旧哈希表中移动指定数量的元素到新哈希表。
- 这个过程会持续进行,直到所有的元素都被迁移到新的哈希表中。
- 在整个 rehash 过程中,Redis 仍然可以正常处理客户端请求,同时逐渐完成 rehash。
渐进式 Rehash 的优点
- 避免阻塞:渐进式 rehash 可以防止在大型哈希表上一次性 rehash 导致的长时间停顿。
- 平滑性能:通过分散 rehash 的工作量,可以使得 Redis 在 rehash 期间仍能保持较为稳定的性能。
Rehash 期间的数据读写
- 读操作:在 rehash 期间,Redis 会同时维护两个哈希表(旧表和新表)。当客户端请求读取数据时,Redis 首先会在新表中查找键;如果找不到,则会到旧表中查找。这种方式保证了所有已存在的键都能被正确访问。
- 写操作:对于写操作(包括插入、更新或删除),Redis 会直接将数据添加到新的哈希表中。如果该键已经在旧表中存在,那么旧表中的相应条目会被标记为已迁移,并且不再参与后续的操作。这样,在 rehash 完成后,所有的新数据都只会存在于新表中。
渐进式 Rehash 的具体步骤
- 初始化:当触发 rehash 条件时,Redis 会创建一个新的更大的哈希表(扩容)或更小的哈希表(缩容)。
- 迁移数据:每次处理完一个客户端命令后,Redis 会执行一次小批量的数据迁移,从旧表移动一部分数据到新表。
- 双重查找:在 rehash 期间,Redis 在处理读请求时需要检查两个哈希表。
- 仅向新表写入:所有写操作只作用于新表。
- 完成 rehash:一旦所有的数据都被迁移到新表,旧表会被释放,rehash 完成。
性能影响
- 读操作:由于需要在两个哈希表之间进行查找,读操作可能会稍微变慢。但是,由于 rehash 是逐步进行的,通常这个影响是微乎其微的。
- 写操作:写操作的性能几乎不受影响,因为它们总是直接写入新表。
注意事项
- 在 rehash 期间,内存使用量可能会暂时增加,因为此时系统中同时存在两个哈希表。这可能会影响内存受限环境下的性能。
- 如果有大量的写操作发生在 rehash 期间,可能会加速 rehash 的进程,因为每次写操作都会推动一些数据从旧表迁移到新表。