Skip to content

Redis设计与实现之字典

1939字约6分钟

redis设计与实现

2021-02-28

什么是字典

字典, 又称 符号表(symbol table)关联数组(associative array) 或者 映射(map) , 是一种用于保存 键值对(key-value pair) 的抽象数据结构。

键值对 在字典中, 一个键(key)可以和一个值(value)进行关联, 这些关联的键和值就被称为键值对。

字典的实现

底层实现

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

哈希表节点定义

代码位置 : dict.h

typedef struct dictEntry {
    void *key;  //字典key
    union {     //数据值
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next; //指向下个哈希表节点,形成链表,hash键冲突时才被用到
} dictEntry;
  • 哈希表节点使用dictEntry结构表示, 每个dictEntry结构都保存着一个键值对
  • key属性保存着键值对中的键, 而v属性则保存着键值对中的值, 其中键值对的值可以是 一个指针 , 或者是一个 uint64_t整数 , 又或者是一个 int64_t整数
  • next属性是指向另一个哈希表节点的指针, 这个指针可以将多个哈希值相同的键值对连接在一起, 以此来 解决键冲突(collision)的问题

哈希表定义

代码位置 : dict.h

typedef struct dictht {
    dictEntry **table;  //哈希表数组
    unsigned long size; //哈希表大小
    unsigned long sizemask; //哈希表大小掩码,用于计算索引值 总是等于 size - 1
    unsigned long used; //该哈希表已有节点的数量
} dictht;
  • table 属性是 一个数组, 数组中的每个元素都是一个指向 dictEntry 结构的指针, 每个 dictEntry 结构保存着一个键值对。
  • size 属性记录了 哈希表的大小
  • used 属性记录了 哈希表目前已有节点(键值对)的数量
  • sizemask 属性的值 总是等于 size - 1 , 这个属性和哈希值一起决定一个键应该被放到 table 数组的哪个索引上面。

字典定义

代码位置 : dict.h

typedef struct dict {
    dictType *type;             //类型特定函数
    void *privdata;             //私有数据
    dictht ht[2];               //哈希表
    long rehashidx;             //rehash 索引 当 rehash 不在进行时,值为 -1
    unsigned long iterators;    //目前正在运行的安全迭代器的数量
} dict;
  • type属性和privdata属性是针对不同类型的键值对, 为创建多态字典而设置的
  • ht属性是一个包含两个项的数组, 数组中的每个项都是一个dictht哈希表, 一般情况下, 字典只使用 ht[0] 哈希表, ht[1] 哈希表只会在对 ht[0] 哈希表进行 rehash 时使用
  • 除了 ht[1] 之外, 另一个和 rehash 有关的属性就是rehashidx: 它记录了 rehash 目前的进度, 如果目前没有在进行 rehash , 那么它的值为 -1 。

eg. 空字典

空字典

eg. 普通状态下的字典(没有进行 rehash)

普通状态下的字典

哈希算法

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

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

// 使用字典设置的哈希函数,计算键 key 的哈希值
hash = dict->type->hashFunction(key);

// 使用哈希表的 sizemask 属性和哈希值,计算出索引值
// 根据情况不同, ht[x] 可以是 ht[0] 或者 ht[1]
index = hash & dict->ht[x].sizemask;

eg. 将一个键值对 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 算法来计算键的哈希值。

哈希冲突的解决

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

字典哈希冲突解决方案 参见 : 哈希冲突解决方案

rehash原理及实现

rehash原理实现 参见 : rehash原理

字典的主要API

函数作用时间复杂度
dictCreate创建一个新的字典。O(1)
dictAdd将指定的键值对添加到字典里O(1)
dictAddRaw基础的向hash表中新增一个键值对的方法O(1)
dictGetIterator获取迭代器, 不安全, safe = 0O(1)
dictGetSafeIterator获取迭代器, 安全, safe = 1O(1)
dictReleaseIterator释放迭代器O(1)
dictNext依据迭代器获取下一个键值对O(1)
dictGetRandomKey随机获取一个键值对O(1)
dictGetSomeKeys随机返回指定数量的keyO(1)
dictEmpty清空字典数据但不释放空间O(1)
dictReplace将给定的键值对添加到字典里面, 如果键已经存在于字典,那么用新值取代原有的值。O(1)
dictFind查找指定的keyO(1)
dictFetchValue返回指定的key的值O(1)
dictDelete从字典中删除给定键所对应的键值对。O(1)
dictRelease释放给定字典,以及字典中包含的所有键值对O(N), N为字典中键值对数量
dictEnableResize启用redis空间再次分配O(1)
dictDisableResize禁用redis空间再次分配(比如: rehash期间ht[0]禁用resize)O(1)
dictResize重计算字典大小,将字典容量设置为可以容纳当前字典数据的最小值O(1)
dictRehash对字典进行rehashO(N),N为字典中键值对数量
dictRehashMilliseconds执行rehash操作,指定运行时间,此时间内rehash可能未最终完成O(N),N为字典中键值对数量

参考资料

参考内容

书籍 : 《Redis 设计与实现》

实体书 : 可在京东搜索购买

注意事项

《Redis 设计与实现》 本书讲解基于redis 3.x 版本, 本人整理基于redis 5.0.6,期间跨了两个大版本, 部分功能具体实现已发生变化, 如发现整理内容与 《Redis 设计与实现》 讲解不一致, 可加 QQ : 2215508028 沟通,或在相关章节评论留言,会自动同步至issue

整理内容时参考的redis源码

版本 : 5.0.6

官方git : redis官方GIT (commit :bb78454b0fef0dc5903328d037ac2520108e0044 (5.0.6版本,redis官方的commit))