📖 前言:从 HashMap 到 Redis Hash
在 Java 的 HashMap 中,当负载因子超过阈值时,会触发一次全局 rehash ,导致所有操作暂停,这在单线程的 Redis 中是无法接受的。Redis 作为内存数据库,对响应时间要求极高(毫秒级),于是它发明了渐进式 Rehash 这一巧妙设计。
"Rome wasn't built in a day, and Redis doesn't rehash in one go."
(罗马不是一天建成的,Redis 也不会一次性完成 Rehash)
🎯 一、什么是 Rehash?为什么需要它?
1.1 哈希表的基本原理
python
# 简化的哈希表插入
hash_table = [None] * 8 # 初始化8个桶
def insert(key, value):
index = hash(key) % len(hash_table) # 计算桶位置
hash_table[index] = (key, value)
# 问题:当元素增多时,冲突加剧!
1.2 负载因子与性能关系
负载因子 = 元素数量 / 桶数量
| 负载因子 | 性能影响 |
|---|---|
| < 0.5 | 性能优秀,冲突少 |
| 0.5 - 0.75 | 性能良好,可接受范围 |
| 0.75 - 1.0 | 性能下降,冲突增多 |
| > 1.0 | 性能急剧下降,链表过长 |
| > 5.0 | 灾难性性能,查找退化为O(n) |
🔧 二、Redis Rehash 的核心设计
2.1 数据结构设计
c
// Redis 字典的核心结构(简化版)
typedef struct dict {
dictht ht[2]; // 两个哈希表,ht[0] 和 ht[1]
long rehashidx; // rehash 进度,-1 表示未进行
unsigned long iterators; // 正在运行的迭代器数量
dictType *type; // 类型特定函数
} dict;
typedef struct dictht {
dictEntry **table; // 哈希表数组(桶数组)
unsigned long size; // 哈希表大小(桶的数量,2^n)
unsigned long sizemask; // 掩码,用于计算索引(size-1)??不是很懂
unsigned long used; // 已使用节点数量
} dictht;
typedef struct dictEntry {
void *key; // 键
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v; // 值
struct dictEntry *next; // 链表下一个节点
} dictEntry;
2.2 双表机制图解
是 否 是 否 开始Rehash 创建新表ht[1] 是否在Rehash? 查询/更新:访问两个表 插入:只插入ht[1] 每次操作迁移一个桶 查询/更新:只访问ht[0] 插入:插入ht[0] 正常操作 Rehash完成? ht[0] = ht[1],清空ht[1]
🚀 三、渐进式 Rehash 工作流程
3.1 完整的 Rehash 生命周期
python
class RedisDict:
def __init__(self):
self.ht = [HashTable(), HashTable()] # 两个哈希表
self.rehashidx = -1 # -1 表示不在 rehash
def expand_if_needed(self):
"""检查是否需要扩容"""
# 如果正在 rehash,不处理
if self.is_rehashing():
return
# 1. 空表,初始化为 4
if self.ht[0].size == 0:
self.resize(4)
return
# 2. 负载因子 >= 1,且允许扩容
if self.ht[0].used >= self.ht[0].size and self.can_resize:
self.resize(self.ht[0].used * 2)
return
# 3. 负载因子 > 5(强制扩容)
if self.ht[0].used > self.ht[0].size * 5:
self.resize(self.ht[0].used * 2)
def resize(self, new_size):
"""准备新哈希表"""
# 计算实际的 size(2 的幂次)
actual_size = self.next_power_of_two(new_size)
# 初始化 ht[1]
self.ht[1] = HashTable(size=actual_size)
self.rehashidx = 0 # 开始 rehash
def incremental_rehash(self, steps=1):
"""渐进式 rehash:每次迁移 steps 个桶"""
if not self.is_rehashing():
return 0
while steps > 0 and self.ht[0].used > 0:
# 1. 找到下一个非空桶
while self.rehashidx < self.ht[0].size and \
self.ht[0].table[self.rehashidx] is None:
self.rehashidx += 1
if self.rehashidx >= self.ht[0].size:
# rehash 完成
self.complete_rehash()
return 0
# 2. 迁移这个桶的所有节点
bucket = self.ht[0].table[self.rehashidx]
while bucket:
next_node = bucket.next
# 计算在新表中的位置
new_idx = hash(bucket.key) & self.ht[1].sizemask
# 插入到新表
bucket.next = self.ht[1].table[new_idx]
self.ht[1].table[new_idx] = bucket
# 更新计数
self.ht[0].used -= 1
self.ht[1].used += 1
bucket = next_node
# 3. 清空旧桶,移动指针
self.ht[0].table[self.rehashidx] = None
self.rehashidx += 1
steps -= 1
return self.ht[0].used # 返回剩余待迁移数量
def complete_rehash(self):
"""完成 rehash"""
# 释放旧表
del self.ht[0]
# 新表变成旧表
self.ht[0] = self.ht[1]
# 重置 ht[1]
self.ht[1] = HashTable()
self.rehashidx = -1
3.2 触发时机的精确控制
负载因子 ≥ 1
且无BGSAVE 负载因子 > 5
无论有无BGSAVE 负载因子 < 0.1 大小 = used * 2 大小 = used * 2 大小 = 第一个 ≥ used 的 2^n ht[0].used == 0 检查负载因子 正常扩容 强制扩容 缩容 创建新哈希表 渐进式迁移 完成迁移 负载因子 = used / size
默认设置:
- 扩容阈值: 1.0
- 强制扩容: 5.0
- 缩容阈值: 0.1
🔄 四、数据一致性的完美保障
4.1 读写操作的详细逻辑
python
def dict_find(dict, key):
"""查找操作的 rehash 感知"""
# 1. 如果正在 rehash,先执行一步
if dict.is_rehashing():
dict.incremental_rehash(1)
# 2. 计算哈希值
h = hash_function(key)
# 3. 在两个表中查找
for table in [0, 1]:
idx = h & dict.ht[table].sizemask
entry = dict.ht[table].table[idx]
while entry:
if compare_keys(entry.key, key):
return entry.value
entry = entry.next
# 4. 如果不在 rehash,不需要查第二个表
if not dict.is_rehashing():
break
return None
def dict_add(dict, key, value):
"""添加操作的 rehash 感知"""
# 1. 如果正在 rehash,先执行一步
if dict.is_rehashing():
dict.incremental_rehash(1)
# 2. 检查 key 是否存在(需要查两个表)
if dict_find_for_add(dict, key):
return DUPLICATE_ERROR
# 3. 确定插入哪个表
if dict.is_rehashing():
table = 1 # 只插入到新表
else:
table = 0
# 4. 插入并更新计数
h = hash_function(key)
idx = h & dict.ht[table].sizemask
new_entry = create_entry(key, value)
new_entry.next = dict.ht[table].table[idx]
dict.ht[table].table[idx] = new_entry
dict.ht[table].used += 1
return SUCCESS
def dict_delete(dict, key):
"""删除操作的 rehash 感知"""
# 1. 如果正在 rehash,先执行一步
if dict.is_rehashing():
dict.incremental_rehash(1)
deleted = False
# 2. 在两个表中都删除(如果在 rehash)
tables_to_check = [0]
if dict.is_rehashing():
tables_to_check.append(1)
for table in tables_to_check:
h = hash_function(key)
idx = h & dict.ht[table].sizemask
prev, entry = None, dict.ht[table].table[idx]
while entry:
if compare_keys(entry.key, key):
# 从链表中删除
if prev:
prev.next = entry.next
else:
dict.ht[table].table[idx] = entry.next
# 释放内存
free_entry(entry)
dict.ht[table].used -= 1
deleted = True
break
prev, entry = entry, entry.next
return deleted
4.2 不同阶段的数据分布
Rehash 进度示意图:
阶段 1:未开始 rehash
┌─────────────────────────────────────┐
│ ht[0] █████████████████████████████ │ 所有数据
│ ht[1] │ 空表
└─────────────────────────────────────┘
rehashidx = -1
阶段 2:rehash 进行中(迁移了 3 个桶)
┌─────────────────────────────────────┐
│ ht[0] ████████████████░░░░░░░░░░░░░ │ 部分数据
│ ht[1] ███░░░░░░░░░░░░░░░░░░░░░░░░░░ │ 已迁移数据
└─────────────────────────────────────┘
↑
rehashidx = 3
阶段 3:rehash 完成
┌─────────────────────────────────────┐
│ ht[0] │ 旧表已释放
│ ht[1] █████████████████████████████ │ 所有数据
└─────────────────────────────────────┘
rehashidx = -1(完成后)
⚙️ 五、触发条件与配置参数
5.1 Redis 配置文件详解
bash
# redis.conf 中关于哈希表的相关配置
# Hash 类型使用 ziplist 的阈值
hash-max-ziplist-entries 512 # 元素数量超过此值转 hashtable
hash-max-ziplist-value 64 # 单个元素值长度超过此值转 hashtable
# 是否激活渐进式 rehash
activerehashing yes # 建议开启,每次访问时进行少量 rehash
# 控制 BGSAVE 期间的扩容策略
# Redis 默认在有子进程时延迟扩容(除非负载因子>5)
5.2 实际触发场景示例
bash
# 场景 1:普通扩容
127.0.0.1:6379> EVAL "
for i=1,1000 do
redis.call('SET', 'key'..i, 'value'..i)
end
return 'done'
" 0
# 当键数量超过哈希表大小时触发 rehash
# 场景 2:Hash 类型编码转换
127.0.0.1:6379> EVAL "
for i=1,600 do # 超过 512
redis.call('HSET', 'myhash', 'field'..i, 'x')
end
" 0
# 从 ziplist 转换为 hashtable,触发 rehash
# 场景 3:大量删除触发缩容
127.0.0.1:6379> EVAL "
for i=1,9000 do
redis.call('DEL', 'key'..i)
end
" 0
# 当负载因子 < 0.1 时触发缩容 rehash
🛠️ 六、实战经验与优化建议
6.1 常见问题与解决方案
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| Redis 响应突然变慢 | 大 key 正在 rehash | 1. 拆分大 key 2. 避开业务高峰期触发 |
| 内存使用率波动 | 双表同时存在 | 监控并预留 2 倍内存 |
| CPU 使用率周期性升高 | 定时任务触发 rehash | 调整 activerehashing 参数 |
| 持久化期间性能下降明显 | BGSAVE 延迟了 rehash | 使用 AOF,或调整持久化策略 |
6.2 最佳实践
- 避免创建大 Hash,拆分大 Hash 为多个小 Hash
- 预扩容避免业务高峰期 rehash,预估数据量,提前触发扩容
- 做好监控告警配置
📝 七、总结
Redis 的渐进式 Rehash 是一个工程与算法的完美结合:
- 设计哲学:用空间换时间,用复杂度换平滑性
- 核心创新:渐进式迁移 + 双表机制
- 数据安全:读写操作天然支持一致性
- 性能平衡:微小的单次开销 vs 避免全局卡顿
"优秀的架构不是没有代价,而是把代价放在了正确的地方。"
渐进式 Rehash 的代价是:
- 额外的内存使用(双表)
- 稍微复杂的查询逻辑(查两个表)
- 持续的 CPU 开销(逐步迁移)
但这些代价换来了:
- 稳定的响应时间(无全局停顿)
- 平滑的性能曲线(无尖峰延迟)
- 更好的用户体验(服务永不中断)