redis---字典详情

概念:

字典是Redis服务器中最常用到的复合型数据结构,除了hash 结构的数据会用到字典外,整个Redis数据库的所有key和value 也组成了一个全局字典,还有带过期时间的 key 集合也是一个字典。zset 集合中存储 value 和 score 值的映射关系也是通过字典结构实现的。redis所使用的C语言并没有内置字典这种结构,redis自己构建了字典的实现。其实现原理跟Java中的HashMap基本一致,内部实现也差不多类似,都是通过 “数组 + 链表” 的机构。其实我们对数据库增删改查操作都是构建在字典的操作之上的。

结构:

源码定义如 dict.h/dictht :

typedefstruct dictht {
    // 哈希表数组
    dictEntry **table;
    // 哈希表大小
    unsignedlong size;
    // 哈希表大小掩码,用于计算索引值,总是等于 size - 1
    unsignedlong sizemask;
    // 该哈希表已有节点的数量
    unsignedlong used;
} dictht;

typedefstruct dict {
    dictType *type;
    void *privdata;
    // 内部有两个 dictht 结构
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    unsignedlong iterators; /* number of iterators currently running */
} dict;

table 属性是一个数组,数组中的每个元素都是一个指向 dict.h/dictEntry 结构的指针,而每个 dictEntry 结构保存着一个键值对。下面是对C语言中**的解释:

*表示指针,**表示指针的指针。
例如:int *a;这个语句声明了一个变量a,a的数据类型是int *,也就是整型变量的指针类型(如果不懂什么是指针,那这个问题就没有意义了)。
也就是说a的值是一个内存地址,在这个地址所在的内存空间中存放的是一个整型变量。再看:int **b;这个语句也声明了一个变量b,b的数据类型
是int **,也就是整型变量的指针的指针类型(二级指针)。也就是说 b的值是一个内存地址,该地址所在的内存空间中存放的是一个整型变量的
指针(一级指针,或许就是上面那个a的值)。

典型的链表结构dictEntry

typedefstruct dictEntry {
    // 键
    void *key;
    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    // 指向下个哈希表节点,形成链表
    struct dictEntry *next;
} dictEntry;

可以从上面的源码 dictht ht[2];中看到,字典内部包含两个 hashtable,通常情况下只有一个 hashtable 是有值的。但是在 扩容缩容时 ,需要分配新的hashtable,然后进行渐进式搬迁,这时候两个 hashtable 存储的分别是旧的 hashtable 和新的 hashtable。待搬迁结束后,旧的has htable 被删除,新的hashtable 取而代之。
在这里插入图片描述

链地址法解决哈希冲突

这里跟java中的hashmap一样,而且采用了跟java8一样的策略头插法。在有新值加入时,总是将值加入到链表的表头。图片来源:《redis设计与实现》
在这里插入图片描述

渐进式rehash

大字典的扩容是比较耗时间的,需要重新申请新的数组,然后将旧字典所有链表中的元素重新挂接到新的数组下面,这是一个 O(n) 级别的操作,作为单线程的 Redis 很难承受这样耗时的过程,所以 Redis 使用 渐进式 rehash 小步搬迁。
在这里插入图片描述
扩缩容的条件:

正常情况下,当 hash 表中元素的个数等于第一维数组的长度时,就会开始扩容,扩容的新数组是原数组大小的 2 倍。不过如果 Redis 正在做 bgsave,为了减少内存页的过多分离 (Copy On Write),Redis 尽量不去扩容 (dict_can_resize),但是如果 hash 表已经非常满了,元素的个数已经达到了第一维数组长度的 5 倍 (dict_force_resize_ratio),说明 hash 表已经过于拥挤了,这个时候就会强制扩容

缩容的条件:

当 hash 表因为元素逐渐被删除变得越来越稀疏时,Redis 会对 hash 表进行缩容来减少 hash 表的第一维数组空间占用。所用的条件是 元素个数低于数组长度的 10%,缩容不会考虑 Redis 是否在做 bgsave。

扩容的过程:

