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-1unsigned 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不在进行时,值为-1long 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

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

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

相关文章

如何定位el-tree中的树节点当父元素滚动时如何定位子元素

使用到的方法 Element 接口的 scrollIntoView() 方法会滚动元素的父容器&#xff0c;使被调用 scrollIntoView() 的元素对用户可见。 参数 alignToTop可选 一个布尔值&#xff1a; 如果为 true&#xff0c;元素的顶端将和其所在滚动区的可视区域的顶端对齐。相应的 scrollIntoV…

算法学习 day26

第二十六天 最大子数组和 53. 最大子数组和 - 力扣&#xff08;LeetCode&#xff09; 动态规划问题 class Solution {public int maxSubArray(int[] nums) {int len nums.length;int[] dp new int[len];dp[0] nums[0];int res dp[0];for(int i 1; i < len; i){dp[i] …

Docker 可视化面板 ——Portainer

Portainer 是一个非常好用的 Docker 可视化面板&#xff0c;可以让你轻松地管理你的 Docker 容器。 官网&#xff1a;Portainer: Container Management Software for Kubernetes and Docker 【Docker系列】超级好用的Docker可视化工具——Portainer_哔哩哔哩_bilibili 环境 …

OpenCV C++ 图像 批处理 (批量调整尺寸、批量重命名)

文章目录 图像 批处理(调整尺寸、重命名)图像 批处理(调整尺寸、重命名) 拿着棋盘格,对着相机变换不同的方角度,采集十张以上(以10~20张为宜);或者棋盘格放到桌上,拿着相机从不同角度一通拍摄。 以棋盘格,第一个内焦点为坐标原点,便于计算世界坐标系下三维坐标; …

远程文件包含演示

远程文件包含 基本介绍 受害机器 10.9.47.181 攻击者机器1 10.9.47.41 攻击者机器2 10.9.47.217 实现过程 受害者机器开启phpstudy 并且开启允许远程连接 攻击者机器1上有一个文件&#xff0c;内容是phpinfo(); 攻击者机器1提供web服务使得受害者机器能够访问到攻击者…

庖丁解牛:NIO核心概念与机制详解 02 _ 缓冲区的细节实现

文章目录 PreOverview状态变量概述PositionLimitCapacity演示&#xff1a; 观察变量 访问方法get() 方法put()方法类型化的 get() 和 put() 方法 缓冲区的使用&#xff1a;一个内部循环 Pre 庖丁解牛&#xff1a;NIO核心概念与机制详解 01 接下来我们来看下缓冲区内部细节 Ov…

IPO解读丨高处不胜寒,澜沧古茶低头取暖?

自A股注册制改革不断深化并全面落地后&#xff0c;不少意欲登陆资本市场的企业转战港股。这个过程中&#xff0c;诞生了很多以“港股”为前缀的“第一股”——“白酒第一股”珍酒李渡、“水果零售第一股”百果园、“智能驾驶第一股”知行汽车、“运动科技第一股”Keep…… 由A…

力扣刷题-二叉树-完全二叉树的节点个数

222.完全二叉树的节点个数 给出一个完全二叉树&#xff0c;求出该树的节点个数。 示例 1&#xff1a; 输入&#xff1a;root [1,2,3,4,5,6] 输出&#xff1a;6 示例 2&#xff1a; 输入&#xff1a;root [] 输出&#xff1a;0 示例 3&#xff1a; 输入&#xff1a;root [1]…

springMvc中的拦截器【巩固】

先实现下想要的拦截器功能 package com.hmdp.utils;import com.hmdp.entity.User; import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.Ht…

Pandas get_dummies用法

get_dummies 是 pandas 实现one hot encode的方式 ​  one-hot的基本思想&#xff1a;将离散型特征的每一种特征取值都看成一种状态&#xff0c;若指定离散特征中有N个 不相同的取值&#xff0c;那么我们就可以将该特征抽象成N种不同的状态&#xff0c;one-hot编码保证了每一…

10 Redis的持久化

