Redis字典实现

前言

        字典又称符号表,关联数组或者映射(map)。是一种保存键值对的抽象数据结构。在字典中一个键和一个值进行关联。这些关联的值被称为键值对。

        字典中每一个键都是独一无二的,没有重复的。我们可以通过键来查找值,更新值或者删除整个键值对等操作。

        字典在Redis中应用广泛,比如Redis数据库的底层就是使用字典来实现的。对数据库的增删查改,该操作也是构建在对字典的操作之上。

        出来用来表示数据库外,字典还是哈希键(说的是Redis的key)的底层实现之一,当一个哈希键包含的哈希键比较多,又或者键值对中的元素都是比较长的字符串时,Redis会使用字典作为哈希键的底层实现。

        由于Redis是用C语言实现的,没有内置字典数据结构,Redis自己构建了字典的实现。

一.字典的实现

        Redis的字典使用作为底层实现,每一个哈希表节点就保存了字典中的一个键值对。

        1.1 哈希表

        Redis字典所使用的哈希表有dict.h/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;
    //哈希表大小
    unsigned long size;
    //哈希表大小掩码,用于计算索引值
    //总是等于size-1
    unsigned long sizemask;
    //该哈希表已有的节点数
    unsigned long used;
} dictht;
  • table: 是一个数组,数值中的每一个元素都是指向dictEntry类型指针,每一个dictEntry中都保存着一个键值对。
  • size: 记录了哈希表的大小,也就是table的大小。
  • used: 记录了哈希表中已有的节点(键值对)数。
  • sizemask: 值总是等于size-1,这个属性和哈希值共同决定了一个键应该被放到table数组的哪一个索引上。

        1.2 哈希表节点

        哈希表的节点使用dictEntry结构表示,每一个dictEntry都保存这一个键值对:

typedef struct dictEntry {
    //键
    void *key;
    //值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    //指向下一个节点,形成链表
    struct dictEntry *next;
} dictEntry;
  • key: 保存键值对中的键
  • v: 保存键值对中的值。可以是一个指针,或者是上面的三种类型
  • next: 指向另一个哈希表节点的指针,可以将哈希值相同的节点连接到一起,解决哈希冲突。

        1.3 字典

        Redis的字典由dict.h/dict结构表示:

typedef struct dict {
    //类型特定函数
    dictType *type;
    //私有数据
    void *privdata;
    //哈希表
    dictht ht[2];

    //rehash索引
    //当rehash不在进行时,值为-1
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */

    //rehash中断标志
    //大于0表示rehash被中断
    int16_t pauserehash; /* If >0 rehashing is paused (<0 indicates coding error) */
} dict;

        type和privdata属性是针对不同类型的键值对,为创建多态字典而设置的:

  • type: 属性是指向一个dictType结构的指针,每一个dictType结构中保存了一簇用于操作特定类型键值对的函数,Redis会为用途不同的字典设置不同类型的特定函数。
  • privdata: 属性保存了需要传给那些类型特定函数的可选参数。
typedef struct dictType {
    //计算哈希值函数
    uint64_t (*hashFunction)(const void *key);
    //复制键函数
    void *(*keyDup)(void *privdata, const void *key);
    //复制值函数
    void *(*valDup)(void *privdata, const void *obj);
    //对比键函数
    int (*keyCompare)(void *privdata, const void *key1, const void *key2);
    //销毁键函数
    void (*keyDestructor)(void *privdata, void *key);
    //销毁值函数
    void (*valDestructor)(void *privdata, void *obj);

    int (*expandAllowed)(size_t moreMem, double usedRatio);
} dictType;
  • ht属性: 是一个包含两个项的数组,每一个项都是一个dict哈希表。一般情况下,字典只是用ht[0]哈希表,ht[1]只会在对对ht[0]进行rehash时使用。
  • rehashidx: 记录rehash进度,如果没有进行rehash该值为-1。

        1.4 哈希算法

        当将一个新键值对添加到字典时,程序需要先根据键值对的键计算出哈希值和索引值。然后再根据索引值,将包含新键值对的哈希表的节点放在哈希表数组指定索引上。

//Redis计算哈希值和索引值方法:
//使用字典设置的哈希函数,计算key的哈希值
hash = dict->type->hashFunction(key);

