Redis中的哈希结构(Dict)

news2025/1/11 21:45:26

前言

哈希结构是一个在计算机中非常常见的结构。哈希结构可以让我们在O(1)时间复杂度查找元素并且对其操作,并且增删改查性能并不会随着数据量的增多而改变。反而数据量的增大,会出现两个关键问题,一个是哈希冲突,另一个是rehash。而在Redis中,使用拉链法来解决哈希冲突,使用渐进式rehash来降低rehash的性能开销。

Redis中的Dict结构

在Redis 6.2.4中,dict.h是这样定义的。

typedef struct dictEntry {
    void *key;
    //只能为其中任意的一个
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;
/* This is our hash table structure. Every dictionary has two of this as we
 * implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; //-1未进行rehash 
    int16_t pauserehash; /* If >0 rehashing is paused (<0 indicates coding error) */
} dict;

它们的关系是这样的
在这里插入图片描述

什么是哈希冲突

我们把存储数据的地方看成一个个桶(Bucket),当数据量超出桶容量或者Hash函数给出的桶号相同的时候,便会出现哈希冲突。
在这里插入图片描述解决办法就是,采用链表的方式,将同一个Bucket位置上的元素连接起来。这样也会有一个弊端,链表太长,开销又大了起来。所以必定不会无休止的链下去,一定要做rehash。

Redis的渐进式rehash

Hash 表在执行 rehash 时,由于 Hash 表空间扩大,原本映射到某一位置的键可能会被映射到一个新的位置上,因此,很多键就需要从原来的位置拷贝到新的位置。而在键拷贝时,由于 Redis 主线程无法执行其他请求,所以键拷贝会阻塞主线程,这样就会产生 rehash 开销。而为了降低 rehash 开销,Redis 就提出了渐进式 rehash 的方法。观察dict结构,它存储了两个相同的dictht, 在正常情况下,所有的数据都存储在ht[0]中。在进行rehash时,会先将数据迁移到ht[1]中,等到所有数据都迁移完成时,将ht[1] 赋值给ht[0],并且释放掉ht[0]空间。
在这里插入图片描述

rehash的触发条件

Redis 用来判断是否触发 rehash 的函数是**_dictExpandIfNeeded**。所以接下来我们就先看看,_dictExpandIfNeeded 函数中进行扩容的触发条件;

//如果Hash表为空,将Hash表扩为初始大小
if (d->ht[0].size == 0) 
   return dictExpand(d, DICT_HT_INITIAL_SIZE);
 
//如果Hash表承载的元素个数超过其当前大小,并且可以进行扩容,或者Hash表承载的元素个数已是当前大小的5倍
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);
}
  • 哈希表的 LoadFactor >= 1,并且服务器没有执行 BGSAVE(RDB快照) 或者 BGREWRITEAOF(AOF重写) 等后台进程;
  • 哈希表的 LoadFactor > 5 ,表明当前的负载太严重了,需要立即进行扩容;
    (LoadFactor = used / size)

我们再来看下 Redis 会在哪些函数中,调用 _dictExpandIfNeeded 进行判断。
通过在dict.c文件中查看 _dictExpandIfNeeded 的被调用关系,我们可以发现,_dictExpandIfNeeded 是被 _dictKeyIndex 函数调用的,而 _dictKeyIndex 函数又会被 dictAddRaw 函数调用,然后 dictAddRaw 会被以下三个函数调用。

  • dictAdd:用来往 Hash 表中添加一个键值对。
  • dictReplace:用来往 Hash 表中添加一个键值对,或者键值对存在时,修改键值对。
  • dictAddorFind:直接调用 dictAddRaw。

因此,当我们往 Redis 中写入新的键值对或是修改键值对时,Redis 都会判断下是否需要进行 rehash。
在这里插入图片描述

扩容扩多大?

在 Redis 中,rehash 对 Hash 表空间的扩容是通过调用 dictExpand 函数来完成的。dictExpand 函数的参数有两个,一个是要扩容的 Hash 表,另一个是要扩到的容量,下面的代码就展示了 dictExpand 函数的原型定义:

int dictExpand(dict *d, unsigned long size);

那么,对于一个 Hash 表来说,我们就可以根据前面提到的 _dictExpandIfNeeded 函数,来判断是否要对其进行扩容。而一旦判断要扩容,Redis 在执行 rehash 操作时,对 Hash 表扩容的思路也很简单,就是如果当前表的已用空间大小为 size,那么就将表扩容到 size2 的大小。

如下所示,当 _dictExpandIfNeeded 函数在判断了需要进行 rehash 后,就调用 dictExpand 进行扩容。这里你可以看到,rehash 的扩容大小是当前 ht[0]已使用大小的 2 倍。

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

