Redis哈希表Rehash全解析:扩容缩容背后的渐进式智慧
引言
作为Redis核心数据结构之一的哈希表(dict
),其性能直接影响着Redis的整体表现。当数据量频繁增减时,哈希表需要动态调整容量以保持高效——这就是传说中的Rehash(重新哈希)。今天我们就来扒一扒Redis哈希表Rehash的底层逻辑,尤其是扩容/缩容时的渐进式迁移机制。
一、为什么需要Rehash?触发条件大揭秘
哈希表的核心矛盾是:空间利用率与查询效率的平衡。当哈希表中元素太多或太少时,要么冲突激增(查询变慢),要么内存浪费(性价比低)。Redis通过**负载因子(Load Factor)**来量化这一矛盾,并以此触发Rehash。
负载因子怎么算?
负载因子 = 已存储键值对数量(size
) / 哈希表桶的数量(table_size
)。
触发Rehash的两种场景:
-
扩容触发(空间不够用):
当负载因子 > 1 时(正常情况),或 Redis 正在执行BGSAVE
/BGREWRITEAOF
持久化时(负载因子 > 5)。
原因:数据量超过桶数量,哈希冲突概率飙升(链式冲突链变长),查询时间复杂度可能从O(1)退化为O(n)。 -
缩容触发(内存浪费):
当负载因子 < 0.1 时。
原因:桶数量远大于实际数据量,大量空间被闲置,内存利用率低下。
二、Rehash的“双保险”:双哈希表设计
如果直接让原哈希表“暴力”扩容,所有数据需要重新计算哈希并插入新桶,这在数据量大时会导致Redis阻塞(主线程被长时间占用)。为了不影响线上服务,Redis设计了双哈希表结构:
typedef struct dict {dictType *type; // 类型方法集合(哈希函数、比较函数等)void *privdata; // 私有数据(如哈希种子)dictht ht[2]; // 核心:两个哈希表(ht[0]和ht[1])long rehashidx; // Rehash进度(-1表示未进行)int16_t pauserehash; // Rehash是否暂停(持久化时可能暂停)
} dict;
- ht[0]:主哈希表,日常存储数据。
- ht[1]:临时哈希表,仅在Rehash时使用,用于承接ht[0]迁移的数据。
Rehash的本质,就是把ht[0]的数据“搬家”到ht[1],完成后交换两者的角色(ht[0]指向原ht[1],ht[1]清空)。
三、渐进式Rehash:边干活边搬家,不阻塞服务
如果一次性迁移所有数据(比如ht[0]有100万键),主线程会被卡住很久,这对高并发场景是不可接受的。Redis的解决方案是渐进式Rehash——将迁移过程拆成无数个小步骤,每次操作(查询、插入、删除)时“顺手”迁移一点数据。
渐进式Rehash的具体流程:
1. 初始化阶段:启动Rehash
当触发Rehash条件时,Redis会:
- 为ht[1]分配新内存(扩容时是ht[0].table_size×2,缩容时是ht[0].table_size/2,且必须是2的幂次方)。
- 设置
rehashidx=0
(表示从ht[0]的第0号桶开始迁移)。 - 将
pauserehash
设为0(允许其他操作参与迁移)。
2. 日常操作:捎带脚迁移数据
Rehash期间,每次对Redis执行增删改查操作时,都会触发一次“小迁移”:
- 查询操作:先查ht[0]的当前桶(如果该桶已被迁移,就查ht[1]的同索引桶)。
- 插入/更新操作:只往ht[1]写数据(避免修改正在迁移的ht[0])。
- 删除操作:同时查ht[0]和ht[1],删除对应键(防止数据残留)。
每次操作后,rehashidx
递增1,直到rehashidx
等于ht[0].table_size(所有桶迁移完成)。
3. 收尾阶段:交换角色,释放旧表
当所有桶迁移完成后:
- ht[0]和ht[1]交换身份(ht[0]指向原ht[1],ht[1]清空)。
- 释放原ht[0]的内存(如果是缩容,可能保留小内存块复用)。
rehashidx
重置为-1,Rehash结束。
四、关键细节:那些你可能忽略的设计巧思
1. 为什么桶的数量必须是2的幂次方?
哈希表的索引计算依赖哈希值与桶数量的取模运算。如果桶数是2的幂次方(如8、16、32),可以用位运算(hash & (table_size - 1)
)代替取模(hash % table_size
),效率更高。扩容/缩容时,桶数翻倍或减半,索引计算依然高效。
2. 缩容也有底线:最小容量限制
为了防止频繁缩扩容,Redis规定缩容后的桶数至少为DICT_HT_INITIAL_SIZE
(默认4)。即使负载因子低至0.05,也不会缩到比4更小。
3. 持久化期间的特殊策略
执行BGSAVE
或BGREWRITEAOF
时,Redis会将扩容的负载因子阈值从1提高到5。这是因为持久化是磁盘IO密集型操作,此时如果触发扩容,可能导致主线程和持久化线程竞争CPU资源,影响性能。
总结:Rehash是Redis的“动态平衡术”
Redis的哈希表Rehash机制,通过双哈希表避免了数据迁移时的服务中断,通过渐进式迁移将大任务拆解为无数小步骤,确保了高并发场景下的稳定性。无论是扩容时的“空间换时间”,还是缩容时的“时间换空间”,本质上都是Redis对性能与内存利用率的精准权衡。
下次使用Redis时,不妨想想:当你执行SET key value
时,可能正悄悄参与着一场“数据搬家”的大工程——这或许就是Redis“高性能”背后的浪漫吧! 😊
(注:文中涉及的源码解析基于Redis 7.0版本,不同版本可能有细微差异。)