//使用哈希表的sizemask属性和哈希值,计算出索引值
//根据情况不同,ht[x]可以是ht[0]或ht[1]
index = hash & dict->ht[x].sizemask;

        当字典被作为数据库或者哈希见底层实现时,Redis使用的是MurmurHash算法来计算键的哈希值。MurmurHash算法是由Austin Appleby于2008年发明,这种算法有点在于,即使输入的键是有规律的,算法仍然能给出很好的随机分布,而且算法速度也很快。MurmurHash现在也有很多版本。

        1.5 解决键冲突

        当由两个或者两个以上的键被分配到哈希表的同一个索引上,我们称这些键发生了冲突。

        Redis的哈希表使用链地址法来解决键冲突。实际Redis的哈希表是一个哈希桶,哈希表的节点中有一个next指针,冲突的键会以单向链表的方式连接在哈希表的同一索引位置。

        程序会使用头插法,将新节点加到链表中。

        哈希表介绍链接:【精选】哈希表(散列表)介绍_hash表_两片空白的博客-CSDN博客

        1.6 rehash 

        1.6.1 步骤

        随着操作的不断进行,哈希表保存的键值对会逐渐地增多或者减少。为了让哈希表的负载因子维持在一个合理范围内,当哈希表保存的键值对的数量太多或者太少时,程序需要对哈希表的大小进行扩展或者收缩。

        扩展和收缩操作可以通过执行rehash(重新排列)操作来完成。Redis对字典的哈希表的rehash步骤如下:

        1. 为字典的ht[1]哈希表分配空间,这个哈希表的空间大小取决于要执行的操作,以及ht[0]中当前包含的键值对的数量(也就是ht[0]中used属性的值)。

  • 如果执行的是扩展操作,那么ht[1]的大小为第一个大于等于ht[0].used*2的2的n次方幂。比如:used为3,3*2等于6,第一个大于等于6的2的n次方幂为8,ht[1]的大小为8。
  • 如果执行的是收缩操作,那么ht[1]的大小为第一个大于等于ht[0].used的2的n次方幂。

        2. 将保存在ht[0]中的所有键值对rehash到ht[1]上,rehash是指以ht[1]哈希表重新计算键的哈希值和索引值,然后将键值对按照索引放到ht[1]指定位置。

        3. 将ht[0]包含的所有键值对都迁移到ht[1]后,ht[0]就变成了一个空的哈希表。释放ht[0]空间,将ht[1]设置为ht[0],并在ht[1]新创建一个空的哈希表,为下一次rehash做准备。

        举个例子:

        假设程序要对下面的字典的ht[0]进行扩展操作,那么程序会进行以下操作:

  • ht[0].used当前的值为4,4*2=8,正好是2的3次幂,所以程序会将ht[1]哈希表的大小设置为8。下图为分配空间之后的样子:

  • 将ht[0]包含的4个键值对都rehash到ht[1]

  • 释放ht[0],将ht[1]设置为ht[0],然后为ht[1]分配一个空哈希表。至此,对哈希表的扩展执行完毕,程序成功将哈希表的大小从原来的4扩展到8。    

         1.6.2 哈希表的扩展和收缩

        1. 当以下任意一个条件满足时,程序会自动开始对哈希表执行扩展操作:

  • redis服务器目前没有进行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于1。
  • redis服务器正在进行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于5。

        哈希表的负载因子公式:load_factor = ht[0].used / ht[0].size

        根据BGSAVE命令或者BGREWRITEAOF命令是否正在执行,服务器执行扩展操作的负载因子不同,这是因为在执行BGSAVE命令或者BGREWRITEAOF命令的过程中,Redis需要创建当前服务器进程的子进程,而大多数操作系统都是通过写时复制技术来优化子进程效率。所以在子进程存在期间,服务器会提高执行扩展操作所需的负载因子,为了避免子进程存在期间进行哈希表的扩展操作,从而避免不必要的内存写入,最大限度的节约内存。

        2. 当哈希表的负载因子小于0.1时,程序自动开始对哈希表进行收缩操作。

        1.7 渐进式rehash

        1.7.1 步骤

        在字典进行扩展和收缩操作时,需要将哈希表ht[0]上所有键值对rehash到ht[1]上,但是rehash的动作并不是一次性,集中完成的,而是分多次,渐进式完成的。

        原因是,当哈希表中保存的键值对数量很大,那么要一次性的将所有键值对全部rehash到ht[1]的话,庞大的计算量可能会导致服务器在一段时间内停止服务。

         哈希表渐进式rehash的详细步骤:

  1. 为ht[1]分配空间,让字典同时拥有ht[0]和ht[1]两个哈希表。
  2. 在字典中维持一个rehashindex索引计数器变量,并把它设置为0,表示rehash工作正式开始。
  3. 在rehash进行期间,每次对字典进行查找,删除,插入或者更新操作时,程序除了执行指定的操作外,还会顺带将ht[0]哈希表在rehashindex所索引上的所有键值对rehash到ht1[1]上,当rehash工作完成之后,程序将rehashindex属性值加一。
  4. 随着字典的操作的不断执行,最终在某个时间点上,ht[1]哈希表上的所有键值对全部被rehash到ht[1]上,这时程序将rehashindex属性值设为-1,表示rehash操作已经完成。

        渐进式rehash好处在于它采用分而治之的方式,将rehash键值对所需要的计算工作均摊到对字典进行查找,插入,删除或更新操作上,从而避免集中式rehash带来的庞大工作量。

        rehash演示:

 

        1.7.2 渐进式rehash期间对哈希表的操作

         因为在rehash期间,字典中同时存在ht[0]和ht[1]两张哈希表。字典在进行查找,插入,删除和更新操作时会在两个哈希表上进行操作。例如:在字典中查找某一个键时,程序会先在ht[0]中查找,如果没有找到,会继续再ht[1]中查找。

        另外再rehash期间,新增到字典中的键值对一律会保存到ht[1]中,而ht[0]不会进行任何添加操作,这一操作保证了ht[0]包含的键值对的数量只减不增,并且随着rehash操作最终会变成一个空表。

        1.8 API

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

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

