Redis底层数据结构之Dict

目录

    • 一、概述
    • 二、Dict结构
    • 三、Dictht结构
    • 四、DictEntry结构
    • 五、核心特性

上一篇文章 reids底层数据结构之quicklist

一、概述

Redis 的 Dict 是一个高效的键值对映射数据结构,采用双哈希表实现以支持无锁的渐进式 Rehash,确保扩容或缩容时的高效性能。它通过哈希表节点以链表形式解决哈希冲突,允许快速的查找、插入和删除操作,是实现 Redis 各种数据类型和高级功能的基础架构之一。

二、Dict结构

在这里插入图片描述

type:这是一个指向 dictType 结构的指针,为该字典提供一套特定的操作函数。dictType 结构包含了一系列函数指针,用于定义键值对的复制、释放、比较等操作。这使得 dict 结构能够以通用的方式操作不同类型的键和值。

privdata:这是一个指向任意类型数据的指针,该数据会被传递给 dictType 结构中的各种函数。它允许这些函数拥有一个通用的接口,同时又能进行针对特定情境的操作。

ht[0]ht[1]: 这是两个 dictht(哈希表)数组,用于存储字典中的元素。Redis 使用一个渐进式的 rehashing 过程来扩展或缩小这个数据结构的容量。通常,所有的操作都在 ht[0] 上进行,当字典需要扩展或缩小时,元素会逐渐从 ht[0] 移动到 ht[1],这一过程是逐渐进行的,以避免长时间的阻塞

rehashidx: 这是一个标记,用来表示rehashing 进程的进度。当不在 rehashing 时,该值为 -1。当开始 rehashing 时,该值会被设置为 0,并随着 rehashing 过程的进行逐渐增加,直到 rehashing 完成,此时再次将该值设置为 -1。

iterators:这是正在对字典进行迭代的迭代器的数量。这个计数有助于管理对字典的迭代,确保即使在 rehashing 过程中也能安全地进行迭代操作。有活跃的迭代器时,可能会暂停 rehashing 过程,以保证迭代器的一致性和准确性。

三、Dictht结构

在这里插入图片描述

/* 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;      // HashTable数组
    unsigned long size;     // HashTable的大小
    unsigned long sizemask; // HashTable大小掩码,总是等于size - 1, 通常用来计算索引
    unsigned long used;     // 已经使用的节点数,实际上就是HashTable中已经存在的dictEntry数量
} dictht;

Redis的字典使用HashTable作为底层实现,一个HashTable中可以保存多个哈希表结点(dictEntry), 而每个dictEntry中就保存着字典中的一个键值对, table属性就是一个数组, 数组中每个元素的类型都是指向dictEntry的指针,而size属性便是table数组的长度, sizemask总是等于size - 1, 用于计算索引信息, used属性记录当前HashTable中dictEntry的总数量

table:这是一个数组,具体类型是指向字典条目(dictEntry)指针的数组。每一个 dictEntry 包含了键值对信息。这个 table 是哈希表的本质存储结构,实际的数据(键值对)都存储在这里。

size:这个成员表示哈希表中 table 数组的大小,也就是可以容纳的 dictEntry 数目。它总是2的幂次,这是为了使用位掩码(sizemask)与运算来替代取模运算,提升计算效率。

sizemask:它是与 size 成员配合使用的位掩码。它总是等于 size - 1。当计算一个键的哈希值来确定其在 table 数组中的位置时,用哈希值对 size 取模,等价于与 sizemask 进行按位与运算。后者由于只涉及二进制位运算,因此效率更高。

used:代表哈希表中已有的元素数量,即实际已经使用的 dictEntry 数目。这个数值在添加或删除键值对时会相应地增加或减少。

四、DictEntry结构

在这里插入图片描述

struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;     /* Next entry in the same hash bucket. */
};
typedef struct {
    void *key;
    dictEntry *next;
} dictEntryNoValue;