而在 dictExpand 函数中,具体执行是由 _dictNextPower 函数完成的,以下代码显示的 Hash 表扩容的操作,就是从 Hash 表的初始大小(DICT_HT_INITIAL_SIZE),不停地乘以 2,直到达到目标大小。

static unsigned long _dictNextPower(unsigned long size)
{
    //哈希表的初始大小
    unsigned long i = DICT_HT_INITIAL_SIZE;
    //如果要扩容的大小已经超过最大值,则返回最大值加1
    if (size >= LONG_MAX) return LONG_MAX + 1LU;
    //死循环直到找到不大于的最小值
    while(1) {
        //如果扩容大小大于等于最大值,就返回截至当前扩到的大小
        if (i >= size)
            return i;
        //每一步扩容都在现有大小基础上乘以2
        i *= 2;
    }
}

为什么叫渐进式

渐进式 rehash 的意思就是 Redis 并不会一次性把当前 Hash 表中的所有键,都拷贝到新位置,而是会分批拷贝,每次的键拷贝只拷贝 Hash 表中一个 bucket 中的哈希项。这样一来,每次键拷贝的时长有限,对主线程的影响也就有限了。

具体过程

在这里插入图片描述

关键函数dictRehash部分代码

//入参:dict , 需要迁移的元素个数
int dictRehash(dict *d, int n) {
    int empty_visits = n*10; /* Max number of empty buckets to visit. */
    if (!dictIsRehashing(d)) return 0;
	//迁移元素,直到迁移完毕或者迁移完n个
    while(n-- && d->ht[0].used != 0) {
        //....这段代码在下面分析
    }
    /* Check if we already rehashed the whole table... */
    //判断迁移是否完成
    if (d->ht[0].used == 0) {
    	//释放ht[0]
        zfree(d->ht[0].table);
        //将ht[0] 指向 ht[1]
        d->ht[0] = d->ht[1];
        //让ht[1]重新指向null
        _dictReset(&d->ht[1]);
        //表示rehash暂停
        d->rehashidx = -1;
        //返回迁移完成
        return 0;
    }
	//还需要继续迁移
    /* More to rehash... */
    return 1;
}

那么,每次迁移几个元素呢?

这就要提到rehashidx了。
rehashidx 变量表示的是当前 rehash 在对哪个 bucket 做数据迁移。比如,当 rehashidx 等于 0 时,表示对 ht[0]中的第一个 bucket 进行数据迁移;当 rehashidx 等于 1 时,表示对 ht[0]中的第二个 bucket 进行数据迁移,以此类推。

而 dictRehash 函数的主循环,首先会判断 rehashidx 指向的 bucket 是否为空,如果为空,那就将 rehashidx 的值加 1,检查下一个 bucket。

所以,渐进式 rehash 在执行时设置了一个变量 empty_visits,用来表示已经检查过的空 bucket,当检查了一定数量的空 bucket 后,这一轮的 rehash 就停止执行,转而继续处理外来请求,避免了对 Redis 性能的影响。下面的代码显示了这部分逻辑。

while(n-- && d->ht[0].used != 0) {
    //如果当前要迁移的bucket中没有元素
    while(d->ht[0].table[d->rehashidx] == NULL) {
        //
        d->rehashidx++;
        if (--empty_visits == 0) return 1;
    }
    ...
}

而如果 rehashidx 指向的 bucket 有数据可以迁移,那么 Redis 就会把这个 bucket 中的哈希项依次取出来,并根据 ht[1]的表空间大小,重新计算哈希项在 ht[1]中的 bucket 位置,然后把这个哈希项赋值到 ht[1]对应 bucket 中。

这样,每做完一个哈希项的迁移,ht[0]和 ht[1]用来表示承载哈希项多少的变量 used,就会分别减一和加一。当然,如果当前 rehashidx 指向的 bucket 中数据都迁移完了,rehashidx 就会递增加 1,指向下一个 bucket。下面的代码显示了这一迁移过程。

 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);
    while(d->ht[0].table[d->rehashidx] == NULL) {
        d->rehashidx++; 
        if (--empty_visits == 0) return 1;
    }
    de = d->ht[0].table[d->rehashidx];
    /* Move all the keys in this bucket from the old to the new hash HT */
    while(de) {
        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;
    }
    d->ht[0].table[d->rehashidx] = NULL;
    d->rehashidx++;
}

还有一个问题,n的大小是多少?从下面这个函数可以看到,是1。每次仅仅迁移一个元素,之后变去执行主要操作。

static void _dictRehashStep(dict *d) {
    if (d->pauserehash == 0) dictRehash(d,1);
}

