Redis数据结构-dict

Redis底层是采用哈希加链表实现的,了解Redis内部数据结构,对学习Redis有很大的帮助。

先来看几个关键词

dictEntry:表示一个key-value节点。

dictht:表示一个dict哈希表,里面有一个数组,数组里面的每个元素都是指向dictEntry的指针。

dict:表示redis的字典结构,里面有2个哈希表,一个用来存储键值对,一个用来rehash。

dictType:保存了一些用于操作特定类型键值对的函数

附上源码:

dictEntry

typedef struct dictEntry {
    void *key;                //key void*表示任意类型指针

    union {                   //联合体中对于数字类型提供了专门的类型优化
       void      *val;
       uint64_t  u64;
       int64_t   s64;
    } v;

    struct dictEntry *next;   //next指针

} dictEntry;

dictht

typedef struct dictht {
    dictEntry **table;        //数组指针,每个元素都是一个指向dictEntry的指针

    unsigned long size;       //表示这个dictht已经分配空间的大小,大小总是2^n

    unsigned long sizemask;   //sizemask = size - 1; 是用来求hash值的掩码,为2^n-1

    unsigned long used;       //目前已有的元素数量
} dictht;

dict

typedef struct dict {
    dictType *type;     //type中定义了对于Hash表的操作函数,比如Hash函数,key比较函数等

    void *privdata;      //privdata是可以传递给dict的私有数据         

    dictht ht[2];       //每一个dict都包含两个dictht,一个用于rehash

    int rehashidx;      //表示此时是否在进行rehash操作

    int iterators;      //迭代器
} dict;

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;

附上对应关系图,方便理解:

备注:下图为没有处于rehash阶段,即ht[1]是空的

 

ReHash和渐进式ReHash

当哈希表的冲突率过高时链表会很长,这时查询效率就会变低,所以有必要进行哈希表扩展,而如果哈希表存放的键值对很少的时候把size设的很大,又会浪费内存,这时就有必要进行哈希表收缩。这里扩展和收缩的过程,其实就是rehash的过程

rehash:

将ht[0]上的Key重新按照Hash函数计算Hash值,存到ht[1]的过程

步骤如下:

1. 为dict的哈希表ht[1]分配空间,分配的空间大小取决于操作类型和当前键值对数量ht[0].used

    (1)如果是扩展操作,ht[1]的大小为第一个大于等于ht[0].used*2*2^{n}的整数

    (2)如果是收缩操作,ht[1]的大小为第一个大于等于ht[0].used*2^{n}的整数

2. 重新计算ht[0]中所有键的哈希值和索引值,将相应的键值对迁移到ht[1]的指定位置中去,需要注意的是,这个过程是渐进式完成的,不然如果字典很大的话全部迁移完需要一定的时间,这段时间内Redis服务器就不可用了哟

3. 当ht[0]的所有键值对都迁移到ht[1]中去后(此时ht[0]会变成空表),把ht[1]设置为ht[0],并重新在ht[1]上新建一个空表,为下次rehash做准备

 

渐进式rehash:

从性能的角度出发考虑,一次性rehash操作,在键太多的情况下,会造成长时间的阻塞,因此提出渐进式rehash,即多次进行rehash操作,一步步最终将所有的键完成rehash。

步骤如下:

1. 为ht[1]分配空间,此时字典同时持有ht[0]和ht[1]

2. 将rehashidx设为0,表示rehash正式开始

3. 在rehash期间,每次对字典执行任意操作时,程序除了执行对应操作之外,还会顺带将ht[0]在rehashidx索引上的所有键值对rehash到ht[1],操作完后将rehashidx的值加一

4. 在rehash期间,对字典进行ht[0].size次操作之后,rehashidx的值会增加到ht[0].size,此时ht[0]的所有键值对都已经迁移到ht[1]了,程序会将rehashidx重新置为-1,以此表示rehash完成

这里需要注意的是,在rehash的过程中,ht[0]和ht[1]可能同时存在键值对,因此在执行查询操作的时候两个哈希表都得查,而如果是执行插入键值对操作,则直接在ht[1]上操作就行。

 

最后说下Redis在什么条件下会对哈希表进行扩展或收缩:

1. 服务器当前没有在执行BGSAVE或BGREWRITEAOF命令且哈希表的负载因子大于等于1时进行扩展操作

2. 服务器正在执行BGSAVE或BGREWRITEAOF命令且哈希表的负载因子大于等于5时进行扩展操作

(这里负载因子的计算公式为:负载因子=哈希表当前保存节点数/哈希表大小,而之所以在服务器进行BGSAVE或BGREWRITEAOF的时候负载因子比较大才进行扩展操作是因为此时Redis会创建子进程,而大多数操作系统采取了写时复制的技术来优化子进程使用效率,不适合在这种时候会做大规模的数据迁移活动,说白了就是为了节约内存和提升效率)

3. 当前负载因子小于0.1时进行收缩操作

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值