Redis 渐进式 Rehash 深度剖析:如何实现平滑扩容与数据一致性

📖 前言:从 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 最佳实践

  1. 避免创建大 Hash,拆分大 Hash 为多个小 Hash
  2. 预扩容避免业务高峰期 rehash,预估数据量,提前触发扩容
  3. 做好监控告警配置

📝 七、总结

Redis 的渐进式 Rehash 是一个工程与算法的完美结合

  1. 设计哲学:用空间换时间,用复杂度换平滑性
  2. 核心创新:渐进式迁移 + 双表机制
  3. 数据安全:读写操作天然支持一致性
  4. 性能平衡:微小的单次开销 vs 避免全局卡顿

"优秀的架构不是没有代价,而是把代价放在了正确的地方。"

渐进式 Rehash 的代价是:

  • 额外的内存使用(双表)
  • 稍微复杂的查询逻辑(查两个表)
  • 持续的 CPU 开销(逐步迁移)

但这些代价换来了:

  • 稳定的响应时间(无全局停顿)
  • 平滑的性能曲线(无尖峰延迟)
  • 更好的用户体验(服务永不中断)
相关推荐
大厂技术总监下海1 天前
数据湖加速、实时数仓、统一查询层:Apache Doris 如何成为现代数据架构的“高性能中枢”?
大数据·数据库·算法·apache
LeenixP1 天前
RK3576-Debian12删除userdata分区
linux·运维·服务器·数据库·debian·开发板
知行合一。。。1 天前
Python--03--函数入门
android·数据库·python
X***07881 天前
理解 MySQL 的索引设计逻辑:从数据结构到实际查询性能的系统分析
数据库·mysql·sqlite
爬山算法1 天前
Hibernate(31)Hibernate的原生SQL查询是什么?
数据库·sql·hibernate
Yuiiii__1 天前
一次并不简单的 Spring 循环依赖排查
java·开发语言·数据库
-曾牛1 天前
Yak语言核心基础:语句、变量与表达式详解
数据库·python·网络安全·golang·渗透测试·安全开发·yak
天意pt1 天前
Blog-SSR 系统操作手册(v1.0.0)
前端·vue.js·redis·mysql·docker·node.js·express
爱吃羊的老虎1 天前
【大模型】向量数据库:Chroma、Weaviate、Qdrant
数据库·语言模型
数据大魔方1 天前
【期货量化实战】跨期套利策略:价差交易完整指南(TqSdk源码详解)
数据库·python·算法·github·程序员创富