Redis中字典的实现

一、概念

        字典, 又称符号表(symbol table)、关联数组(associative array)或者映射(map), 是一种用于保存键值对(key-value pair)的抽象数据结构。在字典中, 一个键(key)可以和一个值(value)进行关联(或者说将键映射为值), 这些关联的键和值就被称为键值对。字典中的每个键都是独一无二的, 程序可以在字典中根据键查找与之关联的值, 或者通过键来更新值, 又或者根据键来删除整个键值对, 等等。字典经常作为一种数据结构内置在很多高级编程语言里面, 但 Redis 所使用的 C 语言并没有内置这种数据结构, 因此 Redis 构建了自己的字典实现。字典在 Redis 中的应用相当广泛, 比如 Redis 的数据库就是使用字典来作为底层实现的, 对数据库的增、删、查、改操作也是构建在对字典的操作之上的。举例说明一下,当我们执行下列命令:


在数据库中创建一个键为 "msg" , 值为 "hello" 的键值对时, 这个键值对就是保存在代表数据库的字典里面的。

二、字典的实现

        Redis 的字典使用哈希表作为底层实现, 一个哈希表里面可以有多个哈希表节点, 而每个哈希表节点就保存了字典中的一个键值对。

1. 哈希表

下列是哈希表的结构定义:

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 数组的哪个索引上面。

下图展示了一个大小为 4 的空哈希表 (没有包含任何键值对)。


2. 哈希表节点

哈希表节点使用 dictEntry 结构表示, 每个 dictEntry 结构都保存着一个键值对,请看dictEntry结构:

typedef struct dictEntry {

    // 键

    void *key;

    // 值

    union {

        void *val;

        uint64_tu64;

        int64_ts64;

    } v;

    // 指向下个哈希表节点,形成链表

    struct dictEntry * next;

} dictEntry;

key 属性保存着键值对中的键, 而 v 属性则保存着键值对中的值, 其中键值对的值可以是一个指针, 或者是一个 uint64_t 整数, 又或者是一个 int64_t 整数。

next 属性是指向另一个哈希表节点的指针, 这个指针可以将多个哈希值相同的键值对连接在一起, 以此来解决键冲突(collision)的问题。

下图展示了如何通过 next 指针, 将两个索引值相同的键 k1 和 k0 连接在一起。


三、字典

下列是字典的数据结构:

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 属性是针对不同类型的键值对, 为创建多态字典而设置的:

a. type 属性是一个指向 dictType 结构的指针, 每个 dictType 结构保存了一簇用于操作特定类型键值对的函数, Redis 会为用途不同的字典设置不同的类型特定函数。

b. 而 privdata 属性则保存了需要传给那些类型特定函数的可选参数。

下面是dictType数据结构:

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)的字典:


下图展示了较为完整的Reids字典结构:


三、使用字典

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

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

# 使用字典设置的哈希函数,计算键 key 的哈希值

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

# 使用哈希表的 sizemask 属性和哈希值,计算出索引值

# 根据情况不同, ht[x] 可以是 ht[0] 或者 ht[1]

index=hash&dict->ht[x].sizemask;

举个例子:

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


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

计算键 k0 的哈希值。

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

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

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


当字典被用作数据库的底层实现, 或者哈希键的底层实现时, Redis 使用 MurmurHash2 算法来计算键的哈希值。

MurmurHash 算法最初由 Austin Appleby 于 2008 年发明, 这种算法的优点在于, 即使输入的键是有规律的, 算法仍能给出一个很好的随机分布性, 并且算法的计算速度也非常快。

学习自http://redisbook.com/    

                                                                                                                                                                                        2019-07-21

你可能感兴趣的:(Redis中字典的实现)