dictEntry是Dictht中结点的表现形式, 每个dictEntry都保存着一个键值对, key属性指向键值对的键对象, 而v属性则保存着键值对的值, Redis采用了联合体来定义v, 使键值对的值既可以存储一个指针, 也可以存储有符号/无符号整形数据,甚至可以存储浮点形数据, Redis使用联合体的形式来存储键值对的值可以让内存使用更加精细灵活, 另外, 既然是HashTable, 不可避免会发生两个键不同但是计算出来存放索引相同的情况, 为了解决Hash冲突的问题, dictEntry还有一个next属性, 用来指向与当前dictEntry在同一个索引的下一个dictEntry.

五、核心特性

哈希算法

Redis计算哈希值和索引值方法如下:

 #1、使用字典设置的哈希函数,计算键 key 的哈希值
 hash = dict->type->hashFunction(key);

 #2、使用哈希表的sizemask属性和第一步得到的哈希值,计算索引值
 index = hash & dict->ht[x].sizemask;

解决哈希冲突

当两个或更多的键被哈希到同一位置时(即发生哈希冲突),Redis 通过链地址法来解决冲突,即在这个位置维护一个链表,所有哈希到这个位置的键值对都会被加入到这个链表中。

自动扩容和缩容

可以预见的是,随着我们不断的对HashTable进行操作,可能会发生以下两种情况:

  • 不断的向HashTable中添加数据,HashTable中每个索引上的dictEntry数量会越来越多,也就是单链表会越来越长,这会十分影响字典的查询效率(最坏的场景可能要把整个单链表遍历完毕才能确定一个Key对应的dictEntry是否存在), 而Redis通常被当做缓存,这种低性能的场景是不被容许的.

  • 向一个本身已经十分巨大的HashTable执行删除节点的操作,由于原先这个HashTable的size很大(也就是说table数组十分巨大,我们假设size为M), 但是执行了大量的删除操作之后,table数组中很多元素指向了NULL(由于对应的索引上已经没有任何dictEntry, 相当于一个空的单链表), HashTable中剩余结点我们假设为N,这时M远大于N,也就是说之上table数组中至少有M - N个元素指向了NULL,这是对内存空间的巨大浪费,而Redis是内存型数据库,这种浪费内存的场景也是不被容许的.

针对以上两种场景,为了让HashTable的负载因子(HashTable中所有dictEntry的数量/HashTable的size值)维持在一个合理的范围内,Redis在HashTable保存的dictEntry数量太多或者太少的时候,会对HashTable的大小进行扩展或者收缩,在没有执行Rehash操作时,字典的所有数据都存储在ht[0]所指向的HashTable中,而在Rehash操作过程中,Redis会创建一个新的HashTable, 并且令ht[1]指向它,然后逐步的将ht[0]指向的HashTable的数据迁移到ht[1]上来

判断扩展HashTable的逻辑

/* Expand the hash table if needed */
static int _dictExpandIfNeeded(dict *d)
{
   /* Incremental rehashing already in progress. Return. */
   if (dictIsRehashing(d)) return DICT_OK;

   /* If the hash table is empty expand it to the initial size. */
   if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);

   /* If we reached the 1:1 ratio, and we are allowed to resize the hash
   * table (global setting) or we should avoid it but the ratio between
   * elements/buckets is over the "safe" threshold, we resize doubling
   * the number of buckets. */
   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;
}

每次获取一个Key的索引信息时,都会调用上述的_dictExpandIfNeeded(dict *d)方法判断是否需要对当前HashTable执行扩展操作,满足下列任意条件之一,便会执行扩展操作:

  • 当前ht[0]所指向的HashTable大小为0
  • 服务器目前没有执行BGSAVE或者BGREWRITEAOF操作,并且HashTable的负载因子大于等于1(d->ht[0].used >= d->ht[0].size)
  • 服务器目前正在执行BGSAVE或者BGREWRITEAOF操作,并且HashTable的负载因子大于5(dict_force_resize_ratio = 5)

判断收缩HashTable的逻辑

