• 谈Redis的refash的增量式扩容


    谈Redis的refash的增量式扩容

    最近在复习的时候,研究了下关于redis为什么rehash对redis的性能影响小,原因之一在于它的增量式复制,也叫渐进式hash吧!其实这种思想很值得借鉴,分清轻重优化选择

    /* 哈希表节点 */
    typedef struct dictEntry {
        // 键
        void *key;
        // 值
        union {
            void *val;
            uint64_t u64;
            int64_t s64;
        } 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. */
    /* 哈希表
     * 每个字典都使用两个哈希表,以实现渐进式 rehash 。
     */
    typedef struct dictht {
        // 哈希表数组
        // 可以看作是:一个哈希表数组,数组的每个项是entry链表的头结点(链地址法解决哈希冲突)
        dictEntry **table;
        // 哈希表大小
        unsigned long size;
        // 哈希表大小掩码,用于计算索引值
        // 总是等于 size - 1
        unsigned long sizemask;
        // 该哈希表已有节点的数量
        unsigned long used;
    } dictht;
    /* 字典 */
    typedef struct dict {
        // 类型特定函数
        dictType *type;
        // 私有数据
        void *privdata;
        // 哈希表
        dictht ht[2];
        // rehash 索引
        // 当 rehash 不在进行时,值为 -1
        int rehashidx; /* rehashing not in progress if rehashidx == -1 */
        // 目前正在运行的安全迭代器的数量
        int iterators; /* number of iterators currently running */
    } dict;
    
    

    dict的结构大致如上,接下来分析一下其中最重要的几个数据成员:

    • dictht::table:哈希表内部的table结构使用了链地址法来解决哈希冲突,刚开始看的时候我很奇怪,这怎么是个二维数组?这其实是一个指向数组的指针,数组中的每一项都是entry链表的头结点。

    • dictht ht[2]:在dict的内部,维护了两张哈希表,作用等同于是一对滚动数组,一张表是旧表,一张表是新表,当hashtable的大小需要动态改变的时候,旧表中的元素就往新开辟的新表中迁移,当下一次变动大小,当前的新表又变成了旧表,以此达到资源的复用和效率的提升。

    • 字段rehashidx:因为是渐进式的哈希,数据的迁移并不是一步完成的,所以需要有一个索引来指示当前的rehash进度。当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.
     *
     * 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.
     * rehash是以bucket(桶)为基本单位进行渐进式的数据迁移的,每步完成一个bucket的迁移,直至所有数据迁移完毕。一个bucket对应哈希表数组中的一条entry链表。新版本的dictRehash()还加入了一个最大访问空桶数(empty_visits)的限制来进一步减小可能引起阻塞的时间。
     */
    int dictRehash(dict *d, int n) {
        int empty_visits = n*10; /* Max number of empty buckets to visit. */
        if (!dictIsRehashing(d)) return 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);
            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 */
            while(de) {
                uint64_t h;
    
                nextde = de->next;
                /* Get the index in the new hash table */
                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;
            d->rehashidx++;
        }
    
        /* Check if we already rehashed the whole table... */
        if (d->ht[0].used == 0) {
            zfree(d->ht[0].table);
            d->ht[0] = d->ht[1];
            _dictReset(&d->ht[1]);
            d->rehashidx = -1;
            return 0;
        }
    
        /* More to rehash... */
        return 1;
    }
    

    接下来我们深扒一下这个函数的具体实现。

    • 判断dict是否正在rehashing,只有是,才能继续往下进行,否则已经结束哈希过程,直接返回。
    • 接着是分n步进行的渐进式哈希主体部分(n由函数参数传入),在while的条件里面加入对.used旧表中剩余元素数目的观察,增加安全性。
      一个runtime的断言保证一下渐进式哈希的索引没有越界。
      *接下来一个小while是为了跳过空桶,同时更新剩余可以访问的空桶数,empty_visits这个变量的作用之前已经说过了。
      *现在我们来到了当前的bucket,在下一个while(de)中把其中的所有元素都迁移到ht[1]中,索引值是辅助了哈希表的大小掩码计算出来的,可以保证不会越界。同时更新了两张表的当前元素数目。
      *每一步rehash结束,都要增加索引值,并且把旧表中已经迁移完毕的bucket置为空指针。
      *最后判断一下旧表是否全部迁移完毕,若是,则回收空间,重置旧表,重置渐进式哈希的索引,否则用返回值告诉调用方,dict内仍然有数据未迁移。

    渐进式哈希的精髓在于:数据的迁移不是一次性完成的,而是可以通过dictRehash()这个函数分步规划的,并且调用方可以及时知道是否需要继续进行渐进式哈希操作。如果dict数据结构中存储了海量的数据,那么一次性迁移势必带来redis性能的下降,别忘了redis是单线程模型,在实时性要求高的场景下这可能是致命的。而渐进式哈希则将这种代价可控地分摊了,调用方可以在dict做插入,删除,更新的时候执行dictRehash(),最小化数据迁移的代价。
    在迁移的过程中,数据是在新表还是旧表中并不是一个非常急迫的需求,迁移的过程并不会丢失数据,在旧表中找不到再到新表中寻找就是了。
    参考博客:https://blog.csdn.net/cqk0100/article/details/8040081

  • 相关阅读:
    20220424 Java核心技术 卷1 基础知识 7
    20220424 Java核心技术 卷1 基础知识 45
    20220424 Java核心技术 卷1 基础知识 14
    20220424 Java核心技术 卷1 基础知识 9
    20220424 Java核心技术 卷1 基础知识 8
    20220424 Java核心技术 卷1 基础知识 13
    15、集合框架_ArrayList
    12接口
    18、集合框架_HashMap\TreeMap
    16、集合框架_LinkedList\Vertor
  • 原文地址:https://www.cnblogs.com/ontoweb-zp/p/10477039.html
Copyright © 2020-2023  润新知