1.字典的实现
Redis中字典使用的哈希表结构
    typedef struct dictht {
        // 哈希表数组
        dictEntry **table;
        // 哈希表大小
        unsigned long size;
        // 哈希表大小掩码,用于计算索引值
        // 总是等于 size - 1
        unsigned long sizemask;
        // 该哈希表已有节点的数量
        unsigned long used;
    } dictht;table是一个数组,存放指向entry键值对节点的指针
size则是数组长度。used则是数组中已有entry的数量。
Redis中哈希表节点的结构(dictEntry)
 
    typedef struct dictEntry {
        // 键
        void *key;
        // 值
        union {
            void *val;
            uint64_t u64;
            int64_t s64;
        } v;
        // 指向下个哈希表节点,形成链表
        struct dictEntry *next;
    } dictEntry;key键值对中键
v为键值对中的值。v可以为指针也可以为一个整数。
next设计为当出现hash冲突时,冲突的hashentry将放置在当前已存在entry的前面,采用头插法。
这里与HashMap里的hash结构类似,数组+链表的结构,除了hash冲突插入方法有所不同以外,其他很相似。
Redis中字典的结构 (dict)
    typedef struct dict {
        // 类型特定函数
        dictType *type;
        // 私有数据
        void *privdata;
        // 哈希表
        dictht ht[2];
        // rehash 索引
        // 当 rehash 不在进行时,值为 -1
        int rehashidx; /* rehashing not in progress if rehashidx == -1 */
    } dict;哈希表的dictht的上层结构又封装了一层,这就是字典了。
字典结构的用处就是:
拥有一个*type用来区分不同类型的键值对,为多态字典保留。privdata也是为了多态字典而准备,用于需要传给那些类型特定函数的可选参数。
拥有存放dictht的一个数组,只包含两个哈希表。初始时ht[0]哈希表被具体使用,当rehash时,ht[0]的数据会被rehash到ht[1]。
Redis中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;由此结构可见,本结构中具体封装了拥有不同类型的entry的哈希表所对应拥有的不同类型的方法函数。为多态字典准备。
到这里我们知道了从外层到内层的结构为:
dict 字典->dictht 哈希表 -> dictEntry数组table -> dictEntry具体的hash节点(单向链表) ->k,v具体的键值对

2.Redis的rehash操作
很多语言中的hashmap的结构都有rehash操作。redis中操作步骤如下:
1.为hash表重新分配空间
- 如果执行的是扩展操作, 那么 ht[1]的大小为第一个大于等于ht[0].used * 2的 (2的n次方幂);
- 如果执行的是收缩操作, 那么 ht[1]的大小为第一个大于等于ht[0].used的 。
如何理解呢,如果是扩容,我们把每个扩容等级想象成上台阶,每一级台阶上可以存放的entry数量为1,2,4,8,16,32...。每次扩容比如entry数量为10时,那么扩容就会扩容到10*2=20,第一个大于等于20的 2的n次方的值就是32,这里大于20的2的n次方的值有32,64,128.....扩容为第一个就行,记得entry的数量要*2
如果是缩容,那么直接缩到当前一个大于等于当前entry数量的一个2的n次方的值就行。比如当前entry为3,那么4就是第一个大于等于3的一个2的n次方的值。
3.Redis的rehash时机
3.1 当以下条件中的任意一个被满足时, 程序会自动开始对哈希表执行扩展操作:
服务器目前没有在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 1
服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 5
3.2 当哈希表的负载因子小于 0.1 时, 程序自动开始对哈希表执行收缩操作。
 
3.3 负载因子
 
    # 负载因子 = 哈希表已保存节点数量 / 哈希表大小
    load_factor = ht[0].used / ht[0].size4.渐进式rehash
渐进式rehash的意义是为了避免 rehash 对服务器性能造成影响, 服务器不是一次性将 ht[0] 里面的所有键值对全部 rehash 到 ht[1] , 而是分多次、渐进式地将 ht[0] 里面的键值对慢慢地 rehash 到 ht[1]
步骤:
4.1 为 ht[1] 分配空间, 让字典同时持有 ht[0] 和 ht[1] 两个哈希表。
4.2 在字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始。
4.3 在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增一。
4.4 随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。
rehash过程中,增删改查操作有以下特点:
增加只会在ht[1]表中增加,这一措施保证了 ht[0] 包含的键值对数量会只减不增, 并随着 rehash 操作的执行而最终变成空表。
删除,修改,查找,都会在两张表执行。


