int htNeedsResize(dict *dict) {
   long long size, used;

   size = dictSlots(dict);
   used = dictSize(dict);
   return (size > DICT_HT_INITIAL_SIZE &&
            (used*100/size < HASHTABLE_MIN_FILL));
}

/* Resize the table to the minimal size that contains all the elements,
* but with the invariant of a USED/BUCKETS ratio near to <= 1 */
int dictResize(dict *d)
{
   int minimal;

   if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;
   minimal = d->ht[0].used;
   if (minimal < DICT_HT_INITIAL_SIZE)
      minimal = DICT_HT_INITIAL_SIZE;
   return dictExpand(d, minimal);
}

Redis会定期的检查数据库字典HashTable的状态,当HashTable的负载因子小于0.1时,会自动的对HashTable执行收缩操作

rehash过程

  • 分配空间:为ht[1]指向的HashTable分配空间, 分配空间的大小取决于要执行的操作,以及ht[0]所指向HashTable中dictEntry结点的数量, 也就是ht[0].used中记录的值:
    • 如果当前执行的是扩展操作,那么新HashTable的大小为第一个大于等于2 * ht[0].used的2的n次方幂
    • 如果当前执行的是收缩操作,那么新HashTable的大小为第一个大于等于ht[0].used的2的n次方幂
  • 渐进式rehash:将ht[0]所指向HashTable中的所有dictEntry节点都迁移到ht[1]所指向的新HashTable上(由于两个HashTable的size不同,所以在迁移过程中要重新计算dictEntry的索引,这也就是rehash的关键所在)
  • 迁移完成:当迁移完成之后,ht[0]所指向的HashTable中已经没有任何节点,释放该HashTable, 并且令ht[0]指向迁入节点的新HashTable, 最后为ht[1]创建一个空白的HashTable,为下一次rehash做准备

在这里插入图片描述
说明:可以看到当前rehashidx指向ht[0]的索引2,这说明ht[0][0, rehashidx - 1]对应buckets上的dictEntry都已经迁移完毕,另外我们发现正在执行渐进式rehash字典中的数据一部分在ht[0]中,而另一部分在ht[1]中,所以在渐进式rehash执行期间,字典的删除,查找以及更新操作,都会在两个HashTable上执行(先尝试在ht[0]上执行操作,如果没有成功,则再尝试在ht[1]上进行操作),而在渐进式rehash执行期间,如果我们需要往字典中添加新的结点,则会一律添加到ht[1]上,这样可以保证ht[0]上的结点只减不增(也就是已经迁移过的buckets不会再出现新的结点)

渐进式rehash

为了避免rehash过程阻塞服务器,Redis采用渐进式rehash策略。在进行rehash期间,所有的读写操作都会同时在ht[0]ht[1]上进行,并逐步将ht[0]上的键值对迁移到ht[1]

rehash触发条件

  • 负载因子过高(扩容):
    • 当前ht[0]所指向的HashTable大小为0
    • 服务器目前没有执行BGSAVE或者BGREWRITEAOF操作,并且HashTable的负载因子大于等于1(d->ht[0].used >= d->ht[0].size)
    • 服务器目前正在执行BGSAVE或者BGREWRITEAOF操作,并且HashTable的负载因子大于5(dict_force_resize_ratio = 5)
  • 负载因子过低(缩容):
    • Redis会定期的检查数据库字典HashTable的状态,当HashTable的负载因子小于0.1时,会自动的对HashTable执行收缩操作

Dict优缺点

Dict 提供了快速的键值对查找和插入性能,能高效地支持数据的增删改查操作;然而,在内存使用上它可能不如一些更紧凑的数据结构高效,尤其是在存储大量小对象时,还有当哈希表发生扩容或收缩时可能会有短暂的性能下降。

参考 redis底层数据结构-Dict

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

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

相关文章

linux autogroup

