一、前言

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

1、ziplist 编码的哈希对象应用压缩列表作为底层实现
2、hashtable 编码的哈希对象应用字典作为底层实现

<p align="right">原文解析</p>

那么第二种形式中的字典到底是怎么的一种构造呢?

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

字典中的每个键都是举世无双的, 程序能够在字典中依据键查找与之关联的值, 或者通过键来更新值, 又或者依据键来删除整个键值对, 等等。

二、实现剖析

Redis 的字典采纳哈希表作为底层实现, 一个哈希表外面能够有多个哈希表节点, 而每个哈希表节点就保留了字典中的一个键值对。所以咱们顺次来剖析一下哈希表、哈希表节点、以及字典的构造。

1.哈希表构造

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

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

形容

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

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

2.哈希表节点

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

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

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

形容

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

以此来解决键抵触(collision)的问题。

构造图解:多个哈希值雷同的键值对存储构造,解决键抵触

3.字典构造实现

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

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

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

type 属性是一个指向 dictType 构造的指针, 每个 dictType 构造保留了一簇用于操作特定类型键值对的函数,Redis 会为用处不同的字典设置不同的类型特定函数。privdata 属性则保留了须要传给那些类型特定函数的可选参数。
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;

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

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

构造图解:一般状态下(没有进行 rehash)的字典

三、哈希表剖析

1.哈希算法

当要将一个新的键值对增加到字典外面时, 程序须要先依据键值对的键计算出哈希值和索引值, 而后再依据索引值, 将蕴含新键值对的哈希表节点放到哈希表数组的指定索引下面。

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

# 应用字典设置的哈希函数,计算键 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 增加到字典外面, 那么程序会先应用语句:

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

计算键 k0 的哈希值。

假如计算得出的哈希值为 8 , 那么程序会持续应用语句:

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.键值对增加到字典的过程, 先依据键值对的键计算出哈希值和索引值, 而后再依据索引值, 将蕴含新键值对的哈希表节点放到哈希表数组的指定索引下面