Redis数据结构扩容源码分析

1 Redis数据结构

redis的数据存储在dict.中,其数据结构为(c源码)

ypedef struct dict {
dictType *type; //理解为面向对象思想,为支持不同的数据类型对应dictType抽象方法,不同的数据类型可以不同实现
void *privdata; //也可不同的数据类型相关,不同类型特定函数的可选参数。
dictht ht[2]; //2个hash表,用来数据存储 2个dictht
long rehashidx; /* rehashing not in progress if
rehashidx == -1 */ // rehash标记 -1代表不再rehash
unsigned long iterators; /* number of iterators
currently running */
} dict;

用图表示其存储流程为:

如图,redis在发生哈希冲突时,采用的是从头插法增加Entry的。那么Redis为什么要采用头插?而我们之前讲的HashMap为什么要用尾插呢?

a.头插在并发情况下会形成死链,而Redis是单线程执行,不会存在并发问题,也不会形成死链

b.头插的性能比尾插要高很多,尾插必须找到链表的最后一个位置,而头插只需要加到数据下标,并且把next指向之前的第一个数据就可以了

2 Redis扩容

redis为什么要扩容?

因为不扩容,那么从上面可以看到,每一个Node节点,若发生哈希冲突后,链表就会越来越长,查询的速度就会接近链表的速度O(n)

那么redis是如何进行扩容的呢?

2.1 扩容的时机

通过向redis加数据的时候,我们找到扩容的源码条件(dict.c文件中 _dictExpandIfNeeded方法)

//扩容条件 hash表中已使用的数量大于等于 hash表的大小
if (d->ht[0].used >= d->ht[0].size &&
//并且dict_can_resize为1 或者 已使用的大小大于hash表大
小的 5倍,大于等于1倍的时候,下面2个满足一个条件即可
(dict_can_resize ||
d->ht[0].used/d->ht[0].size >
dict_force_resize_ratio))   // dict_force_resize_ratio默认为5
{
//扩容成原来的2倍
return dictExpand(d, d->ht[0].used*2);
}

分析:何时扩容由if条件可以得出,a.当已使用的数量大于等于hash表的长度时并且dict_can_resize为true时扩容; b.或者当dict_can_resize不为true,而已使用的数量大于hash表长度的5倍时扩容 

那么dict_can_resize这个参数是代表着什么呢?(c源码中的默认设置值)

static int dict_can_resize = 1; //默认是1,达到一倍的时候就会
扩容
static unsigned int dict_force_resize_ratio = 5; //5倍

dictEnableResize设置允许可以扩容,dictDisableResize会将其设置为0

void dictEnableResize(void) {
dict_can_resize = 1;
}
void dictDisableResize(void) {
dict_can_resize = 0;
}

那么这两个方法又是在哪里调用的呢?