一&#xff1a;概述 对于linux autogroup的作用&#xff0c;很多同学可能是听说过&#xff0c;但&#xff0c;并未验证过。 考虑下面场景&#xff0c;开两个terminal&#xff0c;T1和T2&#xff0c;在T1中运行进程P1&#xff0c;P1开启9个线程编译代码&#xff0c;在T2中运行…

Datawhale ChatGPT基础科普

根据课程GitHub - datawhalechina/hugging-llm: HuggingLLM, Hugging Future. 摘写自己不懂得一些地方&#xff0c;具体可以再到以上项目地址 LM&#xff1a;这是ChatGPT的基石的基石。 Transformer&#xff1a;这是ChatGPT的基石&#xff0c;准确来说它的一部分是基石。 G…

销售经理与员工:如何展开有效的绩效面谈

在当今竞争激烈的商业环境中&#xff0c;销售经理与员工之间的绩效面谈显得尤为重要。有效的绩效面谈不仅能够提升员工的工作积极性&#xff0c;促进团队的整体绩效&#xff0c;还能够加强销售经理与员工之间的沟通与理解&#xff0c;为企业的发展奠定坚实的基础。本文将探讨销…

7.2K star!一个完全免费,可以本地部署的 AI 搜索聚合器。新手可尝试

原文链接&#xff1a;7.2K star&#xff01;一个完全免费&#xff0c;可以本地部署的 AI 搜索聚合器。新手可尝试 ChatGPT 刚上线的时候我用的很少&#xff0c;还是习惯用 Google。主要还是因为不信任&#xff0c;怕它对我胡说八道。 慢慢的&#xff0c;也没有一个明确的时间…

Linux的学习之路:19、进程信号(1)

摘要 今天这张说一下信号的一部分知识 目录 摘要 一、信号 1、生活角度的信号 2、技术应用角度的信号 3、注意 4、用kill -l命令可以察看系统定义的信号列表 5、信号处理常见方式概览 二、产生信号 1、通过终端按键产生信号 2、调用系统函数向进程发信号 3、由软件…

<前端>Electron-builder为公证后的app打更新信息latest.yml

MacOS下&#xff0c;Electron-builder可以很方便的为测试包app打更新信息&#xff08;latest-mac.yml&#xff09;。 但是&#xff0c;正式发布的时候&#xff0c;不可能用测试包app&#xff0c;因为还没有进行公证。如何为公证的app打latest-mac.yml呢。 其实观察latest-mac.y…

FPGA秋招-笔记整理(1)

一、关键路径 关键路径通常是指同步逻辑电路中&#xff0c;组合逻辑时延最大的路径&#xff08;这里我认为还需要加上布线的延迟&#xff09;&#xff0c;也就是说关键路径是对设计性能起决定性影响的时序路径。也就是静态时序报告中WNS&#xff08;Worst Nagative Slack&…

Git 核心概念与实操

这里写目录标题 1 版本回退2 工作区、暂存区、本地仓库、远程仓库 1 版本回退 原文链接&#xff1a;https://www.liaoxuefeng.com/wiki/896043488029600/897013573512192 首先 git log 查看提交记录 在Git中&#xff0c;用 HEAD 表示当前版本 上一个版本就是 HEAD^ &#xff…

Linux-进程间通信:System V消息队列

目录 System V IPC概述标识符与IPC Key System V消息队列创建或打开一个消息队列发送消息接收消息控制消息队列1、IPC_STAT2、IPC_SET3、IPC_RMID 查看系统当前的消息队列代码示例 System V IPC&#xff08;Inter-Process Communication&#xff09;是一组用于在 Unix-like 操作…

【C语言】手撕二叉树

标题&#xff1a;【C语言】手撕二叉树 水墨不写bug 正文开始&#xff1a; 二叉树是一种基本的树形数据结构&#xff0c;对于初学者学习树形结构而言较容易接受。二叉树作为一种数据结构&#xff0c;在单纯存储数据方面没有 顺序表&#xff0c;链表&#xff0c;队列等线性结构…

sklearn 笔记 metrics

