Redis数据结构扩容源码分析

news2025/1/20 10:50:31

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]进行初始化

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

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

相关文章

[AutoSar]BSW_Diagnostic_004 ReadDataByIdentifier(0x22)的配置和实现

目录 关键词平台说明背景一、配置DcmDspDataInfos二、配置DcmDspDatas三、创建DcmDspDidInfos四、创建DcmDspDids五、总览六、创建一个ASWC七、mapping DCM port八、打开davinci developer,创建runnabl九、生成代码 关键词 嵌入式、C语言、autosar、OS、BSW、UDS、…

Maven:继承和聚合

Maven高级 分模块设计和开发 如果在我们自己的项目中全部功能在同一个项目中开发,在其他项目中想要使用我们封装的组件和工具类并不方便 不方便项目的维护和管理 项目中的通用组件难以复用 所以我们需要使用分模块设计 分模块设计 在项目设计阶段,可以将大的项目拆分成若…

欢乐钓鱼大师攻略,兑换码怎么操作?

在努力钓鱼的同时,别忘了收获丰富的奖励和成就,这将是你在游戏中的最大动力和满足感。 完成任务和挑战: 游戏中有各种各样的任务和挑战等着你去完成。通过完成这些任务和挑战,你可以获得丰富的奖励和成就,提升自己的钓…

[Java EE] 文件IO(一):文件概念与文件系统操作

🌸个人主页:https://blog.csdn.net/2301_80050796?spm1000.2115.3001.5343 🏵️热门专栏:🍕 Collection与数据结构 (91平均质量分)https://blog.csdn.net/2301_80050796/category_12621348.html?spm1001.2014.3001.5482 🧀Java …

android studio配置Http Proxy

1、问题描述: Error:Unable to tunnel through proxy. Proxy returns “HTTP/1.1 400 Bad Request” 解决:HTTP Proxy设置 1.File→Settings…→System Settings → HTTP Proxy → Auto-detect proxy settings”; 2.勾选下方“Automatic prox…

景源畅信电商:经营抖店需要电脑吗?

经营抖店是否需要电脑?这个问题看似简单,实则关乎着商家的运营效率和成本投入。在当前数字化、网络化的商业环境中,电脑已经成为了不可或缺的工具。那么,经营抖店究竟是否需要电脑呢?答案是肯定的。 一、高效处理订单 电脑能够高效地处理大…

【408真题】2009-03

“接”是针对题目进行必要的分析,比较简略; “化”是对题目中所涉及到的知识点进行详细解释; “发”是对此题型的解题套路总结,并结合历年真题或者典型例题进行运用。 涉及到的知识全部来源于王道各科教材(2025版&…

【Linux】- Linux环境变量[8]

目录 环境变量 $符号 自行设置环境变量 环境变量 环境变量是操作系统(Windows、Linux、Mac)在运行的时候,记录的一些关键性信息,用以辅助系统运行。在Linux系统中执行:env命令即可查看当前系统中记录的环境变量。 …

代数结构:5、格与布尔代数

16.1 偏序与格 偏序集:设P是集合,P上的二元关系“≤”满足以下三个条件,则称“≤”是P上的偏序关系(或部分序关系) (1)自反性:a≤a,∀a∈P; (2…

将Flutter程序打包为ios应用并进行安装使用

如果直接执行flutter build ios: Building com.example.myTimeApp for device (ios-release)...════════════════════════════════════════════════════════════════════════════════No vali…

Multisim 14简易三人抢答器电路设计

multisim multisim,即电子电路仿真设计软件。Multisim是美国国家仪器(NI)有限公司推出的以Windows为基础的仿真工具,适用于板级的模拟/数字电路板的设计工作。它包含了电路原理图的图形输入、电路硬件描述语言输入方式&#xff0…

汇昌联信:做拼多多网点需要具备什么能力?

在当前电商行业高速发展的背景下,拼多多以其独特的商业模式迅速崛起,成为众多创业者和商家关注的焦点。想要运营一家成功的拼多多网点,不仅需要对平台规则有深入的了解,还需要具备多方面的能力。这些能力是确保网点稳定运营并实现…

【安全每日一讲】加强数据安全保护 共享数字化时代便利

前言 数据安全是数据治理的核心内容之一,随着数据治理的深入,我不断的碰到数据安全中的金发姑娘问题(指安全和效率的平衡)。 DAMA说,降低风险和促进业务增长是数据安全活动的主要驱动因素,数据安全是一种资…

47.乐理基础-音符的组合方式-连线

连线与延音线长得一模一样 它们的区别就是延音线的第三点,延音线必须连接相同的音 连线在百分之九十九的情况下,连接的是不同的音,如下图的对比,连线里的百分之1,以现在的知识无法理解,后续再写 在乐谱中遇…

【MySQL】聊聊你不知道的前缀索引原理以及使用场景

背景 在本周的时候,接到一个需求,需要通过加密后的身份证 md5 去数据库里匹配。由于业务方存储的是身份证 md5username 构建的一列,并且没有加索引。 解决方案:1.新建一列 md5的列,加索引 2.对现有的列进行加前缀索引…

数组实现循环队列

1、分析 循环队列最主要的特点为当前面的空间被pop后,后面的数据可以插入到前面空余的数据中去; 所以最难的部分为判断什么时候为空什么时候为满: a、空满问题 我们先来分析当数据满时,head和tail相等(tail认为是指…

C++:9.scanf扩展——原来这么好用!

——scanf:我**不常用了? 有一天看到了一道题: C 输入一个时间,输出它属于,白天,下午还是黑夜。 输入样例: 15:20 00:00 13:14 05:20 11:45 14:00 ……??? 大胆题目小瞧我的编程水平!!!!!…

什么是 IIS

什么是 IIS 一、什么是 IIS二、IIS 的功能三、IIS 几点说明四、IIS 的版本五、IIS 常见的组合 欢迎关注【云边小网安】 一、什么是 IIS IIS:指 Internet Information Services ,是一种由微软公司开发的 Web 服务器应用程序。IIS:是一种 Web …

GPU prompt

提问: GPU是如何与CPU协调工作的? GPU也有缓存机制吗?有几层?速度差异是多少? GPU渲染流程有哪些阶段?他们的功能分别是什么? Early-Z技术是什么?发生在哪个阶段?这个…

MySQL表死锁查询语句

步骤1:查询表死锁的sql语句: SELECT * FROM information_schema.PROCESSLIST where length(info) >0 ; 或 SELECT * FROM information_schema.INNODB_TRX; 步骤2:删除 kill "对应的线程id"