数据结构-字典

 字典结构

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

ht是一个包含两个哈希表的数组,一般情况下字典只使用ht[0],只有在对ht[0]的哈希表进行rehash时才会使用ht[1]。

哈希表数据结构

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

哈希表节点数据结构

typedef struct dictEntry {
    //
    void *key;
    //
    union {
        // 指向具体redisObject
        void *val;
        // 
        uint64_t u64;
        int64_t s64;
    } v;
    // 指向下个哈希表节点,形成链表
    struct dictEntry *next;
} dictEntry;
  • key属性保存键值对中的键
  • v属性保存键值对中的值,其中键值对的值可以是一个指针,指向具体的redisObject,或者是一个uint64_t整数,或者是一个int64_t整数
  • next属性是指向另一个哈希表节点的指针,这个指针可以将多个哈希值相同的键值对连接在一起,以此来解决键冲突(collision)的问题

哈希冲突

Redis解决哈希冲突的方式,就是链式哈希(头插法)。链式哈希也很容易理解,就是指同一个哈希桶中的多个元素用一个链表来保存,它们之间依次用指针连接。

 rehash(渐进式)

负载因子(used/size)来表述哈希冲突的激烈程度,负载因子越大,冲突越激烈。

size表示哈希表的大小,也就是哈希桶的个数。

used表示有多少个 键值对实体(dictEntry)。

扩容的时机:

  • 负载因子≥1,同时,哈希表被允许进行 rehash(实例正在生成 RDB 或者重写 AOF不允许rehash)
  • 负载因子≥5。
上面说到字典存在2个哈希表,一开始,当你刚插入数据时,默认使用ht[0],此时的ht[1]并没有被分配空间。随着数据逐步增多,Redis 开始执行 rehash,这个过程分为三步:
  1. 给 ht[1] 分配更大的空间,例如是当前 ht[0] 大小的两倍。
  2. 把 ht[0] 中的数据重新映射并拷贝到 ht[1] 中。
  3. 释放哈希表 ht[0] 的空间。
到此,我们就可以从哈希表 ht[0] 切换到哈希表 ht[1],用增大的哈希表ht[1]保存更多数据,而原来的哈希表 ht[0] 留作下一次 rehash 扩容备用。
第二步涉及大量的数据拷贝,如果一次性把 ht[0] 中的数据都迁移完,会造成 Redis 线程阻塞,采用了渐进式 rehash
Redis 每处理一个请求时,从哈希表  ht[0] 中的第一个索引位置开始,顺带着将这个索引位置上的所有 entries 拷贝到哈希表  ht[1] 中;等处理下一个请求时,再顺带拷贝 ht[0] 中的下一个索引位置的 entries。如下图所示:
 
0
把rehashidx置为0表示rehash开始,rehashindex记录原hash表的拷贝进度,每次拷贝进行递增,如果拷贝完成,设置为-1。
在rehash过程中,读、新增、更新、删除操作rehashidx++。读操作会先读原hash表,读不到再读新hash表;新增操作只在新hash表执行;更新、删改操作两个表都要执行。
渐进式rehash这样就巧妙地把一次性大量拷贝的开销,分摊到了多次处理请求的过程中,避免了耗时操作,保证了数据的快速访问。
渐进式rehash执行时,除了根据键值对的操作来进行数据迁移,Redis本身还会有一个定时任务在执行rehash,如果没有键值对操作时,这个定时任务会周期性地(例如每100ms一次)搬移一些数据到新的哈希表中,这样可以缩短整个rehash的过程。
 
posted on 2023-03-25 16:47  zhengbiyu  阅读(34)  评论(0编辑  收藏  举报