Redis 字典的实现 | 青训营

73 阅读3分钟

今天学习一下 Redis 里字典的实现,也就是哈希表。一个哈希表里可以有多个哈希表节点,每个哈希表节点保存了字典中的一个键值对。

哈希表

定义如下:

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

} dictht;
  • table 属性是一个数组,数组中的每个元素都是一个指向哈希表节点对应结构体的指针,每个哈希表节点里保存着一个键值对。
  • size 属性记录了哈希表的大小,也就是 table 数组的大小,used 属性记录了目前已有节点(键值对)的数量。
  • sizemark 属性的值总是等于 size - 1,这个属性和某个键的哈希值一起用来计算这个键的索引。

画个图好理解一点,这是一个大小为 4 的空哈希表:

empty_hashtable.png

哈希表节点

定义如下:

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)问题。

画图举例子,下图里 k1k0 索引一样,用 next 指针来连接它俩:

REDIS_Dict_next_pointer.png

看到这里应该明白这里至少有两个坑在等着,一个是 sizemark 为什么要单独设置一个变量,另一个是 used 的值可能比 size 要大。然后应该能大概猜到一些原因,比如说 size 扩容是怎么扩的。

字典

定义如下:

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 属性是一个包含两项的数组,每一项都是一个哈希表,一般情况下,字典只使用 ht[0] 哈希表,ht[1] 哈希表只会在对 ht[0] 进行 rehash 时使用。
  • rehashidx 属性记录当前 rehash 的进度,没有在 rehash 这个值就是 -1。

依然要画个图,这是普通状态下的字典(还没 rehash):

REDIS_Dict_regular_dict.png

这里可以看到第二个哈希表里 sizesizemark 都是 0,这个时候就和前面规定的不一样了,或许单独拉出来一个 sizemark 变量也有这个原因。

然后这里也可以看到为了方便我的图是一步一步往上加东西的,而且到了最后这张图里其实有点问题了,虽然用链表解决了哈希冲突,但是一个索引上挂上太多的键值对查询效率是会受到影响的,我明明还有空的索引,不需要全都链到一个索引下。到这刚刚好,作为一个引子引出下一篇笔记里的 rehash 操作。