Redis支持RDB和AOF两种持久化机制 1、RDB(Redis DataBase) 是对命令的全量快照随着key的数量增大&#xff0c;那么写入磁盘的开销也会越来越大 2、RDB文件的生成是否会阻塞主线程 save: 使用save的方式会阻塞主线程&#xff0c;影响redis的性能 bgsave: 一般情况下不会阻塞…

C语言模拟实现Liunx操作系统与用户之间的桥梁shell(代码详解)

什么是shell&#xff1f; Shell&#xff08;壳&#xff09;是指命令行界面&#xff08;CLI&#xff09;或脚本语言&#xff0c;它为用户提供了与操作系统交互的方式。它是一个程序&#xff0c;从用户那里接收命令&#xff0c;并通过与操作系统内核交互来执行这些命令。Shell充当…

CTFHub Git泄露

Log 前言 根据题目描述&#xff0c;这个题目需要使用到工具 GitHack 来完成&#xff0c;而 CTFHub 上提供的工具需要在 python2 环境中执行&#xff0c;注意 python3 环境无法使用。 GitHack准备&#xff08;kali Linux&#xff09; 打开虚拟机 sudo su 以管理员的身份运行…

开源更安全? yum源配置/rpm 什么是SSH?

文章目录 1.开放源码有利于系统安全2.yum源配置&#xff0c;这一篇就够了&#xff01;(包括本地&#xff0c;网络&#xff0c;本地共享yum源)3.rpm包是什么4.SSH是什么意思&#xff1f;有什么功能&#xff1f; 1.开放源码有利于系统安全 开放源码有利于系统安全 2.yum源配置…

Java 11及更高版本的Oracle JDK版本

2021 年 9 月 14 日&#xff0c;Oracle 发布了可以长期支持的 JDK17 版本&#xff0c;那么从 JDK11 到 JDK17&#xff0c;到底带来了哪些特性呢&#xff1f;亚毫秒级的 ZGC 效果到底怎么样呢&#xff1f;值得我们升级吗&#xff1f;而且升级过程会遇到哪些问题呢&#xff1f;带…

【Spring boot】RedisTemplate中String、Hash、List设置过期时间

文章目录 前言Redis中String设置时间的方法Redis中Hash和List设置时间的方法Redis中Hash的put、putAll、putIfAbsent区别 前言 时间类型&#xff1a;TimeUnit import java.util.concurrent.TimeUnit;TimeUnit.SECONDS:秒 TimeUnit.MINUTES&#xff1a;分 TimeUnit.HOURS&…

Javaweb之Ajax的详细解析

1.1 Ajax介绍 1.1.1 Ajax概述 我们前端页面中的数据&#xff0c;如下图所示的表格中的学生信息&#xff0c;应该来自于后台&#xff0c;那么我们的后台和前端是互不影响的2个程序&#xff0c;那么我们前端应该如何从后台获取数据呢&#xff1f;因为是2个程序&#xff0c;所以…

“移动机器人课程群实践创新的困境与突围”素材

以下是一篇应用型本科教研论文“移动机器人课程群实践创新的困境与突围”的大纲。您可以根据这个大纲展开您的论文写作&#xff1a; 一、引言 移动机器人技术的发展和应用价值移动机器人课程群在应用型本科教育中的重要性论文目的和研究问题&#xff1a;解析移动机器人课程群实…

利用OpenCV做个熊猫表情包 二

之前写了一篇 利用OpenCV做个熊猫表情包吧_Leen的博客-CSDN博客 回想起来觉得有点太弱了&#xff0c;意犹未尽&#xff0c;每次使用需要自己去手动截取人脸&#xff0c;清除黑边什么的才能使用demo去合成表情&#xff0c;无奈之前由于安装的vs&#xff0c;opencv版本都比较低…

扩散模型实战(十):Stable Diffusion文本条件生成图像大模型

推荐阅读列表&#xff1a; 扩散模型实战&#xff08;一&#xff09;&#xff1a;基本原理介绍 扩散模型实战&#xff08;二&#xff09;&#xff1a;扩散模型的发展 扩散模型实战&#xff08;三&#xff09;&#xff1a;扩散模型的应用 扩散模型实战&#xff08;四&#xff…