看看有哪些函数调用了_dictRehashStep,Ctrl+F找一下。它们发现分别是:dictAddRaw,dictGenericDelete,dictFind,dictGetRandomKey,dictGetSomeKeys。
其中,dictAddRaw 和 dictGenericDelete 函数,分别对应了往 Redis 中增加和删除键值对,而后三个函数则对应了在 Redis 中进行查询操作。调用关系如下图。
在这里插入图片描述

总结

  • 什么是渐进式rehash?为什么要设计两个ht?
    Redis核心命令执行是单线程的,所以一次性迁移全部数据开销很大并且会阻塞服务。在需要rehash的时候,不会立即将全部数据进行迁移,而是通过辅助表来慢慢进行迁移,每次迁移1个元素,进行正常服务的时候,在ht[1]中进行添加操作,其他操作在ht[0]和ht[1]中一起进行。
  • rehashidx有什么用?
    为-1的时候表示没有rehash,为0的时候表示要迁移ht[0]中0号元素到ht[1]中,后续依次类推
  • rehash触发条件?
    扩容或者收缩

dict的伸缩:

  • 当LoadFactor大于5或者LoadFactor大于1并且没有子进程任务时,Dict扩容
  • 当LoadFactor小于0.1时,Dict收缩
  • 扩容大小为第一个大于等于used + 1的2^n
  • 收缩大小为第一个大于等于used 的2^n
  • dict采用渐进式rehash,每次访问Dict时执行一次rehash

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

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

相关文章

15、Spring框架

目录 什么是Spring Spring优点 Spring体系结构 Spring新特性 Spring的入门程序 新建立Maven项目 创建名为HelloSpring的类 新建applicationContext.xml文件 XML文件的约束信息配置 测试类TestHelloSpring 控制反转 依赖注入 依赖注入和控制反转的比较 依赖注入的…

虚拟机类加载机制

目录 1、概述 2、类加载的过程 1、过程总览 2、加载 3、链接-验证 4、链接-准备 5、链接-解析 6、初始化 7、总结 3、类加载的时机 4、类加载器 1、概述 2、类与类加载器 3、三层类加载器 4、双亲委派模型 5、其他加载策略 1、概述 一个Java类会被编译成一个Cl…

grep,sed,awk实战

grep -E ^(root|sshd)\> /etc/passwd 找出以root或者sshd开头的&#xff0c;且只含root或者sshd,>表示匹配到root或者sshd就结束了&#xff0c;类似情况如下图&#xff1a; grep -c ^yu /etc/passwd 匹配含yu这个用户出现的次数 grep -m 2 ^yu /etc/passwd …

arthas使用

文章目录 ArthasArthas&#xff08;阿尔萨斯&#xff09;能为你做什么&#xff1f;安装1.linux中使用2.docker中使用 命令列表jvm 相关class/classloader 相关monitor/watch/traceprofiler/火焰图 Arthas Arthas 是一款线上监控诊断产品&#xff0c;通过全局视角实时查看应用 …

windows nvm 安装过程

1. 官网 Releases coreybutler/nvm-windows GitHubA node.js version management utility for Windows. Ironically written in Go. - Releases coreybutler/nvm-windowshttps://github.com/coreybutler/nvm-windows/releases 下载 nvm-setup.exe; 2. 安装完成后后&#x…

Golang Channel 实现原理与源码分析

Do not communicate by sharing memory; instead, share memory by communicating. 通过通信来共享内存&#xff0c;而不是共享内存来通信 安全访问共享变量是并发编程的一个难点&#xff0c;在 Golang 语言中&#xff0c;倡导通过通信共享内存&#xff0c;实际上就是使用 chan…

23种设计模式之访问者模式(Visitor Pattern)

前言&#xff1a;大家好&#xff0c;我是小威&#xff0c;24届毕业生&#xff0c;在一家满意的公司实习。本篇文章将23种设计模式中的访问者模式&#xff0c;此篇文章为一天学习一个设计模式系列文章&#xff0c;后面会分享其他模式知识。 如果文章有什么需要改进的地方还请大佬…

chatgpt没有免费版的吗?如何使用ChatGPT?

ChatGPT是基于GPT模型的聊天机器人&#xff0c;目前没有免费版。ChatGPT是由OpenAI开发的&#xff0c;OpenAI的GPT模型需要大量的计算资源和技术支持&#xff0c;因此需要付费才能使用。 目前&#xff0c;OpenAI提供了两种方式来使用GPT模型&#xff1a; 1. OpenAI API OpenA…

制造型企业降本增效的最佳工具,质量管理系统,该如何利用好

