文章目录
-
-
- 一、为什么需要rehash?
- 二、Redis哈希表的基础结构
- 三、rehash的完整流程
-
- [1. 触发rehash(确定新容量)](#1. 触发rehash(确定新容量))
- [2. 初始化ht[1]](#2. 初始化ht[1])
- [3. 渐进式迁移元素](#3. 渐进式迁移元素)
- [4. 迁移完成,切换哈希表](#4. 迁移完成,切换哈希表)
- 四、rehash期间的操作处理
- 五、核心优势:渐进式rehash
- 总结
-
在Redis中,
rehash
(重新哈希)是哈希表(如字典dict
,Redis中存储键值对的核心结构)调整容量以维持高效查询的过程。当哈希表中的元素数量过多或过少时,通过rehash
调整哈希表大小,平衡空间利用率和查询效率。
一、为什么需要rehash?
哈希表的性能依赖于负载因子 (
load factor
):
负载因子 = 哈希表已保存的节点数量 / 哈希表的大小
- 若负载因子过大(元素过多):哈希冲突概率增加,查询、插入等操作效率下降(最坏情况从O(1)退化为O(n))。
- 若负载因子过小(元素过少):哈希表空间浪费严重。
因此,Redis需要通过
rehash
动态调整哈希表大小,使负载因子维持在合理范围(通常0.1 ~ 5
之间)。
二、Redis哈希表的基础结构
Redis的字典(
dict
)底层由两个哈希表 (ht[0]
和ht[1]
)组成,每个哈希表包含:
table
:数组(哈希桶),每个元素指向一个链表(解决哈希冲突)。size
:哈希表大小(table
数组的长度,总是2的幂,便于位运算)。used
:已存储的节点数量。平时仅使用
ht[0]
,ht[1]
仅在rehash
时临时使用。
三、rehash的完整流程
Redis的
rehash
是渐进式(incremental) 的,避免一次性迁移所有元素导致的性能阻塞(尤其在数据量大时)。步骤如下:
1. 触发rehash(确定新容量)
当满足以下条件时,触发rehash:
- 扩容:
- 负载因子 ≥ 1,且Redis没有执行
BGSAVE
/BGREWRITEAOF
(后台持久化);- 或负载因子 ≥ 5(无论是否在持久化,强制扩容)。
新容量为
ht[0].size
的最小2的幂倍数 (确保size
是2的幂),且满足新容量 > ht[0].used * 2
。
- 缩容:
- 负载因子 ≤ 0.1时,触发缩容。
新容量为ht[0].used
的最小2的幂倍数 (确保新容量 ≥ ht[0].used
)。
2. 初始化ht[1]
为
ht[1]
分配新容量(即步骤1确定的大小),初始化其table
数组。
3. 渐进式迁移元素
Redis不会一次性将
ht[0]
的所有元素迁移到ht[1]
,而是分多次、逐步迁移,避免单线程阻塞:
- 维护一个
rehashidx
计数器(初始为0),标记当前迁移到ht[0].table
的哪个索引位置。- 每次对字典执行增删改查操作 时,除了完成当前操作,还会顺带将
ht[0].table[rehashidx]
桶中的所有元素迁移到ht[1]
,然后rehashidx += 1
。- 此外,Redis会在定时任务中批量迁移一部分元素(加速rehash过程)。
4. 迁移完成,切换哈希表
当
ht[0]
的所有元素都迁移到ht[1]
后:
- 释放
ht[0]
的内存。- 将
ht[1]
赋值给ht[0]
,重置ht[1]
为空表。- 重置
rehashidx = -1
(标记rehash结束)。
四、rehash期间的操作处理
在rehash过程中(
rehashidx != -1
),字典的所有操作(增、删、改、查)需同时涉及ht[0]
和ht[1]
,确保数据一致性:
- 查询 :先查
ht[0]
,若未找到再查ht[1]
。- 插入 :直接插入到
ht[1]
(避免ht[0]
再次增长,加速迁移)。- 删除/修改 :若元素在
ht[0]
则操作ht[0]
,若在ht[1]
则操作ht[1]
。
五、核心优势:渐进式rehash
Redis的渐进式rehash避免了传统哈希表"一次性迁移"的性能问题:
- 分散迁移压力:将迁移工作分摊到多次操作和定时任务中,每次迁移耗时极短(微秒级)。
- 不阻塞服务:整个过程中,Redis仍能正常处理客户端请求,几乎无感知。
总结
Redis的rehash是通过渐进式迁移实现的哈希表容量调整机制:
- 基于负载因子触发,动态扩容或缩容。
- 使用两个哈希表(
ht[0]
和ht[1]
),逐步迁移元素。- 迁移期间正常处理请求,兼顾效率与可用性。
这种设计使Redis在面对大规模数据时,仍能维持高效的哈希表操作性能。