相关文章

【封装UI组件库系列】搭建项目及准备工作

封装UI组件库系列第一篇搭建项目 前言 &#x1f31f;搭建项目 创建工程 基本结构 1.创建8个组件展示页面 ​ 2.配置路由文件router/index.js 3.页面布局 &#x1f31f;总结 前言 在前端开发中&#xff0c;大家可能已经用过各种各样的UI组件库了&#xff0c;现在市面上热…

最大子段和(分治法+动态规划法)

求最大子段和 此类问题通常是求数列中连续子段和的最大值&#xff0c;经典的股票问题就是考察的这个思想及拓展。 例题&#xff1a; AcWing:1054. 股票买卖 Leetcode:53. 最大子数组和 分治法O(nlogn) 此类问题时分适合采用分治思想&#xff0c;因为所有子区间 [ s t a r t …

网工内推 | 国企、港企网工,年底双薪,NA以上认证即可

01 中航期货有限公司 招聘岗位&#xff1a;信息技术部-网络工程师 职责描述&#xff1a; 1、负责总部、分支机构、外联单位网络的日常运维、故障和应急处置&#xff0c;特别是定期监测设备的运行状态&#xff0c;对存在隐患的地方及时发现改正&#xff0c;保持网络稳定通畅&am…

利用JDBC及Servlet实现对信息录入注册功能的实现

利用JDBC及Servlet实现对登录注册功能的实现&#xff1b; 1.题目要求&#xff1a; 1、新建一个数据库名为&#xff08;个人姓名拼音&#xff09;&#xff0c;表&#xff08;学生所在城市&#xff09;&#xff0c;字段&#xff08;sid&#xff1a;学号&#xff0c;sname&#x…

从硬件到软件:揭秘磁盘结构和文件系统组织

&#x1f4df;作者主页&#xff1a;慢热的陕西人 &#x1f334;专栏链接&#xff1a;Linux &#x1f4e3;欢迎各位大佬&#x1f44d;点赞&#x1f525;关注&#x1f693;收藏&#xff0c;&#x1f349;留言 本博客主要内容讲解了从磁盘的硬件结构&#xff0c;再到操作系统内部是…

采集1688整店商品(店铺所有商品、店铺列表api)

返回数据&#xff1a; 请求链接 {"user": [],"items": {"item": [{"num_iid": "738354436678","title": "国产正品i13 promax全网通5G安卓智能手机源头厂家批发手机","pic_url": "http…

Altium Designer内电层(Plan)GND和POWER出现的死铜如何去除-AD

1.问题描述 更多遇到的是顶层底层敷铜时出现清楚死铜&#xff1b;但是在内电层有时候也会出现死铜。这时候不去除死铜就会在DRC中报错。 2.解决办法1-多边形填充挖空 在工具栏&#xff1a; 放置——多边形填充挖空&#xff1b;然后再错误高亮处的死铜周围画多边形&#xff0c…

制作Go程序的Docker容器(以及容器和主机的网络问题)

今天突然遇到需要将 Go 程序制作成 Docker 的需求&#xff0c;所以进行了一些研究。方法很简单&#xff0c;但是官方文档和教程有些需要注意的地方&#xff0c;所以写本文进行记录。 源程序 首先介绍一下示例程序&#xff0c;示例程序是一个 HTTP 服务器&#xff0c;会显示si…

机器学习第10天:集成学习

文章目录 机器学习专栏 介绍 投票分类器 介绍 代码 核心代码 示例代码 软投票与硬投票 bagging与pasting 介绍 核心代码 随机森林 介绍 代码 结语 机器学习专栏 机器学习_Nowl的博客-CSDN博客 介绍 集成学习的思想是很直观的&#xff1a;多个人判断的结合往往比…

