Redis数据结构-dict

    科技2022-08-15  130

    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]的大小为第一个大于等于的整数

        (2)如果是收缩操作,ht[1]的大小为第一个大于等于的整数

    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时进行收缩操作

    Processed: 0.012, SQL: 8