void updateDictResizePolicy(void) {if (!hasActiveChildProcess()) //hasActiveChildProcess如果为true代表有子进程持久化,前面是!dictEnableResize();else //有子进程时,就会将dict_can_resize=0dictDisableResize();}
/* Return true if there are no active children processesdoing RDB saving,
* AOF rewriting, or some side process spawned by a loadedmodule. */
int hasActiveChildProcess() {return server.rdb_child_pid != -1 ||server.aof_child_pid != -1 ||server.module_child_pid != -1;
}

分析:从上面源码我们可以发现,当没有子进程在进行RDB、AOF持久化或者其它的一些辅助进程则会讲dict_can_resize设置为1,否则设置为0。为什么这么做呢?那是因为现在操作系统写时复制(copy-on-write)来优化子进程的使用效率。在子线程进入RDB跟AOF时,如果发生大量内存写操作,会导致进程的性能降低。所以,当在RDB或者AOF时,将扩容阈值放大到了5倍(有兴趣的可以了解下copy-on-write技术)

扩容时机总结: 1.当没有子进程在进行RDB或者AOF时,已使用的数量大于等于hash表的size时就发生扩容;2.当有子进程在进行EDB或者AOF时,已使用的数量需要大于hash表size的5倍时才进行扩容。

2.2 如何扩容

a.当满足扩容条件触发扩容时,判断是否已经在扩容了,如果在扩容或者扩容的大小跟我现在的ht[0].size一样,则这次扩容不做

b.new一个新的dictht,大小为ht[0].used*2(但是必须向上2的幂,比如6,那么大小就为8),并且ht[1]=新创建的dictht

c.我们此时有一个更大的table了,但是需要把数据迁移到ht[1].table,所以将dict的rehashidx(数据迁移的偏移量赋值0),就代表着可以进行数据迁移了,也就是可以rehash了

d.等待数据迁移完成,数据不会马上迁移,而是采用渐进式rehash,慢慢的把数据迁移到ht[1]

e.当数据迁移完成,ht[0].table=ht[1],ht[1].table=NULL、ht[1].size=0、ht[1].sizemask=0、ht[1].used=0

f.把dict的rehashidx=-1

C源码(dict.c文件中的dictExpand方法)

int dictExpand(dict *d, unsigned long size)
{/* the size is invalid if it is smaller than the number of* elements already inside the hash table *///正在扩容,不允许第二次扩容,或者ht[0]的数据量大于扩容后的数量,没有意义,这次不扩容if (dictIsRehashing(d) || d->ht[0].used > size)return DICT_ERR;dictht n; /* the new hash table *///得到最接近2的幂 跟hashMap思想一样unsigned long realsize = _dictNextPower(size);/* Rehashing to the same table size is not useful. *///如果跟ht[0]的大小一致 直接返回错误if (realsize == d->ht[0].size) return DICT_ERR;/* Allocate the new hash table and initialize all pointers to NULL *///为新的dictht赋值n.size = realsize;n.sizemask = realsize-1;n.table = zcalloc(realsize*sizeof(dictEntry*));n.used = 0;/* Is this the first initialization? If so it's notreally a rehashing* we just set the first hash table so that it canaccept keys. *///ht[0].table为空,代表是初始化if (d->ht[0].table == NULL) {d->ht[0] = n;return DICT_OK;}/* Prepare a second hash table for incremental rehashing *///将扩容后的dictht赋值给ht[1]d->ht[1] = n;//标记为0,代表可以开始rehashd->rehashidx = 0;return DICT_OK;
}

2.3 数据如何迁移

redis中假如一次性进行数据迁移会很耗时间,会让单条指令等待很久很久。会形成阻塞,所以,Redis采用的是渐进式Rehash,所谓渐进式,就是慢慢的,不会一次性把所有数据迁移。

那么什么时候会进行渐进式数据迁移呢?

a.每次进行key的crud操作都会进行一个hash桶的数据迁移

b.定时任务serverCron,进行部分数据迁移

CRUD(_dictRehashStep、dictRehash)

static void _dictRehashStep(dict *d) {if (d->iterators == 0) dictRehash(d,1);
}
int dictRehash(dict *d, int n) {int empty_visits = n*10; /* Max number of emptybuckets to visit. */ //要访问的最大的空桶数 防止此次耗时过长if (!dictIsRehashing(d)) return 0; //如果没有在rehash返回0//循环,最多1次,并且ht[0]的数据没有迁移完 进入循环while(n-- && d->ht[0].used != 0) {dictEntry *de, *nextde;/* Note that rehashidx can't overflow as we are sure there are more* elements because ht[0].used != 0 */assert(d->ht[0].size > (unsigned long)d->rehashidx);//rehashidx rehash的索引,默认0 如果hash桶为空 进入自旋 并且判断是否到了最大的访问空桶数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]的下标为rehashidx桶/* Move all the keys in this bucket from the old to the new hash HT */while(de) { //循环hash桶的链表 并且转移到ht[1]uint64_t h;nextde = de->next;/* Get the index in the new hash table */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;}//转移完后 将ht[0]相对的hash桶设置为nulld->ht[0].table[d->rehashidx] = NULL;d->rehashidx++;}/* Check if we already rehashed the whole table... *///ht[0].used=0 代表rehash全部完成if (d->ht[0].used == 0) {//清空ht[0]tablezfree(d->ht[0].table);//将ht[0] 重新指向已经完成rehash的ht[1]d->ht[0] = d->ht[1];//将ht[1]所有数据重新设置_dictReset(&d->ht[1]);//设置-1,代表rehash完成d->rehashidx = -1;return 0;}/* More to rehash... */return 1;
}

将ht[1]的属性复位

static void _dictReset(dictht *ht)
{ht->table = NULL;ht->size = 0;ht->sizemask = 0;ht->used = 0;
}

分析:CRUD操作每次可访问的hash空桶数为1*10= 10 个hash桶,并且while(n--),n是为1的,也就是说,CRUD操作只会优先查到一个hash桶,然后仅仅把该hash桶中的数据迁移完后就什么也不做了。

定时任务,定时任务的执行频率会根据redis.conf中的hz配置。定时任务也是在规定时间段内会进行部分迁移(每次100+个hash桶),判断一个时间,该时间内能做多少次就做多少次

定时任务的入口方法(server.c文件中的serverCron),最终执行rehash的方法(dictRehashMilliseconds)

int dictRehashMilliseconds(dict *d, int ms) { // ms默认传的是1long long start = timeInMilliseconds();int rehashes = 0;//进入rehash方法 dictRehash 去完成渐进时HASHwhile(dictRehash(d,100)) {rehashes += 100;//判断是否超时if (timeInMilliseconds()-start > ms) break;}return rehashes;
}

可以看到定时任务传给dictRehash方法中的n为100,也就是说,定时任务可访问的最大空桶说为1000,而while循环中,定时任务可以循环100次,可以处理100个hash桶的数据迁移,而ms传值为1,也就是redis中默认的定时任务是在1ms内能处理多少个hash桶就处理多少个hash桶。

3 Redis扩容流程图

假如我们新增了一个数据jane:36,通过计算hash值得出在dicEntry下标为3的位置上,则就使用头插法挂在dicEntry[3]的链表位置上,此时也没有RDB或AOF子进程在进行,而ht[0].used=7>ht[0].size=4,则触发扩容机制,ht[1].size初始化为2*ht[0].used=14,向上取2的幂得16,于是如图所示:

此时满足扩容得条件,则rehashidx=0,即从ht[0].table得下标0处得hash桶开始做迁移,这里为了演示的方便,我们假设索引为0处的hash桶的3个key 重新计算hash后(原下标计算hash(zsc&3) = 0 ,迁移后下标计算hash(zsc) & 15 = 0),下标值仍为0,则迁移后如图所示:

此时代表我们已经成功迁移了一个hash桶,当所有的hash桶都迁移完成后,则将ht[1]赋值为ht[0],ht[1]进行初始化

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

数据缓存,可以尝试RocksDB了

shigen坚持更新文章的博客写手,擅长Java、python、vue、shell等编程语言和各种应用程序、脚本的开发。记录成长,分享认知,留住感动。 个人IP:shigen shigen在最近的学习中,接触到了一款新的缓存数据库RocksDB&#xff…

PyQt5中的LineEdit单行文本框

文章目录 1. 简介1.1 常用方法:1.2 常用信号: 2. LineEdit常用方法使用案例3. LineEdit常用信号使用案例 1. 简介 在PyQt5中,LineEdit(单行文本框)是一个常用的组件,它允许用户输入文本。以下是一些LineEd…

SpringBoot整合SpringScurity权限控制(菜单权限,按钮权限)以及加上SSH实现安全传输

文章目录 项目地址: 一、md5 与 先进的哈希算法的区别1.1. 安全性问题1.2. 设计目的1.3. 功能特性1.4. 适用性1.5. 总结 二、数据传输安全和数据加密实现:2.1 生成证书:2.2、在springboot中进行集成2.2.1 配置证书:2.2.2. 强制使用…

MySQL·索引

目录 索引的意义 索引的理解 为何IO交互要是 Page 理解Page 其他数据结构为何不行? 聚簇索引 VS 非聚簇索引 索引操作 主键索引操作 唯一键索引操作 普通索引的创建 总结 全文索引 索引的意义 索引:提高数据库的性能,索引是物美…

LangChain:模型 I/O 封装使用解析和感触

目录 模型 API:LLM vs. ChatModel OpenAI 模型封装 多轮对话 Session 封装 换个国产模型 模型的输入与输出 Prompt 模板封装 PromptTemplate ChatPromptTemplate MessagesPlaceholder 从文件加载 Prompt 模板 TXT模板 Yaml模板 Json模板 输出封装 Out…

240512-关于如何用VSCode编写C#程序的简单说明

240512-关于如何用VSCode编写C#程序的简单说明 从安装软件开始 ,到编写一个HelloWorld的C#文件结束,介绍如何用VSCode编写C#程序 1 上官网下载一个安装包 官网地址:https://visualstudio.microsoft.com/zh-hans/downloads/ 2 打开安装包进…

嵌入式学习-中断控制系统

补充一下前面NVIC内嵌向量中断控制器的知识 中断 中断类型 中断控制 配置中断 优先级 分组问题 中断使能 NVIC相关库函数和作用 库函数 函数名 描述 NVIC_DeInit 将外设 NVIC 寄存器重设为初始值 NVIC_SCBDeInit 将外设 SCB 寄存器重设为初始值 NVIC_PriorityGroupCon…

Node.js全栈:从一个简单的例子开始

第一章:从一个简单的例子开始第二章:看官方文档的艺术第三章:浏览器显示一个网页 首先,在VSCode编辑器中打开一个没有任何文件的空目录,然后创建一个package.json文件。 为了方便大家复制,我把文件内容放到…

十进制整数转平衡三进制

求解原视频&#xff1a;平衡三进制 求赞&#xff01;100赞买个乒乓球拍&#xff01;_哔哩哔哩_bilibili 题目&#xff1a; 上海市计算机学会竞赛平台 | YACS 求解程序&#xff1a; using namespace std; #include <iostream> #include <cstring>string work(int n…

Zabbix6.0容器化部署(Docker-Composed)

Zabbix 为每个 Zabbix 组件提供 Docker image 作为可移植和自给自足的容器&#xff0c;以加快部署和更新过程。 Zabbix 组件在 Ubuntu、Alpine Linux 和 CentOS 基础 image 上提供:Zabbix 组件支持 MySQL 和 PostgreSQL 数据库、Apache2 和 Nginx Web 服务器。 1. Zabbix 组件…

开源流程引擎选型 —— Activiti、Flowable、Camunda

目录 一. 前言 二. 主流开源流程引擎介绍 2.1. Osworkflow 2.2. JBPM 2.3. Activiti 2.4. Flowable 2.5. Camunda 三. Flowable 与 Camunda 对比分析 3.1. 功能方面对比 3.2. 性能方面对比 四. 总结 一. 前言 市场上比较有名的开源流程引擎有 Osworkflow、JBPM、Act…

C控制语句:分支和跳转

1.1if语句 //colddays.c --找出0摄氏度以下的天数占总天数的百分比 #include <stdio.h>int main(void) {const int FREEZING 0;float temperature;int cold_days 0;int all_days 0;printf("Enter the list of daily low temperature.\n");printf("Use…

CSS - 选择器

目录 一、CSS的基本语法格式&#xff1a; 二、常见的CSS选择器 ​编辑1.标签选择器 2.类选择器 3.id选择器 4.复合选择器 5.通用选择器 三、常见的CSS样式 1.color 2.font-size 3.border 4.width/height 5.padding 6.margin 四、CSS的引入方式 1.行内引入 …

修改MTU值解决Linux下运行top命令卡死问题

上周明月的Linux服务器上运行top命令总是莫名的出现卡死现象&#xff0c;甚至是CtrlC都无法终止进程&#xff0c;今天终于抽空找到了解决办法&#xff0c;原来是需要修改Linux的MTU值&#xff0c;将服务器操作系统数据包调小&#xff0c;加上VxLAN数据包小于1500即可。 top命令…

Python 数据处理 合并二维数组和 DataFrame 中特定列的值

&#x1f349; CSDN 叶庭云&#xff1a;https://yetingyun.blog.csdn.net/ 示例代码如下&#xff1a; import numpy as np import pandas as pddata {label: [1, 2, 3, 4]} df pd.DataFrame(data)values_array df[["label"]].values random_array np.random.ran…

汇聚荣科技:如何加入拼多多网店运营?

随着电子商务的迅猛发展&#xff0c;越来越多的个体商家和品牌看到了线上平台的无限潜力。作为国内领先的电商平台之一&#xff0c;拼多多吸引了众多想要开店的商家。如果你也打算在拼多多上开设自己的网店&#xff0c;那么了解如何加入并有效运营是至关重要的。下面&#xff0…

如何解决IntelliJ IDEA中pom.xml依赖项引发的安全漏洞黄线警告问题

背景 在开发过程中&#xff0c;当我们在pom.xml文件中添加依赖项时&#xff0c;经常会发现IntelliJ IDEA报出黄色警告线条&#xff0c;提示存在潜在的安全漏洞。警告的具体展现形式如下&#xff1a; 解决方案 首先&#xff0c;打开设置菜单界面&#xff0c;接着选择编辑器选…

打造清洁宜居家园保护自然生态环境,基于YOLOv5全系列【n/s/m/l/x】参数模型开发构建自然生态场景下违规违法垃圾倾倒检测识别系统

自然生态环境&#xff0c;作为我们人类赖以生存的家园&#xff0c;其健康与否直接关系到我们的生活质量。然而&#xff0c;近年来&#xff0c;一些不法分子为了个人私利&#xff0c;在河边、路边等公共区域肆意倾倒垃圾&#xff0c;严重破坏了环境的健康与平衡。这种行为不仅损…

如何利用AI提高内容生产效率与AIGC典型案例分析

❤️❤️❤️ 欢迎来到我的博客。希望您能在这里找到既有价值又有趣的内容&#xff0c;和我一起探索、学习和成长。欢迎评论区畅所欲言、享受知识的乐趣&#xff01; 推荐&#xff1a;数据分析螺丝钉的首页 格物致知 终身学习 期待您的关注 导航&#xff1a; LeetCode解锁100…

C++——二叉树搜索树

前面写了初阶数据结构——二叉树&#xff1b;本文内容是来对它来进行结尾 目录 一概念 二实现 2.1查找 2.2插入 2.3删除 完整源代码 三二叉树的应用 四二叉搜索树的性能分析 五二叉搜索树相关的面试题 一概念 二叉搜索树又称二叉排序树&#xff0c;它或者是一棵空树…