Redis学习笔记-字典

  1. 引言
    redis中的字典应该是我们最为熟悉的一个结构,因为redis就可以看做是一个大的内存字典。在很多变成语言中都有字典的实现,如java中的HashMap,但是redis是使用c语言实现的,c语言中没有提供字典的实现,因此redis编写了自己的字典实现。

  2. 哈希表
    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 结构保存着一个键值对

    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 下一个哈希表节点(dictEntry)的指针,
    多个hash值相同的节点会通过next链接在一起形成链表,也就是常说的链地址法解决hash冲突
    

    size 属性记录了哈希表的大小, 也即是 table 数组的大小, 而 used 属性则记录了哈希表目前已有节点(键值对)的数量
    sizemask 属性的值总是等于 size - 1 , 这个属性和哈希值一起决定一个键应该被放到 table 数组的哪个索引上面
    哈希表的图形结构如下所示:(熟悉java的hashmap的同学看到以下结构应该不会感到陌生,这个结构和hashmap极为相相似)
    哈希表的结构

  3. 字典
    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 。
    字典的结构(没有进行rehash的状态下)如下图所示:字典的结构

  4. 渐进式rehash
    ht 是一个包含两个项的数组,即有两个哈希表,其实但一个数组也可以实现字典的,或者说哈希表就可以当做一个字典的实现,为什么redis却使用了两个哈希表。
    这两个哈希表主要是在字典扩容或者缩容的时候使用的,也就是rehash的时候使用。
    我们如果熟悉java的HashMap,会知道HashMap在不断插入数据的时候,当HashMap的中键值的数量超过负载因子的时候,HashMap则会进行扩容操作,此时会出现rehash。HashMap的rehash操作是一次性完成的(我使用的jdk是1.8版本的)。
    redis的rehash则不是一次性完成的,是渐进式的,我觉得原因应该是,redis中保存了大量的key,redis中保存百万级或者说千万级的key太常见了,如果redis的rehash的时候一次性完成,这么多的key进行rehash则耗时十分严重,会导致redis阻塞。
    rehash的过程

    1. 为 ht[1] 分配空间, 让字典同时持有 ht[0] 和 ht[1] 两个哈希表。
    2. 在字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始。
    3. 在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增一。
    4. 随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。

    rehash的过程图如下(rehashidx是在不断的变化):
    1.rehash准备开始(为ht[1]分配内存)rehash准备开始(为ht[1]分配内存
    2.将h[0]中的第一个槽的k2 rehash到ht[1]中,此时rehashidx为0rehash进行中
    3.将h[0]中的第二个槽的k0 rehash到ht[1]中,此时rehashidx为1rehash进行中
    4.将h[0]中的第三个槽的k3 rehash到ht[1]中,此时rehashidx为2rehash进行中
    5.将h[0]中的第三个槽的k1 rehash到ht[1]中,此时rehashidx为3rehash进行中
    6.释放ht[0] ,将 ht[1]设置为ht[0] ,在 ht[1] 新创建一个空白哈希表, 为下一次 rehash 做准备,此时rehashidx为-1,表示没有在进行rehash操作在这里插入图片描述
    补充:

    1. 扩容或缩容每次为h[1]分配多大的空间?
      扩容, ht[1] 的大小为第一个大于等于 ht[0].used * 2 的 2^n (2 的 n 次方幂)
      如:used=10,10*2=20,那么h[1]的大小为2^5 = 32
      缩容: ht[1] 的大小为第一个大于等于 ht[0].used 的 2^n

    2. 什么时候进行扩容或者缩容?
      扩容
      服务器目前没有在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 1
      服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 5

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

      缩容
      当哈希表的负载因子小于 0.1 时程序自动开始对哈希表执行收缩操作

    3. 渐进式rehash进行中,k0原本在h[0]中,rehash到了h[1],此时访问k0,是否会访问不到呢?
      可以正常访问k0,当程序在h[0]中不能找到k0时,会访问h[1],从而获取到k0的值

    4. 渐进式rehash只在查询、更新、删除key的时候进行吗,如果某一个key一直没被访问,rehash岂不是久久不能完成?
      redis的渐进式rehash有两种模式:
      lazy模式,就是文中提到的查询、更新、删除的时候进行。
      active模式,redis有time event,如果redis正在进行渐进式rehash操作,则会花费1毫秒的时间,帮助一起进行渐进式rehash操作

    5. 渐进式rehash是否会带来什么问题?

      1. 如果redis的内存已经十分紧张,如果此时进行渐进式rehash,有可能导致部分key会被淘汰掉,因为rehash的时候需要为h[1]分配内存,如果此时redis内存不足,则会淘汰一部分key,使满足rehash的进行
      2. scan命令可能会返回重复key,或者返回的key不足。返回重复key的情况,比如k0在h[0]的1号槽中,此时被scan返回了,于此同时k0被rehash到了h[1]的8号槽中,当scan命令scan了8号槽的时候k0会被再次返回。
      3. rehash时会导致内存增长到某一个稳定值(因为给h[1]分配了内存),然后一直处于该值一段时间后,内存又下降了(rehash完成h[0]释放)。
  5. 总结
    一直以为redis的字典结构如同java的hashmap一样,但是实际并非如此,redis的字典很好的利用了两个哈希表实现了渐进式rehash,这种分而治之的方式, 将 rehash 键值对所需的计算工作均滩到对字典的每个添加、删除、查找和更新操作上, 从而避免了集中式 rehash 而带来的庞大计算量。

参考资料:

  1. 《redis设计与实现》
  2. 美团针对Redis Rehash机制的探索和实践
  3. 记一次Redis内存诡异增长
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值