通信网络安全防护定级备案流程介绍(附流程图)

通信网络安全防护定级备案是拥有增值电信业务经营许可证并且有开展电信业务的企业要做的一件事情。刚接触这块的家人们在填报操作的时候可能对具体通信网络安全防护定级备案流程还不是很清楚&#xff0c;所以就给大家画张具体的流程图吧&#xff0c;可以更加直观的了解。 通信…

栈和队列知识点+例题

1.栈 1.1栈的概念及结构 栈&#xff1a;一种特殊的线性表&#xff0c;其只允许在固定的一端进行插入和删除元素的操作。进行数据插入和删除操作的一端成为栈顶&#xff0c;另一端成为栈底。遵守后进先出的原则&#xff08;类似于弹夹&#xff09; 压栈&#xff1a;栈的插入操…

【考研数学】正交变换后如果不是标准型怎么办?| 关于二次型标准化的一些思考

文章目录 引言一、回顾二次型的定义是什么&#xff1f;什么叫标准二次型&#xff1f;怎么化为标准型&#xff1f; 二、思考写在最后 引言 前阵子做了下 20 年真题&#xff0c;问题大大的&#xff0c;现在订正到矩阵的第一个大题&#xff0c;是关于二次型正交变换的。和常规不同…

『亚马逊云科技产品测评』活动征文|通过lightsail一键搭建Drupal VS 手动部署

『亚马逊云科技产品测评』活动征文&#xff5c;通过lightsail一键搭建Drupal 提示&#xff1a;授权声明&#xff1a;本篇文章授权活动官方亚马逊云科技文章转发、改写权&#xff0c;包括不限于在 Developer Centre, 知乎&#xff0c;自媒体平台&#xff0c;第三方开发者媒体等亚…

面试官:你能说说常见的前端加密方法吗?

给大家推荐一个实用面试题库 1、前端面试题库 &#xff08;面试必备&#xff09; 推荐&#xff1a;★★★★★ 地址&#xff1a;web前端面试题库 前言 本篇文章略微介绍一下前端中常见的加密算法。前端中常见的加密算法主要形式包括——哈希函数&#xff0c;对称…

图片叠加_图片压缩

图片叠加 try {/* 1 读取第一张图片*/File fileOne new File("1.png");BufferedImage imageFirst ImageIO.read(fileOne);/* 2读取第二张图片 */File fileTwo new File("2.png");BufferedImage imageSecond ImageIO.read(fileTwo);//创建一个最底层画…

Ftrans自动同步软件:满足企业级数据同步的需求

随着数字经济的发展&#xff0c;企业数字化的办公场景越来越复杂&#xff0c;其中一个急需解决的问题就是企业不同服务器之间的文件自动同步的需求。然而&#xff0c;目前市场上的同步软件通常有很多的缺点&#xff0c;让用户感到困扰。 1、数据安全&#xff1a;在同步数据的过…

Apache POI(Java)

一、Apache POI介绍 Apache POI是Apache组织提供的开源的工具包&#xff08;jar包&#xff09;。大多数中小规模的应用程序开发主要依赖于Apache POI&#xff08;HSSF XSSF&#xff09;。它支持Excel 库的所有基本功能; 文本的导入和导出是它的主要特点。 我们可以使用 POI 在…

起立科技(起鸿)在第25届高交会上展示透明OLED技术创新

第二十五届中国国际高新技术成果交易会 日期&#xff1a;2023年11月15日 地点&#xff1a;福田会展中心7号馆 深圳&#xff0c;2023年11月15日 — 起鸿科技&#xff0c;作为透明OLED领域的引领者&#xff0c;于今日参展了第二十五届中国国际高新技术成果交易会。这一展会将汇…

云计算赛项容器云2023搭建

部署容器云平台[5 分] 使 用 OpenStack 私 有 云 平 台 创 建 两 台 云 主 机 &#xff0c; 云 主 机 类 型 使 用 4vCPU/12G/100G 类型&#xff0c;分别作为 Kubernetes 集群的 Master 节点和 node 节点&#xff0c; 然后完成 Kubernetes 集群的部署&#xff0c;并完成 Istio …

MAC上修改mysql的密码(每一步都图文解释哦)

当你想要连接本机数据库时&#xff0c;是不是有可能突然忘记了自己的数据库密码? 在此文中&#xff0c;我们来详细解决一下如何去修改自己的数据库密码&#xff0c;并使用Navicat来连接测试 1.停止mysql服务 打开终端&#xff0c;键入命令,将mysql服务先停止掉&#xff0c;…