Redis数据结构扩容源码分析

Redis扩容策略及实现详解 水平扩容:水平扩容是指通过增加Redis实例的数量来增加整个系统的存储容量。水平扩容的好处是可以充分利用多台服务器的资源,提高整个系统的并发处理能力和存储容量。但是,水平扩容需要对应用程序进行修改,以支持Redis集群的访问方式。水平扩容:水平扩容是指通过增加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=0
        dictDisableResize();
    }
/* 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,代表可以开始rehash
    d->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桶设置为null
        d->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]table
        zfree(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默认传的是1
    long long start = timeInMilliseconds();
    int rehashes = 0;
    //进入rehash方法 dictRehash 去完成渐进时HASH
    while(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]进行初始化

windows11 远程桌面连接 通过代理连接RDP桌面报错 错误代码0x904 拓展错误代码0x7 打开windows Defender防火墙设置--允许应用通过防火墙更改设置--允许其他应用浏览--进入C:\WINDOWS\system32目录选择mstsc.exe进入C:\Windows\SysWOW64目录h'qmstsc.exe添加后列表里多了一个远程桌面连接,还有一个Remote Desktop Connection,配置网络权限后保存连接成功。 阅读详情

相关推荐

保姆级教程:在Rockchip RK3588 SDK中,如何为你的新硬件板子创建第一个DTS文件

本文提供在Rockchip RK3588 SDK中为自定义硬件创建首个DTS文件的详细教程。通过分析DTS在RK3588开发中的角色、快速定位参考设计、实操步骤及调试验证方法,帮助开发者高效完成硬件板级配置,特别适合嵌入式Linux系统开发者参考。

weixin_28678831的博客 288

Redis集群--扩容的过程(原理)

本文介绍Redis的Cluster集群扩容的过程(原理)。

IT利刃出鞘的博客 4833

「 网络安全常用术语解读 」软件成分分析SCA详解:从发展背景到技术原理再到业界常用检测工具推荐

软件成分分析(Software Composition Analysis,SCA)是一种用于识别和分析软件内部组件及其关系的技术,旨在帮助开发人员更好地了解和管理其软件的构建过程,同时可帮助安全人员揭秘软件内部结构的神秘面纱。SCA技术的发展与软件行业的快速发展密不可分,下面将介绍SCA的发展背景、技术原理以及当前主流SCA工具。

所有成功的背后,都是痛苦的坚持,所有的痛苦,都是傻瓜般的不放弃! 5633

Redis中的hash结构和扩容机制

Redis中的hash结构和扩容机制

weixin_43442127的博客 2038

第 4 课 树莓派5的换源与中文输入法

树莓派换源、中文输入法的安装

嵌入式老牛的博客 2288

别再只盯着CXL协议了,聊聊CXL.mem里那个能动态调优内存性能的‘QoS遥测’到底怎么用

本文深入解析CXL.mem协议中的QoS遥测功能,探讨如何通过DevLoad指标动态调优内存性能。文章详细介绍了QoS遥测的核心机制、多QoS类别设备的实战配置,以及AI训练、金融交易和视频处理场景中的优化案例,帮助数据中心实现高效内存资源管理。

weixin_33725722的博客 437

Redis扩容,缩容与rehash机制

redis缩容和扩容的机制和具体实现过程

qq_41988542的博客 2535

从询价到回款:一篇讲透 SAP SD 销售与分销的实战入门指南

摘要: SAP SD(Sales and Distribution)模块是SAP ERP中管理销售与分销的核心组件,贯穿从订单到回款(Order-to-Cash)的全流程。其核心价值在于将销售业务结构化,通过主数据(客户、物料、定价)和交易单据(订单、交货、发票)实现流程可审计、可自动化。SD模块与物流(MM)、生产(PP)、财务(FI)深度集成,涵盖定价策略、信用控制、发运管理、国际贸易等复杂场景。关键设计包括条件技术(Condition Technique)实现灵活定价,单据流(Document Flo

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 258

Redis系列(三)Redis数据结构以及扩容、rehash流程

Redis数据结构以及扩容、rehash流程

CountryLord的博客 2566

LM358模拟声音传感器原理图设计,已量产(声音传感器)

最近在做智能家居设备的声音触发模块时,搭了个基于 LM358 的模拟声音传感器电路 —— 不用复杂的专用芯片,靠通用运放就能把环境声音转化为可被单片机 AD 采集的电压信号,成本压得很低,实际调试下来灵敏度和稳定性都够用。今天把这个电路拆解开,聊聊每部分的设计思路。

不脱发的程序猿 473

基于STM32LXXX的模数转换芯片ADC(CMS24AD2001)驱动C程序设计

/ 电压 = (原始值 / 2^23) * Vref (注意:24位有符号数范围为 -2^23 到 2^23-1)//---------------- 硬件端口映射 (请根据实际电路修改) ----------------ADC_SCLK_HIGH();- PGA 放大倍数可选:1、2、4、8、16、32、64、128、256;//---------------- 宏定义操作 -------------------//---------------- 函数声明 -------------------

myron6688的博客 161

通用: Redis数据结构与扩容、rehash流程:从原理到源码详解

负载因子动态阈值:根据是否在后台持久化调整扩容时机,避免不必要的性能损耗。渐进式rehash:将数据迁移分摊到每次操作中,避免单线程阻塞,保证服务响应速度。2的幂容量设计:通过高效计算索引,同时方便扩容时翻倍容量。理解这一机制,不仅能帮助你更好地使用Redis(如合理设计Hash类型的键值对数量),更能借鉴其"渐进式"思想,在自己的系统中处理大规模数据迁移问题。由于Redis是通过C语言写的,建议不必苛求源码的深层次理解,只需要大概了解其扩容复制的原理即可▲如有纰漏,烦请指正~~

weixin_45149504的博客 749

LVGL嵌入式GUI工程化架构与多窗口实践

嵌入式GUI开发中,LVGL作为轻量级图形库,其核心价值在于高效渲染与事件驱动机制。理解LVGL的对象树模型、资源管理机制(如RAW图像、静态字体)和屏幕生命周期,是构建稳定界面的基础。结合FreeRTOS等实时操作系统,可实现低功耗、高响应的交互体验。典型应用场景包括智能电表、HMI人机界面、工业控制面板等对内存敏感、需长期运行的设备。本文以微信式多窗口架构为范例,深入解析LVGL工程化落地的关键路径:分层目录结构、窗口模块封装、状态机式登录逻辑、TabView多页导航及图表/仪表盘动态可视化,覆盖从资源

weixin_36001279的博客 89

Redis 源码分析-内部数据结构 dict

redis中的dict数据结构详解,redis初始化时ht的容量,什么是渐进式hash,redis如何解决hash冲突,redis中扩容的实现是怎么实现的

qq_56517253的博客 1114

lwip-2.0.3

lwip是瑞典计算机科学院(SICS)的Adam Dunkels 开发的一个小型开源的TCP/IP协议栈。实现的重点是在保持TCP协议主要功能的基础上减少对RAM 的占用。

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

Redis底层数据结构——Dict

redis 中的 dict 是一个哈希表,用来存储键值类型的数据。Set:key用来存储元素,value均为null。ZsetHash:默认采用跳表,当跳表中的元素数量超过且 任意entry大小超过了时,采用dict。可通过和来查看这两个值。// dict.h0 :(unsigned long) 1

qq_41637232的博客 1200

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

redis源码阅读——sds

redis源码阅读——sds 简单动态字符串(simple dynamic strings),是redis中的基本数据结构,主要存储字符串和整数,本次阅读参考redis 5 设计与源码分析,阅读源码版本为redis 6 ,如有错误望指正。 数据结构 // 表示范围2^5 64,flags中前三位表示动态字符串类型,后五位表示字符串长度 // buf为柔性数组 struct __attribute__ ((__packed__)) sdshdr5 { unsigned char flags; /* 3

qq_43278070的博客 327

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

Redis6.0 sds string--初始化和扩容--详解

1 直接上源码(内部有注释) /* Modify an sds string in-place to make it empty (zero length). * However all the existing buffer is not discarded but set as free space * so that next append operations will not require allocations up to the * number of bytes previo.

qfzhangwei的专栏 818

Java必知必会Redis高级功能梳理总结!

大家都知道Redis的业务范围是非常广的,但是对于刚入行的小伙伴来说可能也就知道个缓存跟分布式锁。因为Redis的很多功能在一些小企业里,根本是用不到的,得等到并发量到了一定的程度,系统扛不住了,才会用到Redis那些高级的功能。下面LZ就带大家来看看,Redis到底能干些啥:

wdj_yyds的博客 67

redis底层数据结构(redis底层存储结构、源码分析)

一、redis为什么快?二、redis的底层数据结构2.1、redis的底层存储的扩容机制2.1.1、扩容时间2.1.2、扩容多大2.1.3、扩容后的rehash2.1.4、何时进行rehash2.1.5、俩hashtable访问那个呢?三、redis的key的底层数据类型(sds)3.1、sds(Simple dynamic string)3.2、sds诞生的原因3.3、sds的数据结构3.4、sds扩容原理3.5、redis

weixin_42351206的博客 6420

【Redis源码】Redis 6.0 字典扩容与渐进式 Rehash 源码分析

Redis 6.0 的字典扩容与渐进式 Rehash 源码分析

长路漫漫的歇脚处 1788

redis6.0源码分析:字典扩容与渐进式rehash

扩容状态码0// 成功1# define DICT_OK 0 // 成功 # define DICT_ERR 1 // 失败哈希表初始值4# define DICT_HT_INITIAL_SIZE 4 // 哈希表 (ht) size 的初始值扩容安全阈值。

qq_29750559的博客 657

5分钟搞定,Redis的哈希表何时扩容?|原创

今天讲一道面试中区分度比较高的题:请你详细讲讲 Redis 中 hash 结构何时扩容(何时rehash)?点击上方“后端开发技术”,选择“设为星标” ,优质资源及时送达这道题已经超出了一般面试中只问到数据类型的层次,要求面试者阅读过 Redis 源码,并且深入探究过 Hash 编码的扩容过程。哈希表在 Redis 中,哈希数据类型的底层实现是hash表、压缩列表,在未来 6.2以后 listpa...

Daniel的博客 1406

Redis数据结构深度剖析:从源码实现到性能优化

本文基于Redis 7.0源码,系统解析其核心数据结构的底层实现与设计哲学。通过拆解动态字符串(SDS)的空间预分配策略、intset的整数类型升级机制,揭示Redis内存优化的核心思路。深入探讨Dict哈希表的渐进式Rehash策略,以及ziplist通过紧凑编码实现高压缩率的原理,并剖析其连锁更新问题的触发条件与规避方案。结合QuickList的分段存储设计与SkipList的多级索引逻辑,阐明Redis如何在高性能与内存效率间取得平衡。对RedisObject元数据结构的解读,进一步串联了String

2501_90923422的博客 2088

Redis底层设计与源码分析(一)__底层数据结构逻辑分析

最近在工作中接触redis,但实际应用不多,可用场景也不是很明确,所以想借机多多学习下redis相关知识,一下内容都是通过自己找资源学习之后归纳总结的知识,若有相关错误点,请指正。

weixin_44593504的博客 713

redis数据结构之IntSet介绍以及源码分析

IntSet是Redis中set集合的一种实现方式,基于整数数组来实现,并且具备长度可变、有序等特征。 结构如下: typedef struct intset { uint32_t encoding; /* 编码方式,支持存放16位、32位、64位整数 */ uint32_t length; /* 元素个数 */ int8_t contents[]; /* 整数数组,保存集合数据 */ } intset; 其中encoding包含三种模式,表示存储的整数大小不同: /* Not

eyes++的博客 1046

Redis数据结构之——hash

Redis数据结构之——hash

weixin_46935110的博客 1万+
上一篇: Redis不同数据类型value存储
下一篇: JVM之加载class文件
山河小帅
博客等级 码龄7年 732粉丝 61原创
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值