1 分类 1.1 accuracy_score 分类准确率得分 在多标签分类中&#xff0c;此函数计算子集准确率&#xff1a;y_pred的标签集必须与 y_true 中的相应标签集完全匹配。 1.1.1 参数 y_true真实&#xff08;正确&#xff09;标签y_pred由分类器返回的预测标签normalize 默认为 Tr…

Linux:Win10平台上,用VMware安装Centos7.x及系统初始化关键的相关配置(分步骤操作,详细,一篇足以)

VMware安装Centos7.x镜像的详细步骤&#xff1a;VMWare安装Centos系统&#xff08;无桌面模式&#xff09; 我这里是为了安装Hadoop集群&#xff0c;所以&#xff0c;以下这些步骤是必须进行的 如果你是学习Linux&#xff0c;可以跳过非必须的那些配置项 我安装的版本是&…

前端实现将二进制文件流,并下载为excel文件

目录 一、关于二进制流二、项目实践三、常见问题及解决 一、关于二进制流 含义&#xff1a;二进制流是一种计算机文件格式&#xff0c;它的数据以二进制形式存储&#xff0c;与文本文件不同。 二进制文件可以包含任意类型的数据&#xff0c;例如&#xff1a;图像、音频、视频…

智慧园区引领产业智慧化:深入探索智慧技术如何点亮园区创新发展之路,构建未来产业生态圈,驱动区域经济持续升级

目录 一、引言 二、智慧园区的内涵与特征 三、智慧技术点亮园区创新发展之路 1、智慧技术推动产业转型升级 2、智慧技术促进新兴产业发展 3、智慧技术提升园区创新能力 四、智慧园区在产业智慧化中的作用与价值 1、优化资源配置&#xff0c;提高经济效益 2、提升服务品…

Kibana安装部署(Linux)

Kibana是Elasticsearch的开源可视化工具&#xff0c;与存储在Elasticsearch中的数据进行交互。 1. 下载软件 这里使用的Elasticsearch的版本是7.12.0&#xff0c;所以kibana选择同样的7.12.0版本。 官网下载地址&#xff1a;https://www.elastic.co/cn/downloads/past-releas…

【全网首发】Mogdb 5.0.6新特性:CM双网卡生产落地方案

在写这篇文章的时候&#xff0c;刚刚加班结束&#xff0c;顺手写了这篇文章。 前言 某大型全国性行业核心系统数据库需要A、B两个物理隔离的双网卡架构方案&#xff0c;已成为行业标准。而最新发布的MogDB 5.0.6的CM新增支持流复制双网段部署&#xff0c;用于网卡级高可用容灾(…

Meta 向第三方开放 MR 操作系统;黄仁勋:人形机器人成本可能比人们预期要低得多丨 RTE 开发者日报 Vol.190

开发者朋友们大家好&#xff1a; 这里是「RTE 开发者日报」&#xff0c;每天和大家一起看新闻、聊八卦。我们的社区编辑团队会整理分享 RTE&#xff08;Real Time Engagement&#xff09; 领域内「有话题的新闻」、「有态度的观点」、「有意思的数据」、「有思考的文章」、「有…

大厂产品专家是做晋升述职的?

在大厂里,晋升都是需要述职的。与年终述职不同,晋升述职要求严格很多。这种情况下,如何完美表达自己才是适合晋升的人选?这篇文章,值得即将晋升和准备晋升的各位看看。 之前学姐写了一篇文章,讲怎么做年度述职,反响还不错~有兴趣的童鞋可以戳这里复习。今天学姐来讲一个…

25计算机考研院校数据分析 | 上海交通大学

上海交通大学电子信息与电气工程学院成立于2001年12月&#xff0c;其前身可湖源至百年前的电机专科&#xff0c;具有中国电气工程师“摇篮”之美称。50年代根据学科发展需要分为电工与计算机科学系(三系)和电子工程系(四系)。1985年&#xff0c;三系和四系合并&#xff0c;成立…

【LeetCode:216. 组合总和 III + 递归】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…