Redis字典dict的渐进式rehash机制是如何工作的?

来源:站长查询作者:辉辉头衔:草根站长
导读:本期聚焦于辉辉创作的《Redis字典dict的渐进式rehash机制是如何工作的?》,敬请观看详情。Redis字典扩容缩容时为什么不会一次性完成数据迁移?这背后靠的是渐进式rehash机制。dict结构内部维护ht[0]和ht[1]两张哈希表,扩容时按负载因子决定新表大小,然后把rehashidx置为0,在后续每次增删改查操作中顺带迁移一个桶,同时后台定时任务也会分批搬运数据。迁移期间新写入只进新表,读取则先查旧表再查新表。这种分摊策略把一次性的大量CPU开销拆散成小步执行,避免了单线程的Redis被阻塞。本文详细拆解dict的数据结构、扩容触发条件、rehashidx推进过程以及查找删除等操作在双表期间的走向,帮你彻底弄懂Redis源码里这套经典设计。

Redis的核心数据结构dict(字典)是支撑字符串键空间、哈希对象、集合对象等功能的底层基石。这个结构最值得称道的设计之一,就是渐进式rehash。哈希表在扩容或缩容时,理论上需要把旧表所有节点重新计算哈希值再挂到新表上,如果字典里存了上千万个键,一次性搬运会让Redis这个单线程服务出现明显卡顿。Redis的解法是把整个迁移过程拆成无数个微小的步骤,分摊到每一次操作里慢慢做。这篇文章结合源码逐一拆解这个机制的实现细节。

Redis字典dict的渐进式rehash机制是如何工作的?

一、dict的核心数据结构

要理解渐进式rehash,先得弄清楚dict长什么样。在dict.h中,字典结构由四个字段组成,其中最关键的是两个dictht哈希表和一个rehashidx索引。正常情况下只用ht[0]这一张表,ht[1]只在rehash进行中才会派上用场。

typedef struct dict {
    dictType *type;   // 类型特定函数,比如哈希函数、键比较函数
    void *privdata;   // 私有数据,传给类型特定函数的可选参数
    dictht ht[2];     // 两张哈希表,rehash期间新旧共存
    long rehashidx;   // rehash进度,-1表示当前没有进行rehash
    int iterators;    // 当前运行中的安全迭代器数量
} dict;

typedef struct dictht {
    dictEntry **table;      // 哈希桶数组
    unsigned long size;     // 桶数量,总是2的幂
    unsigned long sizemask; // size-1,用于取模定位桶下标
    unsigned long used;     // 已有节点数量
} dictht;

rehashidx是整个机制的灵魂。当它等于-1时,表示字典处于稳定状态,所有数据都在ht[0]里;一旦扩容开始,这个值被置为0,表示接下来要从ht[0]的第0个桶开始搬数据。每搬完一个桶,rehashidx就加1,直到ht[0]的全部数据搬空,rehashidx被重置回-1,ht[1]接管ht[0]的位置,一次rehash才算彻底结束。

另外值得留意的是sizemask的用法。Redis定位桶下标时用的是位运算hash & sizemask,这要求size必须是2的幂,位运算比取模快得多,这也是每次扩容都是翻倍的原因之一。

二、扩容与缩容的触发条件

是否需要rehash由_dictExpandIfNeeded函数判断,核心逻辑是计算负载因子,即used除以size。触发条件主要有以下几种情况:

  • 没有执行BGSAVE或BGREWRITEAOF时,负载因子大于等于1就扩容;
  • 正在执行BGSAVE或BGREWRITEAOF时,负载因子要大于等于5才扩容,这是为了尽量减少子进程存活期间的写时复制内存拷贝;
  • dictCanResize开关被关闭时,同样使用高阈值5;
  • 缩容则在serverCron定期检查时触发,当字典的填充率低于10%(used小于size的十分之一)时,收缩到恰好容纳used个节点的最小2的幂。

新表大小的计算规则很简单:扩容时取第一个大于等于used*2的2的幂,比如used是5000,则新size为16384。缩容时的目标大小同样是满足条件的最小2的幂,但不会小于初始的4。

