想知道Redis字典结构的底层是如何实现的嘛

文章很长文末有福利

一、前言#

上节《闲扯Redis六》Redis五种数据类型之Hash型 中说到 Hash(哈希对象)的底层实现有:

1、ziplist 编码的哈希对象使用压缩列表作为底层实现

2、hashtable 编码的哈希对象使用字典作为底层实现

原文解析

那么第二种方式中的字典究竟是怎样的一种结构呢?


字典, 又称符号表(symbol table)、关联数组(associative array)或者映射(map), 是一种用于保存键值对(key-value pair)的抽象数据结构。在字典中, 一个键(key)可以和一个值(value)进行关联(或者说将键映射为值), 这些关联的键和值就被称为键值对。

字典中的每个键都是独一无二的, 程序可以在字典中根据键查找与之关联的值, 或者通过键来更新值, 又或者根据键来删除整个键值对, 等等。

二、实现分析#

Redis 的字典采用哈希表作为底层实现, 一个哈希表里面可以有多个哈希表节点, 而每个哈希表节点就保存了字典中的一个键值对。所以咱们依次来分析一下哈希表、哈希表节点、以及字典的结构。

1.哈希表结构#

哈希表结构定义 (dict.h/dictht):

Copy

typedef struct dictht {// 哈希表数组dictEntry **table;// 哈希表大小unsignedlongsize;// 哈希表大小掩码,用于计算索引值// 总是等于 size - 1unsignedlongsizemask;// 该哈希表已有节点的数量unsignedlongused;} dictht;

描述

Copy

table属性是一个数组, 数组中的每个元素都是一个指向 dict.h/dictEntry 结构的指针, 每个 dictEntry 结构保存着一个键值对。size 属性记录了哈希表的大小, 也即是table数组的大小, 而 used 属性则记录了哈希表目前已有节点(键值对)的数量。sizemask 属性的值总是等于 size -1, 这个属性和哈希值一起决定一个键应该被放到table数组的哪个索引上面。

结构图解:一个空的哈希表

2.哈希表节点#

一个哈希表里面可以有多个哈希表节点,那么每个哈希表节点的结构以及多个哈希表节点之间的存储关系是怎么样的呢?

哈希表节点结构定义 (dictEntry):

Copy

typedef struct dictEntry {// 键void*key;// 值union {void*val;        uint64_t u64;        int64_t s64;    } v;// 指向下个哈希表节点,形成链表struct dictEntry *next;} dictEntry;

描述

Copy

key 属性保存着键值对中的键, 而 v 属性则保存着键值对中的值, 其中键值对的值可以是一个指针, 或者是一个uint64_t整数, 又或者是一个int64_t整数。next 属性是指向另一个哈希表节点的指针, 这个指针可以将多个哈希值相同的键值对连接在一次,

以此来解决键冲突(collision)的问题。

结构图解:多个哈希值相同的键值对存储结构,解决键冲突

3.字典结构实现#

字典结构定义 (dict.h/dict):

Copy

typedef struct dict {// 类型特定函数dictType *type;// 私有数据void*privdata;// 哈希表dictht ht[2];// rehash 索引// 当 rehash 不在进行时,值为 -1intrehashidx;/* rehashing not in progress if rehashidx == -1 */} dict;

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

Copy

type属性是一个指向 dictType 结构的指针, 每个 dictType 结构保存了一簇用于操作特定类型键值对的函数,Redis会为用途不同的字典设置不同的类型特定函数。privdata属性则保存了需要传给那些类型特定函数的可选参数。

Copy

typedef struct dictType {// 计算哈希值的函数unsignedint(*hashFunction)(constvoid*key);// 复制键的函数void*(*keyDup)(void*privdata,constvoid*key);// 复制值的函数void*(*valDup)(void*privdata,constvoid*obj);// 对比键的函数int(*keyCompare)(void*privdata,constvoid*key1,constvoid*key2);// 销毁键的函数void(*keyDestructor)(void*privdata,void*key);// 销毁值的函数void(*valDestructor)(void*privdata,void*obj);} dictType;

ht 属性是一个包含两个项的数组, 数组中的每个项都是一个 dictht 哈希表, 一般情况下, 字典只使用 ht[0] 哈希表, ht[1] 哈希表只会在对 ht[0] 哈希表进行 rehash 时使用。

除了 ht[1] 之外, 另一个和 rehash 有关的属性就是 rehashidx : 它记录了 rehash 目前的进度, 如果目前没有在进行 rehash , 那么它的值为 -1 。

结构图解:普通状态下(没有进行 rehash)的字典

三、哈希表分析#

1.哈希算法#

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

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

Copy

# 使用字典设置的哈希函数,计算键 key 的哈希值hash = dict->type->hashFunction(key);# 使用哈希表的 sizemask 属性和哈希值,计算出索引值# 根据情况不同, ht[x] 可以是 ht[0] 或者 ht[1]index = hash & dict->ht[x].sizemask;

如图 4-4:

举个例子, 对于图 4-4 所示的字典来说, 如果我们要将一个键值对 k0 和 v0 添加到字典里面, 那么程序会先使用语句:

Copy

hash = dict->type->hashFunction(k0);

计算键 k0 的哈希值。

假设计算得出的哈希值为 8 , 那么程序会继续使用语句:

Copy

index = hash & dict->ht[0].sizemask =8&3=0;

计算出键 k0 的索引值 0 , 这表示包含键值对 k0 和 v0 的节点应该被放置到哈希表数组的索引 0 位置上,

结构图解:图 4-5

2.键冲突解决#

当有两个或以上数量的键被分配到了哈希表数组的同一个索引上面时, 我们称这些键发生了冲突(collision)。

Redis 的哈希表使用链地址法(separate chaining)来解决键冲突: 每个哈希表节点都有一个 next 指针, 多个哈希表节点可以用 next 指针构成一个单向链表, 被分配到同一个索引上的多个节点可以用这个单向链表连接起来, 这就解决了键冲突的问题。

举个例子, 假设程序要将键值对 k2 和 v2 添加到图 4-6 所示的哈希表里面, 并且计算得出 k2 的索引值为 2 , 那么键 k1 和 k2 将产生冲突, 而解决冲突的办法就是使用 next 指针将键 k2 和 k1 所在的节点连接起来。

结构图解:图 4-7


因为 dictEntry 节点组成的链表没有指向链表表尾的指针, 所以为了速度考虑, 程序总是将新节点添加到链表的表头位置(复杂度为 O(1)), 排在其他已有节点的前面。

四、要点总结#

1.字典 ht 属性是包含两个哈希表项的数组,一般情况下, 字典只使用 ht[0], ht[1] 哈希表只会在对 ht[0] 哈希表进行 rehash (下节分析) 时使用

2.哈希表使用链地址法(separate chaining)来解决键冲突

3.键值对添加到字典的过程, 先根据键值对的键计算出哈希值和索引值, 然后再根据索引值, 将包含新键值对的哈希表节点放到哈希表数组的指定索引上面

《 Java学习、面试;文档、视频资源免费获取》

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