Redis数据结构详解(2)-redis中的字典dict

前提知识🧀

字典,又被称为符号表(symbol table)或映射(map),其实简单地可以理解为键值对key-value
比如Java的常见集合类HashMap,就是用来存储键值对的。
字典中的键(key)都是唯一的,由于这个特性,我们可以根据键(key)查找到对应的值(value),又或者进行更新和删除操作。

字典dict的实现

Redis的字典使用了哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,每个节点也保存了对应的键值对。
Redis的字典dict结构如下:


typedef struct dict {
    //类型特定函数
    //是一个指向dictType结构的指针,可以使dict的key和value能够存储任何类型的数据
    dictType *type;
    
    //私有数据
    //私有数据指针,不是讨论的重点,暂忽略
    void *privdata;
    
    //哈希表
    dictht ht[2];
    
    //rehash 索引
    //当 rehash 不在进行时,值为 -1
    int rehashidx;
}

我们重点关注两个属性就可以:

  • ht 属性:

可以看到ht属性是一个 size为2dictht哈希表数组,在平常情况下,字典只用到 ht[0],ht[1] 只会在对 ht[0] 哈希表进行rehash时才会用到。

  • rehashidx 属性:

它记录了rehash目前的进度,如果现在没有进行rehash,那么它的值为-1,可以理解为rehash状态的标识。

下图就是一个普通状态下的字典:


实际的数据在 ht[0] 中存储;ht[1] 起辅助作用,只会在进行rehash时使用,具体作用包括rehash的内容我们会在后面进行详细介绍。

哈希算法定位索引

PS:如果你有HashMap的相关知识,知道如何计算索引值,那么你可以跳过这一部分。

假如我们现在模拟将 hash值从0到5的哈希表节点 放入 size为4的哈希表数组 中,也就是将包含键值对的哈希表节点放在哈希表数组的指定索引上。

对应索引的计算公式:
index = hash & ht[x].sizemask

看不懂没关系,可以简单的理解为hash值对哈希表数组的size值求余;
比如上面 hash值为0的节点,0 % 4 = 0,所以放在索引0的位置上,
hash值为1的节点,1 % 4 = 1,所以放在索引1的位置上,
hash值为5的节点,5 % 4 = 1,也等于1,也会被分配在索引1的位置上,并且因为dictEntry节点组成的链表没有指向链表表尾的指针,所以会将新节点添加在链表的表头位置,排在已有节点的前面。

我们把上面索引相同从而形成链表的情况叫键冲突,而且因为形成了链表!那么就意味着查找等操作的复杂度变高了!
例如你要查找hash=1的节点,你就只能先根据hash值找到索引为1的位置,然后找到hash=5的节点,再通过next指针才能找到最后的结果,也就意味着键冲突发生得越多,查找等操作花费的时间也就更多。

如果解决键冲突?rehash!

其实rehash操作很好理解,可以简单地理解为哈希表数组扩容或收缩操作,即将原数组的内容重新hash放在新的数组里
比如还是上面的数据,我们这次把它们放在 size等于8的哈希表数组 里。
如下图,此时size = 8,hash为5的键值对,
重新计算索引:5 % 8 = 5
,所以这次会放在索引5的位置上。

那么假如我们还要找hash=1的节点,因为没有键冲突,自然也没有链表,我们可以直接通过索引来找到对应节点。
可以看到,因为rehash操作数组扩容的缘故,键冲突的情况少了,进而我们可以更高效地进行查找等操作。

触发rehash操作的条件

首先我们先引入一个参数,叫做负载因子(load_factor),要注意的是:它与HashMap中的负载因子代表的含义不同;在HashMap里负载因子loadFactor作为一个默认值为0.75f的常量存在,而在redis的dict这里,它是一个会动态变化的参数,等于哈希表的 used属性值/size属性值,也就是 实际节点数/哈希表数组大小。假如一个size为4的哈希表有4个哈希节点,那么此时它的负载因子就是1;size为8的哈希表有4个哈希节点,那么此时它的负载因子就是0.5。

