微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

redis数据结构(三)字典(dict)

dict为redis服务器中出现的使用最为频繁的复合型数据结构,不但在hash类型和zset中使用了dict结构,整个redis数据库就是一个大的字典表,带有过期时间的key也组成一个大的字典表.

1.dict的内部结构

  1.1 dict的内部结构

typedef struct dict {
    dictType *type;//dictType中定义了很多dict中常用的方法,具体见dictType
    void *privdata;//一些私有数据
    dictht ht[2];//两个dictht数组,正常只有一个在用,只有当rehash时才会使用两个
    long rehashidx; /* dic是否正在进行rehash,当不为-1时,说明正在rehash */
    int iterators; /* 遍历器的个数*/
} dict;

 

ht[2]:里面存放了两个dictht类型的数据,dictht用于保存真正的数据,但是通常情况下只会使用一个dictht保存数据,只有当dict进行rehash的时候才会两个同时使用,关于rehash的详情见rehash小节.

rehashidx:dict是否正在rehash的标识,如果值为-1,标识dict没有正在进行rehash,否则标识dict正在进行rehash,并且rehashidx的值表示dictht中还没有进行rehash的键值对的数量.

dictType和privdata属性是针对不同类型的键值对为创建多态字典而设置的.dictType中定义了一组操作特定类型键值对的函数,privdata属性则保存了需要传给那些类型特定函数的可选参数

 

  1.2 dictType的内部结构

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;

 

 

  1.3 dictht的内部结构

typedef struct dictht {
    dictEntry **table; //dictEntry数组保存真正的键值对
    unsigned long size;//table数组的长度
    unsigned long sizemask;//table数组的长度-1,为了方便计算键值对保存在table中的位置而定义的变量
    unsigned long used;//dictht中已经存在的键值对数量
} dictht;

size:是table数组的长度,这个长度是固定的,并不会因为插入和删除元素而改变

used:保存在table中元素的数量,即当前dictht中有多少元素

 

  1.4 dictEntry的内部结构

typedef struct dictEntry {
    void *key; //键值对中的key
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v; //键值对中的value,对不同的value选取不同的值
    struct dictEntry *next;//指向下一个dictEntry的指针
} dictEntry;

key,v:key,v分别保存了键值对的键和值.

next:记录数组中统一个位置的下一个dictEntry的.

 

2.hash冲突

因为键值对在table中的位置是根据key的hashCode决定的,所以可能会发生hash冲突的情况,Redis的哈希表使用链地址法(separate chaining)来解决键冲突,每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来,这就解决了键冲突的问题。因为dictEntry节点组成的链表没有指向链表表尾的指针,所以为了速度考虑,程序总是将新节点添加到链表的表头位置(头插法,复杂度为O(1)),排在其他已有节点的前面。

 

3.扩容

一般来讲,当dictht中used和size相等的时候就会触发redis的扩容操作,认redis扩容的时候会把容量扩大到原来size的两倍,计算方式为比used*2大的最小的2的n次方.

但是如果dict正在进行bgsave,为了减少内存页的数量,redis会暂缓扩容,但是如果dict中元素的数量继续增加,当到达size的5被的时候,redis就会忽略bgsave进行强制扩容,

dict扩容的时候会提前为ht数的第二个元素提前分配好内存空间,然后逐个对第一个元素中的dictEntry进行rehash转移到第二个元素中,,然后把第一个元素指向第二个元素

 

4.缩容

当dict中的元素不断被删除的时候,dictht中剩余的空间越来越多,当used的数量小于size的10%的时候就会触发redis的缩容操作,缩容的时候是不会考虑redis是否正在进行bgsave操作的.

dict缩容和扩容一样也会先为tht数组的第二个元素分配内存,然后把第一个元素中的数据rehash然后放到第二个元素中,然后把第一个元素指向第二个元素

 

5.rehash

因为redis是单线程执行命令的.而如果dict中元素的数量表多,则进行扩容或则缩容的时候就会耗费的时间比较长,这就导致后续的指令都会返回的较慢,redis为了解决这个问题采用饿了渐进式hash的策略.

即再进行rehash的时候不是一次性的把第一个dictht中的元素一次性全部rehash然后放到第二个dictht中,而是每次等到dict中有增删改查操作的时候每次顺便把dictht一种的部分元素转移到dictht2中,这样就把rehash的时间分配到了不同的指令上,不会造成一个指令延迟时间较长的情况.如果正在进行rehash的dict中没有来自客户端的指令,那么redis会定再定时任务中对数据进行主动的rehash操作

 

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