许多制造业企业质量管理主要用于解决制造业质检效率低下、作业不规范等难题&#xff0c;形成质量检验、质量方案、档案数据、统计分析一体化的质量管理体系&#xff0c;有效为企业质量管理提速降本增效&#xff0c;实现企业数字化转型。在没有正确利用质量管理系统之前&#xf…

45个 Cha​tGPT 常用插件说明

45个 ChatGPT 常用插件说明 ChatGPT常用的45个插件&#xff0c;以及它们用途说明&#xff1a; 1/ Slack&#xff1a;查询Slack信息 2/ Zapier&#xff1a;与5000应用&#xff0c;如Google Sheets和Docs进行交互。 3/ Expedia&#xff1a;在一个地方激活你的旅行计划 4/ Kla…

Worldclim(v1.4、v2.1)数据集使用介绍

最近在使用Worldclim的数据&#xff0c;在这里记录一下该数据集的使用。 如果你想得到过去、现在和未来的气候数据&#xff0c;那么你可以使用这个数据集&#xff1a;Worldclim数据集 该数据集包含了4种时期的气候数据&#xff1a;历史时期的末次盛冰期、全新世中期、当前时…

操作系统(3.3)--线程的实现方式

进程调度的任务、机制和方式 1.进程的调度任务 进程调度的任务主要有三&#xff1a; (1)保存处理机的现场信息。在进程调度进行调度时&#xff0c;首先需要保存当前进程的处理机的现场信息&#xff0c;如程序计数器、多个通用寄存器中的内容等 (2)按某种算法选取进程。调度…

脉冲神经网络深度残差学习(ResNet)

来源&#xff1a;投稿 作者&#xff1a;小灰灰 编辑&#xff1a;学姐 论文标题&#xff1a;Deep Residual Learning in Spiking Neural Networks 论文链接: https://arxiv.org/pdf/2102.04159v3.pdf 代码链接&#xff1a;https: //github.com/fangwei123456/Spike-Element-Wi…

MYSQL数据库基础(数据库)

文章目录 一、数据库使用流程二、数据库的操作三、常用数据类型3.1 数值类型3.2 字符串类型3.3 日期类型 四、数据表操作 一、数据库使用流程 用户在客户端输入SQL语句客户端会把SQL通过网络发送给服务器服务器会执行这个SQL&#xff0c;把结果返回给客户端客户端接收到结果后…

第十九篇、基于Arduino uno,获取光电开关(NPN/PNP型)的信号——结果导向

0、结果 说明&#xff1a;先来看看串口调试助手显示的结果&#xff0c;如果有遮挡会输出低电平或者高电平&#xff0c;没有遮挡会输出高电平或者低电平&#xff0c;如果是你想要的&#xff0c;可以接着往下看。 1、外观 说明&#xff1a;这里要区分到底是NPN型号的&#xff0…

分享几个索引创建的小 Tips

文章目录 1. 冗余索引1.1 联合索引左边列1.2 索引中加入主键 2. 隐藏的索引排序3. 删除不使用的索引4. 手动更新索引统计信息5. 适时优化表 关于 MySQL 中的索引&#xff0c;松哥前面已经和小伙伴们聊了不少了&#xff0c;不过在索引使用的时候&#xff0c;还是有一些需要注意的…

如何发布一个npm包

1、注册账号 https://www.npmjs.com/ 使用邮箱注册即可 a. 邮箱会在本地登录时发送验证码使用 b. 发布包后邮箱会收到通知 2、生成AccessToken &#xff08;1&#xff09;直接本地登录 # 根据提示输入用户名、密码、注册邮箱 npm login# 输入完邮箱会发送验证码&#xff0c…

如何做一个有质量的技术分享

分享信息并不难,大多数人都能做到,就算是不善言谈性格内向的技术人员,通过博客或社交媒体,或是不正式的交流,他们都能或多或少的做到。但是如果你想要做一个有质量有高度的分享,这个就难了。 所谓的有质量和有高度,我心里面的定义有两点: 分享内容的保鲜期是很长的会被…

win11本地安装k8s

1、确保本地已经安装DesktopDocker&#xff1b; 2、使用choco下载安装Kind&#xff0c;正常下载安装报错提示&#xff0c;建议使用管理员权限 使用管理员权限下载安装Kind 也可以从github下载kind到本地进行安装&#xff0c;下载地址 Releases kubernetes-sigs/kind GitHub …

分布式锁Redis基础理论与落地实现与Redisson。

分布式锁Redis基础理论与落地实现 基本概念基于Redis的分布式锁基本用法基于Redis实现分布式锁初级版本改进Redis的分布式锁问题Redis的Lua脚本利用Lua脚本写释放锁业务流程再次改进Redis的分布式锁 总结 Redisson基于setnx实现的分布式锁存在下面的问题Redisson入门Redisson可…