渐进式 rehash 会在 rehash 的同时,保留新旧两个 hash 结构,查询时会同时查询两个 hash 结构,然后在后续的定时任务以及 hash 操作指令中,循序渐进的把旧字典的内容迁移到新字典中。当搬迁完成了,就会使用新的 hash 结构取而代之。

  • 为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表。
  • 在字典中维持一个索引计数器变量rehashidx,并将它置为0,表示rehash工作开始。
  • 在rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定操作以外,还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1]中,当rehash工作完成之后,程序将rehashidx属性的值+1。
  • 随着字典操作的不断进行,最终在某个时间点上,ht[0]的所有键值对都被rehash到ht[1]上,这时将rehashidx属性设为-1,表示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.
 *
 * 执行 N 步渐进式 rehash 。
 *
 * 返回 1 表示仍有键需要从 0 号哈希表移动到 1 号哈希表,
 * 返回 0 则表示所有键都已经迁移完毕。
 *
 * 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.
 *
 * 注意,每步 rehash 都是以一个哈希表索引作为单位的,这个哈希表的索引可以看做是桶(联想桶排序)。
 * 一个桶里可能会有多个节点,被 rehash 的桶里的所有节点都会被移动到新哈希表。
 *
 * T = O(N)
 */
int dictRehash(dict *d, int n) {

    // dictIsRehashing标识是否在执行rehash
    // 只可以在 rehash 进行中时执行
    if (!dictIsRehashing(d)) return 0;

    // 进行 N 步迁移
    // T = O(N)
    while(n--) {
        dictEntry *de, *nextde;    

        /* Check if we already rehashed the whole table... */
        // 如果 0 号哈希表为空,那么表示 rehash 执行完毕
        // T = O(1)
        if (d->ht[0].used == 0) {
            // 释放 0 号哈希表
            zfree(d->ht[0].table);
            // 将原来的 1 号哈希表设置为新的 0 号哈希表
            d->ht[0] = d->ht[1];
            // 重置旧的 1 号哈希表
            _dictReset(&d->ht[1]);
            // 关闭 rehash 标识
            d->rehashidx = -1;
            // 返回 0 ,向调用者表示 rehash 已经完成
            return 0;
        }

        /* Note that rehashidx can't overflow as we are sure there are more
         * elements because ht[0].used != 0 */
        // 断言,确保 rehashidx 没有越界
        assert(d->ht[0].size > (unsigned)d->rehashidx);

        // 略过数组中为空的索引,找到下一个非空索引
        while(d->ht[0].table[d->rehashidx] == NULL) d->rehashidx++;

        // de指向该索引的链表表头节点
        de = d->ht[0].table[d->rehashidx];
        /* Move all the keys in this bucket from the old to the new hash HT */
        // 将链表中的所有节点迁移到新哈希表
        // T = O(1)
        while(de) {     //链表中的每个节点都需要重新计算所在位置,而不是将整个链表直接存放到table一个索引中
            unsigned int h;

            // 保存下个节点的指针
            nextde = de->next;

            /* Get the index in the new hash table */
            // 计算新哈希表的哈希值,以及节点插入的索引位置
            // 索引位置为哈希值&掩码,比如说一个数和8相与,则得到的结果不可能大于8
            h = dictHashKey(d, de->key) & d->ht[1].sizemask;

            // 插入节点到新哈希表,每次插入都插入到链表的头结点,这样的时间复杂度为O(1)
            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;
        // 更新 rehash 索引
        d->rehashidx++;
    }

    return 1;
}

下图出自:《redis设计与实现》
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
查询键值对的 get 命令底层 API 调用,底层会调用 dictFind 方法

dictEntry *dictFind(dict *d, const void *key)
{
    dictEntry *he;
    unsigned int h, idx, table;

    if (d->ht[0].used + d->ht[1].used == 0) return NULL; 
    if (dictIsRehashing(d)) _dictRehashStep(d);
    h = dictHashKey(d, key);
    for (table = 0; table <= 1; table++) {
        idx = h & d->ht[table].sizemask;
        he = d->ht[table].table[idx];
        while(he) {
            if (key==he->key || dictCompareKeys(d, key, he->key))
                return he;
            he = he->next;
        }
        if (!dictIsRehashing(d)) return NULL;
    }
    return NULL;
}

同样也是有 dictIsRehashing 方法的判断,如果字典处于 rehash 状态,即需要去完成一个桶的转移,然后才能返回。值得注意的是,方法的中间逻辑是嵌套在一个 for 循环中的,供两次循环,第一次从 ht[0] 中搜索我们给定 key 的键值对,如果没有找到,第二次循环将从 ht[1] 中搜索我们要查询的键值对。

之所以说 redis 的 rehash 是渐进式的,就是因为即便它处于 rehash 状态下,所有节点的插入、查询甚至于删除都是不受影响的,直至整个 rehash 结束,redis 释放原先 ht[0] 占用无用内存。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值