当前位置: 首页 > 编程日记 > 正文

Redis源码解析——字典结构

C++语言中有标准的字典库,我们可以通过pair(key,value)的形式存储数据。但是C语言中没有这种的库,于是就需要自己实现。本文讲解的就是Redis源码中的字典库的实现方法。(转载请指明出于breaksoftware的csdn博客)

一般情况下,我们谈到字典,难免要谈到红黑树。但是Redis这套字典库并没有使用该方案去实现,而是使用的是链表,且整个代码行数在1000行以内。所以这块逻辑还是非常好分析的。

我们可以想象下,如果使用普通的链表去实现字典,那么是不是整个数据都在一条链表结构上呢?如果是这么设计,插入和删除操作是非常方便的,但是查找操作可能就非常耗时——需要从前向后一个个遍历对比。很显然不能采用这种方案。于是有一种替代性的方案,就是使用数组去存储,然后通过下标去访问。因为下标操作就是指针的移动,所以查找元素变得非常快。相应的问题便是如何将数据的Key转换成数组下标?

一种比较容易想到的就是使用Key对应的二进制码作为下标。比如我们要保存pair(1,"String1"),则使用其Key的值1对应的二进值1作为下标;再比如pair('A',"stringA"),则使用A字符对应的编码十进制值65作为下标。这种设计方法固然简单,但是有个非常现实的问题——到底要分配多大的数组?上面两个例子还比较简单,我们看个稍微复杂的例子,比如要保存pair("AAAA","StringAAA"),则AAAA的二进制编码对应的十进制是65656565,难道我们要分配那么大的数组?想想也不可能,因为我们往往需要保存的数据比上面这些例子还要复杂很多。如果这么设计,我们的内存可能是否不够分配的,且其使用率也非常低。那怎么解决呢?于是我们就要提到Hash算法了。

我们看下Hash中文定义:Hash,一般翻译做“散列”,也有直接音译为“哈希”的,就是把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间不同的输入可能会散列成相同的输出,所以不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。(源自百度百科)

上面的加粗文字,说明Hash算法可以解决我们之前的问题。但是可想想下,将无限的数据归于有限的空间之内,必然会出现碰撞的问题。对于碰撞问题的解决,也有很多方法。下面将介绍Redis的Dict库中Hash碰撞解决方案,只有弄明白这个方案,才能理解该库的设计思想。

Hash算法碰撞解决方案——拉链法

为了让我们的例子说明比较简单,我杜撰出一种Hash算法和限定使用范围,这样将复杂的问题简单化,从而让我们一窥问题究竟。

我们将Key的使用范围限定于0~4,Hash算法的定义是hash_value = key%5。则我们可以构建一个数组保存key为0~4的数据

但是,当我们认知范围从0~4扩展到0~9,则通过我们上面的Hash算法将产生大量的碰撞。在碰撞无法避免的情况下,只有改变我们的存储结构,但是我们还想使用数组,那怎么办呢?那我们就对Hash的值再Hash,再Hash的方法是hash_value%3。于是有

上面就是拉链解决Hash碰撞的思路。它将碰撞的数据通过链表的形式连接在一块,而通过数组的形式找到该链表的起始元素。这种方案可以解决碰撞问题,但是相应的效率也会有所下降,但是下降的幅度要视链表的长度来决定。因为通过Hash值寻找数组元素是非常快速的,通过数组元素定位到链表的时间消耗也是快速的,因为它们都是寻址运算。所以可以想象真正消耗时间的是链表中数据的查找。

对上面的问题,我们该如何优化呢?我们可以想到的最简单的方法就是适度的扩大数组的长度。比如我们将数组长度扩大到5个,则链表长度将缩小,其查找效率会明显提升:

现在再考虑一个情况,如果我们随机的去掉大部分元素,仅仅留下元素1和4,那么我们上面的结构变为

上图可以看出该结构显得非常松散,也浪费内存。这个时候我们可以重新定义再Hash算法,比如让hash_value%2,则

上面这两种再Hash是针对链表过长或者空间过于零散的场景设计的。如果把这些看明白了,那么Redis的Dict的实现思想也就大致清楚了。

Dict的基础结构

Redis的Dict中最基础的元素结构是

typedef struct dictEntry {void *key;union {void *val;uint64_t u64;int64_t s64;double d;} v;struct dictEntry *next;
} dictEntry;

该结构自身内部有一个指向下一个该结构对象的指针,可以见得这是链表元素的结构。key字段是一个无类型指针,我们可以让该key指向任意类型,从而支撑Dict的key是任意类型的能力。联合体v则是key对应的value,它可以是uint_64_t、int64_t、double和void*型,void*型是无类型指针,它使得Dict可以承载任意类型的value值。

