代码改变世界

redis代码解析-dictionary类型

2017-09-19 11:00  NicGanon  阅读(538)  评论(0编辑  收藏  举报

dict本质上是为了解决算法中的查找问题(Searching),一般查找问题的解法分为两个大类:一个是基于各种平衡树,一个是基于哈希表。

 

redis中的dict传统的哈希算法类似,它采用某个哈希函数从key计算得到在哈希表中的位置,采用拉链法解决冲突,并在装载因子(load factor)超过预定值时自动扩展内存,引发重哈希(rehashing).在一个dict中有两个hash表,目的是来实现逐步的rehash,同时rehash时仍然可以访问dict.

 

正常使用的是ht[0],在rehash的时候先设置ht[1]大小为rehash后的需要的大小,然后逐步的将ht[0]中的内容rehash到ht[1]中,rehashidx记录着当前rehash到的index,在全部rehash完成之后,将ht[1]给ht[0],再reset ht[1].

当前dict是否处于rehash过程中可以通过rehashidx的值来判断,rehash时记录的是已经进行到的index,非rehash时为-1.

逐步的rehash是将rehash操作分散到对于dict的各个增删改查的操作中去。这种方法能做到每次只对一小部分key进行重哈希,而每次重哈希之间不影响dict的操作。dict这样设计避免重哈希期间单个请求的响应时间的剧烈增加

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    unsigned long iterators; /* number of iterators currently running */
} dict;

 

一个hash table即dictht中的table是一个dictEntry数组,dict每次rehash时,table扩容或者缩小的时候都是2的n次方大小.

同时dictht还记录了当前table的大小,以及存在的dictEntry的数量.判断是否需要rehash就是通过used/size==1为需要rehash,used/size==5需要强制rehash

dictht中sizemask的值为size-1,因为size总是2的n次方,所以sizemask是n位的1,用来一个key的hash&sizemask定位出这个key应该在table中的位置.

 同时,解决hash冲突的方法是链式

typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;

/* This is our hash table structure. Every dictionary has two of this as we
 * implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;


 

一个dict的图示如下:

图片引用自:http://zhangtielei.com/posts/blog-redis-dict.html

 

dict的其他操作都非常简单,都是正常的对hash table的操作

dict scan还有点意思,因为redis中dict数据结构的关系,所以没办法使用传统的顺序遍历,因为

  1 如果发生了扩容,原来的表大小是8,扩容之后是16,就会把原来的1-8rehash到新的1-16个slot中.如果现在要顺序访问8,则从8-16之前的其实全部在旧的表中1-7中访问过,会有大量的重复访问.

  2 如果发生了缩表,原来的16缩小到8,就正好相反会有遗漏.

  3 如果正在rehash,t0表中的数据是不全的也依然肯定有问题.

所以,redis中dict scan设计了一种新的遍历方法:对二进制高位进行加1遍历.

如果表的大小是8的话,和大小为2的访问顺序会是如下:

000 -> 0   00-> 0
100 -> 4   10-> 2
010 -> 2   01-> 1
110 -> 6   11-> 3
001 -> 1
101 -> 5
011 -> 3
111 -> 7

 1 表扩容,先访问n,然后是访问n+2的n次方,不会重复,不会遗漏.

2 表缩小,通过n&m0(size mask)定位,可能会有重复的情况,不会遗漏.

3 如果正在rehash过程中,访问n的时候,会先在容量小的表中访问(n&m0),然后在大的表中遍历出全部的n&m0对应的位置.

dict scan 详细可参考这篇blog,http://chenzhenianqing.cn/articles/1101.html