Redis字典

参考《Redis设计与实现》第四章 和 Redis6 源码
参考笔记链接

  • Redis的数据库是用字典来作为底层实现的,字典也是哈希键的底层实现之一。字典中每个键都是独一无二的。
  • Redis字典使用哈希表作为底层实现。

哈希表

Redis哈希表的结构如下

/* 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指针的一维数组。
    dictEntry **table;

    // 哈希表大小
    unsigned long size;

    // 哈希表大小掩码,用于计算索引值
    // 总是等于size - 1
    unsigned long sizemask;

    // 该哈希表已有节点的数量
    unsigned long used;
} dictht;

哈希表节点

typedef struct dictEntry {
    
    // 键
    void *key;

    // 值
    
    // union是共用体结构,成员之间会相互影响,
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;

    // 指向下一个哈希表节点,形成链表
    struct dictEntry *next;
} dictEntry;

其中,对键值对的值v赋值时,该值可以是void*指针,也可以是uint64_t整数,又或者是一个int64_t整数,也可以是double类型。

字典

// 字典
typedef struct dict {
    
    // 类型特定函数
    dictType *type;

    // 私有数据
    void *privdata;

    // 哈希表
    dictht ht[2];

    // rehash索引,指向下一个要rehash的bucket(节点链表)
    // 当rehash不在进行时,值为-1
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */

    // 如果>0暂停rehasing,<0表示编码错误
    int16_t pauserehash; /* If >0 rehashing is paused (<0 indicates coding error) */
} dict;
typedef struct dictType {
    // 计算哈希值的函数
    uint64_t (*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);

    // 字典内存扩展的函数
    int (*expandAllowed)(size_t moreMem, double usedRatio);
} dictType;
  • Redis会为用途不同的字典设置不同的类型特定函数。
  • ht[1]一般只会在对ht[0]哈希表进行rehash时使用。

rehash

具体见《Redis设计与实现》第四章 4.4 rehash。

为了让哈希表的负载因子(load factor)维持在一个合理的范围之内,需要对哈希表的大小进行相应的扩展或收缩。

负载因子的计算:
在这里插入图片描述

扩展和收缩哈希表的工作可以通过执行rehash (重新散列) 操作来完成。第一步就是为ht[1]分配内存空间:
在这里插入图片描述
例如ht[0].used = 5,如果执行扩展操作, 5 * 2 = 10;因此ht[1]的大小就是16;如果执行收缩操作,ht[1]的大小就是8。

渐进式rehash

  • 为了避免rehash对服务器性能造成影响(键值对数量太多),服务器分多次、渐进式地将ht[0]里地键值对慢慢地rehash到ht[1]。
  • 渐进式rehash期间,新添加的键值对一律保存到ht[1]中,ht[0]不再进行任何添加操作,保证ht[0]在这期间包含的键值对数量只减不增。

这期间在字典里面查找一个键,程序会先到ht[0]找,再到ht[1]找,具体实现如下:

// 根据键key查找哈希表节点
dictEntry *dictFind(dict *d, const void *key)
{
    dictEntry *he;
    uint64_t h, idx, table;

    if (dictSize(d) == 0) return NULL; /* dict is empty */
    // 如果字典正在rehash,则执行rehash的步骤。
    if (dictIsRehashing(d)) _dictRehashStep(d);
    // 得到key对应的hash值
    h = dictHashKey(d, key);

    /* 先在ht[0]里找,再到ht[1]里去找 */
    for (table = 0; table <= 1; table++) {
        // 根据hash值得到在表中对应的索引值
        idx = h & d->ht[table].sizemask;
        // 根据索引值得到表中对应索引值的哈希表头节点
        he = d->ht[table].table[idx];
        // he为NULL,则该hash值对应的节点不在该表中(已经迁移)
        // 否则,访问表中对应索引值的节点链表找到节点
        while(he) { 
            if (key==he->key || dictCompareKeys(d, key, he->key))
                return he;
            he = he->next;
        }

        /* 如果字典已经停止rehash了,那么就意味着ht[1]为空了,
        在ht[0]没找到,也不用去ht[1]中找了*/
        if (!dictIsRehashing(d)) return NULL;
    }
    return NULL;
}

rehash过程

/* Performs N steps of incremental rehashing. Returns 1 if there are still
 * keys to move from the old to the new hash table, otherwise 0 is returned.
 *
 * Note that a rehashing step consists in moving a bucket (that may have more
 * than one key as we use chaining) from the old to the new hash table, however
 * since part of the hash table may be composed of empty spaces, it is not
 * guaranteed that this function will rehash even a single bucket, since it
 * will visit at max N*10 empty buckets in total, otherwise the amount of
 * work it does would be unbound and the function may block for a long time. */

 // n为rehash的bucket数量,bucket即表索引对应的链表
 // dickFind()调用该函数每次都只rehash一个bucket,即n = 1
 // 返回0表示Rehash结束,返回1表示还得继续
int dictRehash(dict *d, int n) {
    // 允许访问空bucket的最大数量
    // 如果n = 1, 最多只能访问10个空bucket,超过就不访问直接返回。
    // 即rehashidx最多加10 (n * 10)
    int empty_visits = n*10; /* Max number of empty buckets to visit. */
    if (!dictIsRehashing(d)) return 0;

    // 表的节点数量不能为0
    while(n-- && d->ht[0].used != 0) {
        dictEntry *de, *nextde;

        /* Note that rehashidx can't overflow as we are sure there are more
         * elements because ht[0].used != 0 */
        // 防止数组越界访问
        assert(d->ht[0].size > (unsigned long)d->rehashidx);
        // 找到非空bucket,empty_visits次数后找不到就返回了
        while(d->ht[0].table[d->rehashidx] == NULL) {
            d->rehashidx++;
            if (--empty_visits == 0) return 1;
        }
        de = d->ht[0].table[d->rehashidx];
        /* Move all the keys in this bucket from the old to the new hash HT */
        // bucket(链表)节点全部迁移,即一次迁移整个bucket(链表)
        while(de) {
            uint64_t h;

            nextde = de->next;
            /* Get the index in the new hash table */
            // 得到节点在ht[1]中的索引值
            h = dictHashKey(d, de->key) & d->ht[1].sizemask;
            de->next = d->ht[1].table[h];
            d->ht[1].table[h] = de;
            d->ht[0].used--;
            d->ht[1].used++;
            de = nextde;
        }
        d->ht[0].table[d->rehashidx] = NULL;
        // rehashidx是下一个要rehash的bucket(节点链表)的索引
        d->rehashidx++;
    }

    /* Check if we already rehashed the whole table... */
    // 如果ht[0]中的节点已经全部迁移
    if (d->ht[0].used == 0) {
        // 清空ht[0]的哈希表
        zfree(d->ht[0].table);
        // ht[1]赋给ht[0]
        d->ht[0] = d->ht[1];
        // 重新初始化一个ht[1]供下次rehash使用
        _dictReset(&d->ht[1]);
        // rehashidx为-1表示rehash结束
        d->rehashidx = -1;

        return 0;
    }

    /* More to rehash... */
    return 1;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值