一般一个dict只能承载一种类型的(key,value)对,而key和value的类型则可以是自定义的。这种开放的能力需要优良架构设计的支持。因为对类型没有约束,而框架自身无法得知这些类型的一些信息。但是流程上却需要得知一些必要信息,比如key字段如何进行Hash?key和value如何复制和析构?key字段如何进行等值对比?这些框架无法提前预知的能力只能让数据类型提供者去提供。Redis的Dict中通过下面的结构来指定这些信息

typedef struct dictType {unsigned int (*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);
} dictType;

承载dictEntry的是下面这个结构,它就是我们之前讨论Hash碰撞时拉链算法的体现

typedef struct dictht {dictEntry **table;unsigned long size;unsigned long sizemask;unsigned long used;
} dictht;

table是一个保存dicEntry指针的数组;size是数组的长度;sizemask是用于进行hash再归类的桶,它的值是size-1;used是元素个数,我们通过一个图来解释


        
        似乎我们可以用这个结构已经可以实现字典了。但是Redis在这个基础上做了一些优化,我们看下它定义的字典结构:

typedef struct dict {dictType *type;void *privdata;dictht ht[2];long rehashidx; /* rehashing not in progress if rehashidx == -1 */int iterators; /* number of iterators currently running */
} dict;

type字段定义了字典处理key和value的相应方法,通过这个字段该框架开放了处理自定义类型数据的能力。privdata是私有数据,但是一般都传NULL。ht是个数组,它有两个元素,都是可以用于存储数据的。这儿有个问题,就是为什么要两个dictht对象?我们在讲解拉链法时抛出过两个问题,即数据链过长时或数据松散时如何进行优化?我们采用的是扩大数组个数和缩小数组个数,即再Hash(rehash)的方案。其实Redis就是这样的方案去做的,只是它处理的比较精细。ht[0]作为主要的数据存储区域,ht[1]则是用于rehash操作的结果,但是一旦rehash完成,就将ht[1]中的数据赋值给ht[0]。那么为什么不让ht[1]作为rehash操作中一个栈上临时变量,而要保存在字典结构中呢?这是因为如果我们将rehash操作当成一个原子操作在一个函数中去做,此时如果有数据插入或者删除,则需要等到rehash操作完成才可以执行。而当数据量很大时,rehash操作会比较慢,这样势必影响其他操作的速度。于是Redis在设计时,采用的是一种渐进式的rehash方法。因为渐进式非原子性,所以中间状态也要保存在字典结构中以保证数据完整性。这就是为什么有两个dictht的原因。rehashidx是rehash操作时ht[0]中正在被rehash操作的数组下标,如果它是-1则代表没有在进行rehash操作。iterators是迭代器,我们会在之后讲解。

相关文章:

十步,教你把Python运行速度提升 30%

作者 | Martin Heinz译者 | 陆离编辑 | Jane出品 | AI科技大本营(ID:rgznai100)【导读】一直以来,诟病 Python语言的人经常说,他们不想使用的一个原因是 Python 的速度太慢了。不管使用哪一种编程语言,程序…

字符串转换成NSDate类型的 为nil解决方法

