zoukankan      html  css  js  c++  java
  • Redis 字典

    字典

    字典,是一种保存键值对(key-value)的抽象数据结构。

    在字典中一个key 是独一无二的,并且和一个值(value)进行关联。

    字典经常作为数据结构内置在高级编程语言中,比如python,但是Redis是用c编写的,c本身并没有内置字典这个数据结构。因此Redis构建了自己的字典。

    字典在Redis应用很广泛,我们知道Redis其实就是一个key-value数据库,所以对Redis数据的增删改查都是利用字典结构。

    Redis字典

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

    哈希表

    Redis 字典所使用的哈希表由 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 数组的哪个索引上面。

    哈希表节点

    哈希表节点使用 dictEntry 结构表示, 每个 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)的问题。

    字典

    Redis 中的字典由 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 。

    整个字典的结构可以用下图表示:

     哈希算法

    要将一个键值对添加到字典里面,需要先将建的哈希值和索引算出来,再将包含键值对的哈希表节点放到哈希表数组的指定索引上面。

    计算哈希值:

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

    计算索引:

    #使用哈希表的sizemask属性(size-1)、ht[x](可以是ht[0]或者ht[1])
    #计算出索引
    index = hash & dict->ht[x].sizemask;

    解决建冲突

    上述哈希算法算出来的索引值可能会相等,这个现象称为键冲突(collision)

    Redis 的哈希表使用链地址法解决键冲突,Redis 哈希表节点用next 属性,将哈希值一样的键值对链接再一起构成链表。

    rehash

    随着操作的不断执行, 哈希表保存的键值对会逐渐地增多或者减少, 为了让哈希表的负载因子维持在一个合理的范围之内

    当哈希表保存的键值对数量太多或者太少时, 程序需要对哈希表的大小进行相应的扩展或者收缩。

    扩展和收缩哈希表的工作可以通过执行 rehash (重新散列)操作来完成, Redis 对字典的哈希表执行 rehash 的步骤如下:

    1. 为字典的 ht[1] 哈希表分配空间, 这个哈希表的空间大小取决于要执行的操作, 以及 ht[0] 当前包含的键值对数量 (也即是 ht[0].used 属性的值):
      • 如果执行的是扩展操作, 那么 ht[1] 的大小为 第一个 >=  ht[0].used 2 的 2^n (2 的 n 次方幂);
      • 如果执行的是收缩操作, 那么 ht[1] 的大小为第一个 >= ht[0].used 的 2^n 。
    1. 将保存在 ht[0] 中的所有键值对 rehash 到 ht[1] 上面: rehash 指的是重新计算键的哈希值和索引值, 然后将键值对放置到 ht[1] 哈希表的指定位置上。
    2. 当 ht[0] 包含的所有键值对都迁移到了 ht[1] 之后 (ht[0] 变为空表), 释放 ht[0] , 将 ht[1] 设置为 ht[0] , 并在 ht[1] 新创建一个空白哈希表, 为下一次 rehash 做准备。

    哈希表的扩展与收缩

    当以下条件中的任意一个被满足时, 程序会自动开始对哈希表执行扩展操作:

    1. 服务器目前没有在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 1 ;
    2. 服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 5 ;

    其中哈希表的负载因子可以通过公式:

    #负载因子 = 哈希表已经保存的节点数 / 哈希表大小
    load_factor = ht[0].used / ht[0].size

    上述两个条件反过来,执行收缩操作。

    参考文献

    redis设计与实现第二版

    https://jiajunhuang.com/tutorial/data_structure/dict.md

  • 相关阅读:
    Longest Substring Without Repeating Characters
    Longest Valid Parentheses
    LInux下编译发生的libc相关错误
    【转载】字符编码笔记:ASCII,Unicode和UTF-8
    Python深入:super函数
    Python基础:常用函数
    25最小操作数问题
    24字符串最短编辑距离
    23最大乘积子串
    22倒排索引简介
  • 原文地址:https://www.cnblogs.com/hulunbao/p/13921434.html
Copyright © 2011-2022 走看看