Redis rehash 相关问题

前言

本文主要介绍 Redis Hash 表 rehash 相关的三个问题:

  • 什么时候触发 rehash
  • rehash 扩容扩多大
  • rehash 如何执行

介绍的源码基于 Redis 3.0.0 版本,会删除一些不影响理解的部分。

什么时候触发 rehash

Redis 用于判断是否触发 rehash 的函数是 _dictExpandIfNeeded。它的源码如下:

static int _dictExpandIfNeeded(dict *d) {
    if (dictIsRehashing(d)) {
        return DICT_OK;
    }

    // 对应初始化场景,不属于 rehash 操作
    if (d->ht[0].size == 0) {
        return dictExpand(d, DICT_HT_INITIAL_SIZE);
    }

    if (d->ht[0].used >= d->ht[0].size &&
        (dict_can_resize ||
         d->ht[0].used/d->ht[0].size > dict_force_resize_ratio)) {
        return dictExpand(d, d->ht[0].used*2);
    }
    return DICT_OK;
}

从上述代码可以看到有两个扩容条件:

  1. Hash 表可以进行扩容,同时 ht[0] 的负载因子大于等于 1
  2. Hash 表不可以进行扩容,但 ht[0] 的负载因子大于 dict_force_resize_ratio(默认值为 5)

dict_can_resize 这个变量由以下两个函数设置:

void dictEnableResize(void) {
    dict_can_resize = 1;
}

void dictDisableResize(void) {
    dict_can_resize = 0;
}

上面两个函数被 updateDictResizePolicy 函数调用,启用扩容的条件是:当前没有 RDB 子进程,也没有 AOF 子进程,即没有在执行 BGSAVE 命令或 BGREWRITEAOF 命令。

void updateDictResizePolicy(void) {
    if (server.rdb_child_pid == -1 && server.aof_child_pid == -1) {
        dictEnableResize();
    } else {
        dictDisableResize();
    }
}

为什么有子进程时要提高执行扩展所需的负载因子呢?

这是因为目前大多数操作系统都采用写时复制技术来优化子进程的使用效率,这样可以避免不必要的内存写入操作。

_dictExpandIfNeeded 被 _dictKeyIndex 调用,而 _dictKeyIndex 又被 dictAddRaw 调用。

rehash 扩容扩多大

在 Redis 中,扩容是通过调用 dictExpand 函数完成的。上面的调用中有如下调用:

dictExpand(d, d->ht[0].used*2);

是将当前已使用大小的 2 倍传给 dictExpand 函数,dictExpand 代码如下:

int dictExpand(dict *d, unsigned long size) {
    // 新的哈希表
    dictht n; 
    unsigned long realsize = _dictNextPower(size);

    // 申请空间并进行初始化
    n.size = realsize;
    n.sizemask = realsize-1;
    n.table = zcalloc(realsize*sizeof(dictEntry*));
    n.used = 0;

    d->ht[1] = n;
    d->rehashidx = 0;
    return DICT_OK;
}

上面的代码调用 _dictNextPower 获取了 realsize。如果执行的是扩展操作,那么 realsize 的大小为第一个大于等于 dt[0].used * 2 的 2 n 2^n 2n

为什么要扩容为之前的 2 倍,并且是 2 n 2^n 2n 的倍数呢?这样做有两方面好处:

  1. 可以将 hash % n 操作转换为 hash & (n - 1),取模转换为位运算操作,提高效率
  2. 对元素的重新分配操作更简单,只需要根据 hash & oldCap 是否等于 0 拆分成两部分。等于 0 的 newTable[i] = oldTable[i];不等于 0 的 newTable[i + oldCap] = oldTable[i]

渐进式 rehash 如何执行

扩容哈希表需要将 ht[0] 里面的所有键值对 rehash 到 ht[1] 里面,但这个 rehash 动作并不是一次性完成的,而是分多次,渐进式完成的。

这样做的原因在于,如果哈希表里保存的键值对数量很多,那么要一次性将这些键值对全部 rehash 到 ht[1] 的话,庞大的计算量可能会导致服务器在一段时间内停止服务。

以下是哈希表渐进式 rehash 的详细步骤:

  1. 为 ht[1] 分配空间,让字典同时持有 ht[0] 和 ht[1] 两个哈希表
  2. 在字典中维护一个索引计数器变量 rehashidx,并将它的值设置为 0,表示 rehash 工作开始
  3. 在 rehash 进行期间,每次对字典执行添加、删除、查找或更新操作时程序除了执行指定的操作之外,还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1],当 rehash 工作完成之后,程序将 rehashidx 属性的值加 1
  4. 随着字典操作的不断进行,ht[0] 上的所有键值对都会被 rehash 到 ht[1],这时程序将 rehashidx 属性的值设为 -1,表示 rehash 操作已完成