满足下面任一条件,程序就会对哈希表进行rehash操作:

  • 扩容操作条件:
    • 服务器目前**没有执行 **BGSAVE 或者 BGREWRITEAOF 命令,负载因子大于等于1。
    • 服务器目前正在执行 BGSAVE 或者 BGREWRITEAOF 命令,负载因子大于等于5。
  • 收缩操作条件:
    • 负载因子小于0.1时。

BGSAVE 和 BGREWRITEAOF 命令可以统一理解为redis的实现持久化的操作。

  • BGSAVE 表示通过fork一个子进程,让其创建RDB文件,父进程继续处理命令请求。
  • BGREWRITEAOF 类似,不过是进行AOF文件重写。

渐进式rehash?rehash的过程是怎么样的?

首先我们知道redis是单线程,并且对性能的要求很高,但是rehash操作假如碰到了数量多的情况,比如需要迁移百万、千万的键值对,庞大的计算量可能会导致服务器在一段时间里挂掉!
为了避免rehash对服务器性能造成影响,redis会分多次、渐进式地进行rehash,即渐进式rehash。
(可以理解粗略地理解为程序有空闲再来进行rehash操作,不影响其他命令的正常执行)

对哈希表进行渐进式rehash的步骤如下:

  1. 首先为 ht[1] 哈希表分配空间,size的大小取决于要执行的操作,以及 ht[0] 当前的节点数量(即ht[0]的used属性值):
    • 扩展操作,ht[1]的size值为第一个大于等于ht[0].used属性值乘以2的 2^n
    • 收缩操作,ht[1]的size值为第一个小于ht[0].used属性值的 2^n

(有没有很熟悉,其实跟Java中的HashMap、ConcurrentHashMap操作类似)

  1. 将哈希表的rehashidx值从-1置为0,表示rehash工作开始。
  2. 节点转移,重新计算键的hash值和索引值,再将节点放置到ht[1]哈希表的对应索引位置上。
  3. 每次rehash工作完成后,程序会将rehashidx值加一。

(这里的每次rehash就指渐进式rehash)

  1. 当ht[0]的所有节点都转移到ht[1]之后,释放ht[0],将ht[1]设置为ht[0],并在ht[1]新创建一个空白的hash表,等待下次rehash再用到。(其实就是数据转移到ht[1]后,再恢复为 ht[0]储存实际数据,ht[1]为空白表的状态)
  2. 最后程序会将rehashidx的值重置为-1,代表rehash操作已结束。

进行渐进式rehash的时候会影响字典的其他操作吗?

因为在进行渐进式rehash的时候,字典会同时用到ht[0]和ht[1]这两个哈希表,所以在这期间,字典的删除(delete)、查找(find)、更新(update)等操作会在两个哈希表进行;而进行添加操作时,会直接插入到ht[1]。

比如查找一个键时,程序会先在ht[0]里面查找,没找到的话再去ht[1]里进行查找。

搜资料的时候还看到好多评论,都对逻辑产生了疑问,还举了例子说有问题,但我仔细看了下,其实都是忽略了删除和更新都会在两个哈希表进行的前提条件。

写在最后的最后

我是苏易困,大家也可以叫我易困,一名Java开发界的小学生,文章可能不是很优质,但一定会很用心。

距离上次更新都过去了好久,一是因为上海的疫情有点严重,一直没静下心来好好整理知识,还有就是发现自己得先很好地消化完知识才能够整理出来,不然其实各方面收获不大;所以后面也会自己先认真消化后再整理分享,不会追求速度,但会认真总结整理。

因为疫情要一直封到4月1号,我们小区还有1例阳性,更不知道到什么时候了,每天早上也要定闹钟抢菜,但还抢不到,因为没有绿叶菜的补给,我感觉已经得口腔溃疡了,还好买到了维C泡腾片,感觉可以稍微缓缓。

疫情掰扯这么多,其实我和大家一样,我有想吃的美食,有想去的地方,更有马上想见到的人,所以最后还是希望疫情能够赶紧好起来~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容