这里有个容易被忽略的细节:为什么后台进程存在时要提高阈值?因为fork产生的子进程会共享父进程的物理内存页,Redis利用写时复制来节省内存。如果这时大量扩容rehash,每次写操作都会触发内存页复制,反而让内存占用暴涨。所以Redis宁可容忍更高的冲突率,也要推迟扩容。这个设计体现了对操作系统内存机制的深度理解。

三、渐进式迁移的完整流程

扩容决策做出后,dictExpand会为ht[1]分配空间并把rehashidx置为0,此后字典进入rehash状态。真正的搬运动作由_dictRehashStep和dictRehash两个函数配合完成。

int dictRehash(dict *d, int n) {
    int empty_visits = n * 10; // 最多访问的空桶数量
    while (n-- && d->ht[0].used != 0) {
        dictEntry *de, *nextde;
        // 跳过连续的空桶,避免长时间空转
        while (d->ht[0].table[d->rehashidx] == NULL) {
            d->rehashidx++;
            if (--empty_visits == 0) return 1;
        }
        de = d->ht[0].table[d->rehashidx];
        // 把该桶下所有节点搬到ht[1]
        while (de) {
            uint64_t h;
            nextde = de->next;
            h = dictHashKey(d, de->key) & d->ht[1].sizemask;
            de->next = d->ht[1].table[h];
            d->ht[1].table[h] = de;
            d->ht[0].used--;
            d->ht[1].used++;
            de = nextde;
        }
        d->ht[0].table[d->rehashidx] = NULL;
        d->rehashidx++;
    }
    // 检查是否已经搬完
    if (d->ht[0].used == 0) {
        zfree(d->ht[0].table);
        d->ht[0] = d->ht[1];
        _dictReset(&d->ht[1]);
        d->rehashidx = -1;
        return 0; // 表示rehash全部完成
    }
    return 1; // 表示尚未完成
}

渐进式的推进时机分布在两处。第一处是_dictRehashStep,它每次只搬一个桶,会在每次增删改查操作(dictAdd、dictFind、dictDelete等)调用时顺带执行,前提是没有安全迭代器在运行。第二处是serverCron里的定时任务dictRehashMilliseconds,默认每次执行1毫秒,在这1毫秒内尽可能多地搬100个桶一批的数据,即使字典完全空闲没有客户端访问,也能靠定时任务完成迁移。

注意代码里的empty_visits机制:如果ht[0]里有大片连续空桶(极端情况下可能几百万个),逐桶扫描会非常浪费时间,所以Redis规定单次rehash最多跳过n*10个空桶,超了就直接返回,把机会留给下一次调用。这是对最坏情况的一种防御。

四、rehash期间各类操作的走向

双表共存期间,所有操作的代码路径都要照顾两张表,这是渐进式rehash带来的复杂性:

查找操作:先根据rehashidx判断是否处于rehash状态,是的话先查ht[0],找不到再查ht[1],两张表都要算一遍哈希、走一遍链表。对应的实现在dictFind中,通过dictRehashStep在每次查找时还顺带搬一个桶。

新增操作:只往ht[1]插入,绝不再写ht[0],这样保证ht[0]的used单调递减,迁移必然收敛。这个规则在dictAddRaw里体现为计算下标时直接使用ht[1]的sizemask。

删除和更新:先在ht[0]找,找不到再切到ht[1],找到哪张表就在哪张表上操作。更新值不需要移动节点,只有删除会影响used计数。

迭代器:普通迭代器在遍历期间假设字典结构不变,所以持有迭代器时会禁止执行rehash step;安全迭代器则允许修改数据,但同样会暂停渐进式迁移。SCAN命令遍历大字典时依赖反转二进制游标算法来保证在rehash过程中不漏键、尽量少重复,这也是渐进式设计在命令层面配套的方案。

综合来看,这套机制用很小的代码复杂度换来了巨大的收益:无论字典多大,任何一次操作的耗时都是有上界的,Redis的主线程永远不会被一次哈希表扩容卡住。理解了dict的渐进式rehash,再看Redis的SCAN、随机事件采样、缩容逻辑等周边设计,都会豁然开朗。

Redis渐进式rehashdict字典修改时间:2026-09-16 01:52:36

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0916/57638.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。