rehash 在代码层面是如何实现的呢?有两个关键函数:dictRehash 和 _dictRehashStep。下面是 dictRehash 的代码,它将 n 个桶的元素移动到 ht[1]:

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[0] 移动到 ht[1]
        while(de) {
            unsigned int h;

            nextde = de->next;
            // 获取在 ht[1] 的下标
            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++;
    }

    // 检查是否已经完成了 rehash
    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 函数实现了对一个 bucket 执行 rehash。一共有 5 个函数调用 _dictRehashStep 函数,分别是:dictAddRaw、dictGenericDelete、dictFind、dictGetRandomKey 和 dictGetSomeKeys。

那么此时问题来了,如果我们将 ht[0] 中 0、1、2 号下标 bucket 中的元素移动到了 ht[1],后续又往这几个地方插入该怎么办?

不用担心,因为在 rehash 时,会用如下代码判断要操作的哈希表,所以不会有上述问题。

ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];

查找和删除操作会检查两个哈希表,所以也没有任何问题。

参考资料

  • 《Redis 设计与实现》
  • 极客时间:Redis 源码剖析与实战

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/591264.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Redis-单机安装

试图从官网注册不了我也不知道什么情况。 网盘自取吧,链接:https://pan.baidu.com/s/1KERBQaH9gCT10AGt9z0_jg?pwdyjen 安装比较简单,照着敲就完了每一步都试过了,先单机安装,后面搭建集群。 1.将安装包放到/usr/…

ARP防火墙能够为网络安全贡献什么样的力量

ARP防火墙(Address Resolution Protocol Firewall)作为网络安全的一环,起到保护网络免受ARP欺骗攻击的关键作用。今天德迅云安全给您介绍ARP防火墙的相关方面,帮助您深入了解和认识这一关键的安全措施。 网络安全对于现代社会的信…

专业渗透测试 Phpsploit-Framework(PSF)框架软件小白入门教程(四)

本系列课程,将重点讲解Phpsploit-Framework框架软件的基础使用! 本文章仅提供学习,切勿将其用于不法手段! 继续接上一篇文章内容,讲述如何进行Phpsploit-Framework软件的基础使用和二次开发。 当我们牢记登陆账户、…

【简单讲解下npm常用命令】

🌈个人主页: 程序员不想敲代码啊 🏆CSDN优质创作者,CSDN实力新星,CSDN博客专家 👍点赞⭐评论⭐收藏 🤝希望本文对您有所裨益,如有不足之处,欢迎在评论区提出指正,让我们共…

文章解读与仿真程序复现思路——电力自动化设备EI\CSCD\北大核心《电-氢-混氢天然气耦合的城市综合能源系统低碳优化调度》

本专栏栏目提供文章与程序复现思路,具体已有的论文与论文源程序可翻阅本博主免费的专栏栏目《论文与完整程序》 论文与完整源程序_电网论文源程序的博客-CSDN博客https://blog.csdn.net/liang674027206/category_12531414.html 电网论文源程序-CSDN博客电网论文源…

政安晨:【Keras机器学习示例演绎】(三十二)—— 在 Vision Transformers 中学习标记化

目录 导言 导入 超参数 加载并准备 CIFAR-10 数据集 数据扩增 位置嵌入模块 变压器的 MLP 模块 令牌学习器模块 变换器组 带有 TokenLearner 模块的 ViT 模型 培训实用程序 使用 TokenLearner 培训和评估 ViT 实验结果 参数数量 最终说明 政安晨的个人主页&…

STM32单片机实战开发笔记-EXIT外部中断检测

嵌入式单片机开发实战例程合集: 链接:https://pan.baidu.com/s/11av8rV45dtHO0EHf8e_Q0Q?pwd28ab 提取码:28ab EXIT模块测试 功能描述 外部中断/事件控制器由19个产生事件/中断要求的边沿检测器组成。每个输入线可以独立地配置输入类型&a…

政安晨:【Keras机器学习示例演绎】(三十三)—— 知识提炼

目录 设置 构建 Distiller() 类 创建学生和教师模型 准备数据集 培训教师 将教师模型蒸馏给学生模型 从头开始训练学生进行比较 政安晨的个人主页:政安晨 欢迎 👍点赞✍评论⭐收藏 收录专栏: TensorFlow与Keras机器学习实战 希望政安晨的博客能够…

基于Spring Boot的医疗服务系统设计与实现

基于Spring Boot的医疗服务系统设计与实现 开发语言:Java框架:springbootJDK版本:JDK1.8数据库工具:Navicat11开发软件:eclipse/myeclipse/idea 系统部分展示 医疗服务系统首页界面图,公告信息、医疗地图…