方法一 通过下列函数来解决 但是得到的日期会改变 修改方法fix- (NSDate *)timeForString:(NSString *)string {NSMutableString *timeString [[NSMutableString alloc] initWithString:string]; [timeString setString:[timeString stringByReplacingOccurrence…

Redis源码解析——字典基本操作

有了《Redis源码解析——字典结构》的基础,我们便可以对dict的实现进行展开分析。(转载请指明出于breaksoftware的csdn博客) 创建字典 一般字典创建时,都是没有数据的,但是字典类型需要确定,所以我们看到R…

[转]控制 Cookie 的作用范围

默认时,网站的所有 Cookies 都一起被存储在客户端,并且所有 Cookies 连同网站的任何请求一起被发送到服务器。换句话说,网站中的每个页面都能够为网站获取所有的 Cookies。但是,你能够通过两个方式来设置 Cookies 的作用范围&…

强化学习70年演进:从精确动态规划到基于模型

作者 | Nathan Lambert译者 | 泓礼编辑 | 夕颜出品 | AI科技大本营(ID: rgznai100)【导读】这是一份帮你了解强化学习算法本质的资源,无需浏览大量文档,没有一条公式,非常适合学生和研究人员阅读。作为强化学习研究人员…

Android ActionBar相关

1.Android 5.0 删除ActionBar下面的阴影 于Android 5.0假设你发现的ActionBar下面出现了阴影&#xff0c;例如&#xff0c;下面的设置&#xff0c;以消除阴影&#xff1a; getActionBar().setElevation(0); Android 5.0之前能够用以下代码消除阴影&#xff1a; <item name&q…

Redis源码解析——字典遍历

之前两篇博文讲解了字典库的基础&#xff0c;本文将讲解其遍历操作。之所以将遍历操作独立成一文来讲&#xff0c;是因为其中的内容和之前的基本操作还是有区别的。特别是高级遍历一节介绍的内容&#xff0c;充满了精妙设计的算法智慧。&#xff08;转载请指明出于breaksoftwar…

开发者在行动!中国防疫开源项目登上GitHub TOP榜

整理 | 唐小引出品 | CSDN&#xff08;ID&#xff1a;CSDNnews&#xff09;【导读】用开发者们的方式支援这场没有硝烟的战争&#xff01;截止北京时间 1 月 28 日下午 15:47&#xff0c;全国确诊新型冠状病毒的数字已经到达了 4586 例&#xff0c;疑似高达 6973 例&#xff0c…

像童话一样学习OSPF原理

可以把整个网络&#xff08;一个自治系统AS&#xff09;看成一个王国&#xff0c;这个王国可以分成几个 区(area)&#xff0c;现在我们来看看区域内的某一个人(你所在的机器root)是怎样得到一张 世界地图(routing table)的。   首先&#xff0c;你得跟你周围的人&#xff08;…

队列——PowerShell版

继续读啊哈磊《啊哈&#xff01;算法》感悟系列——队列 地铁售票处排队&#xff0c;先来的人先到队首先买完先走&#xff0c;后来的人排在队尾等候后买完后走。 想买票&#xff0c;必须排在队尾&#xff1b;买完票&#xff0c;只能从队首离开。 这种先进先出&#xff08;First…

Redis源码解析——双向链表

相对于之前介绍的字典和SDS字符串库&#xff0c;Redis的双向链表库则是非常标准的、教科书般简单的库。但是作为Redis源码的一部分&#xff0c;我决定还是要讲一讲的。&#xff08;转载请指明出于breaksoftware的csdn博客&#xff09; 基本结构 首先我们看链表元素的结构。因为…

12月第三周安全要闻回顾:浏览器安全不容忽视,SSL弱点影响网站安全

本周&#xff08;081215至081221&#xff09;安全方面的新闻众多&#xff0c;主要集中在***与威胁趋势方面。浏览器安全方向波澜起伏&#xff0c;微软推出了针对上周公开的IE7新漏洞的紧急安全补丁&#xff0c;但目前互联网上针对该漏洞的大规模***仍在继续&#xff0c;******的…

GPT2文本生成有问题?这里有些潜在解决思路

作者 | Leo Gao译者 | 凯隐编辑 | 夕颜出品 | AI科技大本营&#xff08;ID: rgznai100&#xff09; 【导读】在过去的一年中&#xff0c;人们对文本生成模型的兴趣重新燃起&#xff0c;这在很大程度上要归功于GPT2&#xff0c;它主要展示了使用更大模型、更大数据和更大计算量的…

HTML5学习之二:HTML5中的表单2

&#xff08;本内容部分节选自《HTML 5从入门到精通》) 对表单的验证 ———————————————————————————————————————————————————————— •1、required属性 required属性主要目的是确保表单控件中的值已填写。在提交时&…

Redis源码解析——有序整数集

有序整数集是Redis源码中一个以大尾&#xff08;big endian&#xff09;形式存储&#xff0c;由小到大排列且无重复的整型集合。它存储的类型包括16位、32位和64位的整型数。在介绍这个库的实现前&#xff0c;我们还需要先熟悉下大小尾内存存储机制。&#xff08;转载请指明出于…

GitHub标星1.2w+,Chrome最天秀的插件都在这里

作者 | Rocky0429来源 | Python空间&#xff08;ID: Devtogether&#xff09;大家好&#xff0c;我是 Rocky0429&#xff0c;一个沉迷 Chrome 不能自拔的蒟蒻...作为一个在远古时代用过什么 IE、360、猎豹等浏览器的资深器哥&#xff0c;当我第一次了解 Chrome 的时候&#xff…

基础篇 第四节 项目进度计划编辑 之 任务关联性设定

1.任务关联性的类型 ◎完成 —— 开始 FS ◎开始 —— 开始 SS ◎开始 —— 完成 SF 完成 —— 完成 FF 2.设定任务关联性 三种方法&#xff1a; ◎在条形图中直接拖拽 ◎在“前置任务”列中编辑 ◎在“任务信息”中的“前置任务”选项卡中编辑 3.设定“延隔时间” 延隔时间小于…

开坑,写点Polymer 1.0 教程第3篇——组件注册与创建

之前一篇算是带大家大致领略了一下Polymer的风采。这篇我们稍微深入一丢丢&#xff0c;讲下组件的注册和创建。 创建自定义组件的几种方式 这里我们使用Polymer函数注册了一个自定义组件"my-element" // register an element Polymer({is: my-element,created: funct…

Redis源码解析——Zipmap

本文介绍的是Redis中Zipmap的原理和实现。&#xff08;转载请指明出于breaksoftware的csdn博客&#xff09; 基础结构 Zipmap是为了实现保存Pair(String,String)数据的结构&#xff0c;该结构包含一个头信息、一系列字符串对&#xff08;之后把一个“字符串对”称为一个“元素…

IIS7入门之旅:(3)CGI application和FastCGI application的区别

前言&#xff1a; 一如既往地&#xff0c;IIS支持通过提供pluggable module来提供对第3方script的支持,例如php等。在IIS7中&#xff0c;对于CGI的支持有了一个新的变化&#xff0c;就是同时提供了2种CGI支持模块&#xff0c;分别为&#xff1a;CGIModule (cgi.dll)和FastCGIMo…

抗击疫情!阿里云为加速新药疫苗研发提供免费AI算力

1月29日&#xff0c;阿里云正式宣布&#xff1a;疫情期间&#xff0c;向全球公共科研机构免费开放一切AI算力&#xff0c;以加速本次新型肺炎新药和疫苗研发。 目前&#xff0c;中国疾控中心已成功分离病毒&#xff0c;疫苗研发和药物筛选仍在争分夺秒地进行。新药和疫苗研发期…

SpriteBuilder中如何平均拉伸精灵帧动画的距离

首先要在Timeline中选中所有的精灵帧&#xff0c;可以通过如下2种的任意一种办法达成&#xff1a; 1按下Shift键的同时鼠标单击它们 2鼠标在Timeline空白区拖拽直到拉出的矩形包围住所有精灵帧方块后放开鼠标。 你可以通过查看Timeline中精灵帧方块上是否有阴影来辨识是否选中…

C++拾趣——类构造函数的隐式转换

之前看过一些批判C的文章&#xff0c;大致意思是它包含了太多的“奇技淫巧”&#xff0c;并不是一门好的语言。我对这个“奇技淫巧”的描述颇感兴趣&#xff0c;因为按照批判者的说法&#xff0c;C的一些特性恰巧可以让一些炫耀技术的同学有了炫耀的资本——毕竟路人皆知的东西…

数十名工程师作战5天,阿里达摩院连夜研发智能疫情机器人

作者 | Just出品 | AI科技大本营&#xff08;ID:rgznai100&#xff09;新型肺炎疫情防控战在各大互联网科技公司拉响&#xff0c;阿里、百度等公司陆续对外提供相应技术和产品。当前&#xff0c;疫情当前防控一线人员紧缺&#xff0c;多地政务热线迎来大波问询市民&#xff0c;…

路由器互联端口处于不同网段的路由方法和原理

如下图&#xff1a;两台cisco路由器相连接的两个端口不在同一个网络&#xff0c;如何实现两台路由器的互联&#xff1f;初看似乎绝对不可能&#xff0c;但是这是可行的&#xff0c;而且我已经把这个变成了现实。这里讲述配置的方法&#xff0c;以及解释原理。这个就要讲到路由原…

上网行为管理产品选型简单考量

信息化浪潮汹涌向前&#xff0c;人们的生活、工作、学习越来越离不开IT&#xff0c;离不开网络。 对于很多组织来讲&#xff0c;网络就意味着效率、甚至生产力&#xff0c;协同办公、决策、科研、资金划拨等等都对网络有了前所未有的依赖。网络应用日益复杂、多变、动态特征发展…

码农技术炒股之路——配置管理器、日志管理器

配置管理器和日志管理器是项目中最为独立的模块。我们可以很方便将其剥离出来供其他Python工程使用。文件的重点将是介绍Python单例和logging模块的使用。&#xff08;转载请指明出于breaksoftware的csdn博客&#xff09; 配置管理器 在《码农技术炒股之路——架构和设计》中我…

“数学不好,干啥都不行!”资深程序员:别再瞎努力了!

之前很多程序员读者向我们反馈&#xff1a;1&#xff09;做算法优化时&#xff0c;只能现搬书里的算法&#xff0c;遇到不一样的问题&#xff0c;就不会了。2&#xff09;面试一旦涉及到算法和数据结构&#xff0c;如果数学不行&#xff0c;面试基本就凉凉了。3&#xff09;算法…

受限列表 队列与栈

2019独角兽企业重金招聘Python工程师标准>>> 队列与栈为受限列表&#xff0c;队列为先入先出型列表&#xff0c;而栈为先入后出型列表&#xff0c;有关列表的实现可以查看 http://my.oschina.net/u/2011113/blog/514713 。 结构图为 Queue实现了IQueue接口&#xff…