快速幂笔记

快速幂即为快速求出一个数的幂&#xff0c;这样可以避免TLE&#xff08;超时&#xff09;的错误。 传送门&#xff1a;快速幂模板 前置知识&#xff1a; 1) 又 2) 代码&#xff1a; #include <bits/stdc.h> using namespace std; int quickPower(int a, int b) {int…

容斥原理以及Nim基础(异或,SG函数)

容斥原理&#xff1a; 容斥的复杂度为O&#xff08;2^m)&#xff0c;所以可以通过&#xff0c;对于实现&#xff0c;一共2^n-1种&#xff0c;我们可以用二进制来实现 下面是AC代码&#xff1a; #include<bits/stdc.h> using namespace std; typedef long long LL; cons…

uniapp 文字转语音(文字播报、语音合成)、震动提示插件 Ba-TTS

简介&#xff08;下载地址&#xff09; Ba-TTS 是一款uniapp语音合成&#xff08;tts&#xff09;插件&#xff0c;支持文本转语音&#xff08;无服务费&#xff09;&#xff0c;支持震动提示。 支持语音合成&#xff0c;文本转语音支持震动&#xff08;可自定义任意震动效果…

【云原生】Docker 实践(四):使用 Dockerfile 文件的综合案例

【Docker 实践】系列共包含以下几篇文章&#xff1a; Docker 实践&#xff08;一&#xff09;&#xff1a;在 Docker 中部署第一个应用Docker 实践&#xff08;二&#xff09;&#xff1a;什么是 Docker 的镜像Docker 实践&#xff08;三&#xff09;&#xff1a;使用 Dockerf…

【LinuxC语言】信号的基本概念与基本使用

文章目录 前言一、信号的概念二、信号的使用2.1 基本的信号类型2.2 signal函数 总结 前言 在Linux环境下&#xff0c;信号是一种用于通知进程发生了某种事件的机制。这些事件可能是由操作系统、其他进程或进程本身触发的。对于C语言编程者来说&#xff0c;理解信号的基本概念和…

36.Docker-Dockerfile自定义镜像

镜像结构 镜像是将应用程序及其需要的系统函数库、环境、配置、依赖打包而成。 镜像是分层机构&#xff0c;每一层都是一个layer BaseImage层&#xff1a;包含基本的系统函数库、环境变量、文件系统 EntryPoint:入口&#xff0c;是镜像中应用启动的命令 其他&#xff1a;在…

从0开始学习制作一个微信小程序 学习部分(6)组件与事件绑定

系列文章目录 学习篇第一篇我们讲了编译器下载&#xff0c;项目、环境建立、文件说明与简单操作&#xff1a;第一篇链接 第二、三篇分析了几个重要的配置json文件&#xff0c;是用于对小程序进行的切换页面、改变图标、控制是否能被搜索到等的操作第二篇链接、第三篇链接 第四…

QT的TcpServer

Server服务器端 QT版本5.6.1 界面设计 工程文件&#xff1a; 添加 network 模块 头文件引入TcpServer类和TcpSocket&#xff1a;QTcpServer和QTcpSocket #include <QTcpServer> #include <QTcpSocket>创建server对象并实例化&#xff1a; /*h文件中*/QTcpServer…

基于SSM SpringBoot vue宾馆网上预订综合业务服务系统

基于SSM SpringBoot vue宾馆网上预订综合业务服务系统 系统功能 首页 图片轮播 宾馆信息 饮食美食 休闲娱乐 新闻资讯 论坛 留言板 登录注册 个人中心 后台管理 登录注册 个人中心 用户管理 客房登记管理 客房调整管理 休闲娱乐管理 类型信息管理 论坛管理 系统管理 新闻资讯…

Docker-Compose编排LNMP并部署WordPress

前言 随着云计算和容器化技术的快速发展&#xff0c;使用 Docker Compose 编排 LNMP 环境已经成为快速部署 Web 应用程序的一种流行方式。LNMP 环境由 Linux、Nginx、MySQL 和 PHP 组成&#xff0c;为运行 Web 应用提供了稳定的基础。本文将介绍如何通过 Docker Compose 编排 …

BUUCTF---misc---被偷走的文件

1、题目描述 2、下载附件&#xff0c;是一个流量包&#xff0c;拿去wireshark分析&#xff0c;依次点开流量&#xff0c;发现有个流量的内容显示flag.rar 3、接着在kali中分离出压缩包&#xff0c;使用下面命令&#xff0c;将压缩包&#xff0c;分离出放在out3文件夹中 4、在文…
最新文章