精华内容
下载资源
问答
  • laravel-needs-auto-hash ... ...composer require samasend/laravel-needs-auto-rehash 基本用法 就是这样,您只需要安装软件包即可。 :rocket: 这是如何运作的? 这个神奇的程序包侦听的内置Illumi
  • 基于Redis Cluster打造的缓存系统。...本文尝试分享在运维过程中踩过的Redis Rehash机制的一些坑以及我们的解决方案,其中在高负载情况下物理机发生丢包的现象和解决方案已经写成博客。感兴趣的同学可以参考:
  • 对HashMap扩容时重新计算旧数组元素在新数组地址的rehash方法中的(e.hash&oldCap)==0算法推导
  • Project-ReHash-源码

    2021-08-04 10:49:23
    项目-ReHash 仅测试 测试 2
  • rehash

    千次阅读 2018-07-14 23:35:43
    rehash当哈希表的大小不能满足需求,就可能会有两个或者以上数量的键被分配到了哈希表数组上的同一个索引上,于是就发生冲突(collision),在Redis中解决冲突的办法是链接法(separate chaining)。但是需要尽可能...
    rehash
    

    当哈希表的大小不能满足需求,就可能会有两个或者以上数量的键被分配到了哈希表数组上的同一个索引上,于是就发生冲突(collision),在Redis中解决冲突的办法是链接法(separate chaining)。但是需要尽可能避免冲突,希望哈希表的负载因子(load factor),维持在一个合理的范围之内,就需要对哈希表进行扩展或收缩。

    Redis对哈希表的rehash操作步骤如下:

    扩展或收缩
    扩展:ht[1]的大小为第一个大于等于ht[0].used * 2的 2n2n 。
    收缩:ht[1]的大小为第一个大于等于ht[0].used的 2n2n 。
    将所有的ht[0]上的节点rehash到ht[1]上。
    释放ht[0],将ht[1]设置为第0号表,并创建新的ht[1]。
    源码再此:

    扩展操作
    static int _dictExpandIfNeeded(dict *d) //扩展d字典,并初始化
    {
        /* Incremental rehashing already in progress. Return. */
        if (dictIsRehashing(d)) return DICT_OK;     //正在进行rehash,直接返回

        /* If the hash table is empty expand it to the initial size. */
        if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE); //如果字典(的 0 号哈希表)为空,那么创建并返回初始化大小的 0 号哈希表

        /* If we reached the 1:1 ratio, and we are allowed to resize the hash
         * table (global setting) or we should avoid it but the ratio between
         * elements/buckets is over the "safe" threshold, we resize doubling
         * the number of buckets. */
        //1. 字典已使用节点数和字典大小之间的比率接近 1:1
        //2. 能够扩展的标志为真
        //3. 已使用节点数和字典大小之间的比率超过 dict_force_resize_ratio
        if (d->ht[0].used >= d->ht[0].size && (dict_can_resize ||    
             d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))    
        {
            return dictExpand(d, d->ht[0].used*2);  //扩展为节点个数的2倍
        }
        return DICT_OK;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    收缩操作:
    int dictResize(dict *d)     //缩小字典d
    {
        int minimal;

        //如果dict_can_resize被设置成0,表示不能进行rehash,或正在进行rehash,返回出错标志DICT_ERR
        if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;

        minimal = d->ht[0].used;            //获得已经有的节点数量作为最小限度minimal
        if (minimal < DICT_HT_INITIAL_SIZE)//但是minimal不能小于最低值DICT_HT_INITIAL_SIZE(4)
            minimal = DICT_HT_INITIAL_SIZE;
        return dictExpand(d, minimal);      //用minimal调整字典d的大小
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    扩展和收缩操作都调用了dictExpand()函数,该函数通过计算传入的第二个大小参数进行计算,算出一个最接近2n2n的realsize,然后进行扩展或收缩,dictExpand()函数源码如下:

    int dictExpand(dict *d, unsigned long size)     //根据size调整或创建字典d的哈希表
    {
        dictht n; /* the new hash table */
        unsigned long realsize = _dictNextPower(size);  //获得一个最接近2^n的realsize

        /* the size is invalid if it is smaller than the number of
         * elements already inside the hash table */
        if (dictIsRehashing(d) || d->ht[0].used > size) //正在rehash或size不够大返回出错标志
            return DICT_ERR;

        /* Rehashing to the same table size is not useful. */
        if (realsize == d->ht[0].size) return DICT_ERR; //如果新的realsize和原本的size一样则返回出错标志
        /* Allocate the new hash table and initialize all pointers to NULL */
        //初始化新的哈希表的成员
        n.size = realsize;
        n.sizemask = realsize-1;
        n.table = zcalloc(realsize*sizeof(dictEntry*));
        n.used = 0;

        /* Is this the first initialization? If so it's not really a rehashing
         * we just set the first hash table so that it can accept keys. */
        if (d->ht[0].table == NULL) {   //如果ht[0]哈希表为空,则将新的哈希表n设置为ht[0]
            d->ht[0] = n;
            return DICT_OK;
        }

        /* Prepare a second hash table for incremental rehashing */
        d->ht[1] = n;           //如果ht[0]非空,则需要rehash
        d->rehashidx = 0;       //设置rehash标志位为0,开始渐进式rehash(incremental rehashing)
        return DICT_OK;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    收缩或者扩展哈希表需要将ht[0]表中的所有键全部rehash到ht[1]中,但是rehash操作不是一次性、集中式完成的,而是分多次,渐进式,断续进行的,这样才不会对服务器性能造成影响。因此下面介绍渐进式rehash。

    5. 渐进式rehash(incremental rehashing)

    渐进式rehash的关键:

    字典结构dict中的一个成员rehashidx,当rehashidx为-1时表示不进行rehash,当rehashidx值为0时,表示开始进行rehash。
    在rehash期间,每次对字典的添加、删除、查找、或更新操作时,都会判断是否正在进行rehash操作,如果是,则顺带进行单步rehash,并将rehashidx+1。
    当rehash时进行完成时,将rehashidx置为-1,表示完成rehash。
    源码在此:

    static void _dictRehashStep(dict *d) {      //单步rehash
        if (d->iterators == 0) dictRehash(d,1); //当迭代器数量不为0,才能进行1步rehash
    }

    int dictRehash(dict *d, int n) {       //n步进行rehash
        int empty_visits = n*10; /* Max number of empty buckets to visit. */
        if (!dictIsRehashing(d)) return 0;  //只有rehashidx不等于-1时,才表示正在进行rehash,否则返回0

        while(n-- && d->ht[0].used != 0) {  //分n步,而且ht[0]上还有没有移动的节点
            dictEntry *de, *nextde;

            /* Note that rehashidx can't overflow as we are sure there are more
             * elements because ht[0].used != 0 */
            //确保rehashidx没有越界,因为rehashidx是从-1开始,0表示已经移动1个节点,它总是小于hash表的size的
            assert(d->ht[0].size > (unsigned long)d->rehashidx);

            //第一个循环用来更新 rehashidx 的值,因为有些桶为空,所以 rehashidx并非每次都比原来前进一个位置,而是有可能前进几个位置,但最多不超过 10。
            //将rehashidx移动到ht[0]有节点的下标,也就是table[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];     //ht[0]下标为rehashidx有节点,得到该节点的地址
            /* Move all the keys in this bucket from the old to the new hash HT */
            //第二个循环用来将ht[0]表中每次找到的非空桶中的链表(或者就是单个节点)拷贝到ht[1]中
            while(de) {
                unsigned int h;

                nextde = de->next;  //备份下一个节点的地址
                /* Get the index in the new hash table */
                h = dictHashKey(d, de->key) & d->ht[1].sizemask;    //获得计算哈希值并得到哈希表中的下标h

                //将该节点插入到下标为h的位置
                de->next = d->ht[1].table[h];
                d->ht[1].table[h] = de;

                //更新两个表节点数目计数器
                d->ht[0].used--;
                d->ht[1].used++;

                //将de指向以一个处理的节点
                de = nextde;
            }
            d->ht[0].table[d->rehashidx] = NULL;    //迁移过后将该下标的指针置为空
            d->rehashidx++;                         //更新rehashidx
        }

        /* Check if we already rehashed the whole table... */
        if (d->ht[0].used == 0) {           //ht[0]上已经没有节点了,说明已经迁移完成
            zfree(d->ht[0].table);          //释放hash表内存
            d->ht[0] = d->ht[1];            //将迁移过的1号哈希表设置为0号哈希表
            _dictReset(&d->ht[1]);          //重置ht[1]哈希表
            d->rehashidx = -1;              //rehash标志关闭
            return 0;                       //表示前已完成
        }

        /* More to rehash... */
        return 1;           //表示还有节点等待迁移
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    6. 迭代器

    redis在字典结构也定义了迭代器

    typedef struct dictIterator {
        dict *d;                    //被迭代的字典
        long index;                 //迭代器当前所指向的哈希表索引位置
        int table, safe;            //table表示正迭代的哈希表号码,ht[0]或ht[1]。safe表示这个迭代器是否安全。
        dictEntry *entry, *nextEntry;   //entry指向当前迭代的哈希表节点,nextEntry则指向当前节点的下一个节点。
        /* unsafe iterator fingerprint for misuse detection. */
        long long fingerprint;      //避免不安全迭代器的指纹标记
    } dictIterator;
    1
    2
    3
    4
    5
    6
    7
    8
    迭代器分为安全迭代器和不安全迭代器:

    非安全迭代器只能进行Get等读的操作, 而安全迭代器则可以进行iterator支持的任何操作。
    由于dict结构中保存了safe iterators的数量,如果数量不为0, 是不能进行下一步的rehash的; 因此安全迭代器的存在保证了遍历数据的准确性。
    在非安全迭代器的迭代过程中, 会通过fingerprint方法来校验iterator在初始化与释放时字典的hash值是否一致; 如果不一致说明迭代过程中发生了非法操作.
    关于dictScan()反向二进制迭代器的原理介绍:Scan迭代器遍历操作原理
    展开全文
  • Rehash

    千次阅读 2019-09-19 20:14:17
    Rehash的初次见面 概念    Redis 的字典使用哈希表作为底层实现, 一个哈希表里面可以有多个哈希表节点, 而每个哈希表节点就保存了字典中的一个键值对。    哈希表中的键值对随着不断进行的操作增加或减少,...

    与Rehash的初次见面

    概念

       Redis 的字典使用哈希表作为底层实现, 一个哈希表里面可以有多个哈希表节点, 而每个哈希表节点就保存了字典中的一个键值对。

       哈希表中的键值对随着不断进行的操作增加或减少,为了将哈希表的负载因子维持在较为合理的范围内,程序需对哈希表的大小进行相应的扩展或者收缩,而rehash(重新散列)操作就可以完成这项工作。

    Redis 对字典的哈希表执行 rehash 的步骤如下:

    • 为字典的 ht[1] 哈希表分配空间, 这个哈希表的空间大小取决于要执行的操作, 以及 ht[0]当前包含的键值对数量(也即是ht[0].used 属性的值):如果执行的是扩展操作, 那么 ht[1] 的大小为第一个大于等于ht[0].used * 2 的 2^n (2 的 n 次方幂); 如果执行的是收缩操作, 那么 ht[1]的大小为第一个大于等于 ht[0].used 的 2^n。

    • 将保存在 ht[0] 中的所有键值对 rehash 到 ht[1]上面:rehash指的是重新计算键的哈希值和索引值,然后将键值对放置到 ht[1] 哈希表的指定位置上。

    • 当 ht[0]包含的所有键值对都迁移到了ht[1] 之后 (ht[0] 变为空表), 释放 ht[0] , 将 ht[1] 设置为 ht[0] , 并在 ht[1] 新创建一个空白哈希表, 为下一次 rehash 做准备。

       当以下条件中的任意一个被满足时, 程序会自动开始对哈希表执行扩展操作:

    • 服务器目前没有在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 1 ;
    • 服务器目前正在执行BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 5 ;

       其中哈希表的负载因子可以通过公式:负载因子 = 哈希表已保存节点数量 / 哈希表大小load_factor = ht[0].used / ht[0].size计算得出。
       比如说, 对于一个大小为 4 , 包含 4 个键值对的哈希表来说, 这个哈希表的负载因子为:load_factor = 4 / 4 = 1
       又比如说, 对于一个大小为 512 , 包含 256 个键值对的哈希表来说, 这个哈希表的负载因子为:load_factor = 256 / 512 = 0.5

       根据 BGSAVE 命令或 BGREWRITEAOF 命令是否正在执行, 服务器执行扩展操作所需的负载因子并不相同,这是因为在执行 BGSAVE 命令或BGREWRITEAOF 命令的过程中, Redis 需要创建当前服务器进程的子进程。 而大多数操作系统都采用写时复制(copy-on-write)技术来优化子进程的使用效率, 所以在子进程存在期间, 服务器会提高执行扩展操作所需的负载因子, 从而尽可能地避免在子进程存在期间进行哈希表扩展操作, 这可以避免不必要的内存写入操作, 最大限度地节约内存。
       另一方面, 当哈希表的负载因子小于 0.1 时, 程序自动开始对哈希表执行收缩操作。

    Rehashing

      rehash动作是分多次、渐进式地完成的。原因在于, 如果 ht[0] 里只保存着四个键值对, 那么服务器可以在瞬间就将这些键值对全部 rehash 到 ht[1] ; 但是, 如果哈希表里保存的键值对数量不是四个, 而是四百万、四千万甚至四亿个键值对, 那么要一次性将这些键值对全部 rehash 到 ht[1] 的话, 庞大的计算量可能会导致服务器在一段时间内停止服务。

      步骤:

    • ht[1] 分配空间, 让字典同时持有 ht[0] 和 ht[1] 两个哈希表。
    • 在字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始。
    • 在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增一。
    • 随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。

      渐进式 rehash 的好处在于它采取分而治之的方式, 将 rehash 键值对所需的计算工作均滩到对字典的每个添加、删除、查找和更新操作上, 从而避免了集中式 rehash 而带来的庞大计算量。

      图形表示步骤:

    • 准备开始rehash
      在这里插入图片描述
    • rehash索引0上的键值对

    在这里插入图片描述

    • rehash索引1上的键值对

    在这里插入图片描述

    • rehash索引2上的键值对

    在这里插入图片描述

    • rehash索引3上的键值对

    在这里插入图片描述

    • rehash执行完毕

    在这里插入图片描述
    《Redis设计与实现》

    展开全文
  • 渐进式ReHash

    2021-07-13 09:32:20
    一图胜千言 (1)准备 rehash (2)rehash index=0 (3)rehash index=1 (4)rehash index=2 (5)rehash index=3 (6)rehash 完成 缩容扩容的思考 看完了上面的流程,不知道你对 rehash 是否有一个大概了思路呢...

    本文来说下redis中hash表的扩容问题。


    本文思维导图

    整个rehash过程并不是一步完成的,而是分多次、渐进式的完成。如果哈希表中保存着数量巨大的键值对时,若一次进行rehash,很有可能会导致服务器宕机。

    在这里插入图片描述

    渐进式rehash的好处在于它采取分为而治的方式,将rehash键值对的计算均摊到每个字典增删改查操作,避免了集中式rehash的庞大计算量。


    HashMap 的rehash回顾

    读过 HashMap 源码的同学,应该都知道 map 在扩容的时候,有一个 rehash 的过程

    这里简单介绍下:

    扩容(resize)就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。

    当然Java里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组,就像我们用一个小桶装水,如果想装更多的水,就得换大水桶。


    redis 中的扩容设计

    HashMap 的扩容需要对集合中大部分的元素进行重新计算,但是对于 redis 这种企业级应用,特别是单线程的应用,如果像传统的 rehash 一样把所有元素来一遍的话,估计要十几秒的时间。

    十几秒对于常见的金融、电商等相对高并发的业务场景,是无法忍受的。

    那么 redis 的 rehash 是如何实现的呢?

    实际上 redis 的 rehash 动作并不是一次性、集中式地完成的, 而是分多次、渐进式地完成的

    这里补充一点,不单单是扩容,缩容也是一样的道理,二者都需要进行 rehash。

    只增不降就是对内存的浪费,浪费就是犯罪,特别是内存还这么贵。

    ps: 这种思想和 key 淘汰有异曲同工之妙,一口吃不了一个大胖子,一次搞不定,那就 1024 次,慢慢来总能解决问题。


    Redis 的渐进式 rehash

    为什么要渐进式处理

    实际上 redis 内部有两个 hashtable,我们称之为 ht[0] 和 ht[1]。传统的 HashMap 中只有一个。

    为了避免 rehash 对服务器性能造成影响, 服务器不是一次性将 ht[0] 里面的所有键值对全部 rehash 到 ht[1] , 而是分多次、渐进式地将 ht[0] 里面的键值对慢慢地 rehash 到 ht[1] 。


    详细步骤

    哈希表渐进式 rehash 的详细步骤

    1. 为 ht[1] 分配空间, 让字典同时持有 ht[0] 和 ht[1] 两个哈希表。
    2. 在字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始。
    3. 在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增1。
    4. 随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。

    渐进式 rehash 的好处在于它采取分而治之的方式, 将 rehash 键值对所需的计算工作均滩到对字典的每个添加、删除、查找和更新操作上, 从而避免了集中式 rehash 而带来的庞大计算量


    rehash 间的操作怎么兼容

    因为在进行渐进式 rehash 的过程中, 字典会同时使用 ht[0] 和 ht[1] 两个哈希表, 那这期间的操作如何保证正常进行呢?

    查询一个信息

    这个类似于我们的数据库信息等迁移,先查询一个库,没有的话,再去查询另一个库。

    ht[0] 中没找到,我们去 ht[1] 中查询即可。

    新数据怎么办?

    这个和数据迁移一样的道理。

    当我们有新旧的两个系统时,新来的用户等信息直接落在新系统即可,

    这一措施保证了 ht[0] 包含的键值对数量会只减不增, 并随着 rehash 操作的执行而最终变成空表。


    一图胜千言

    (1)准备 rehash

    在这里插入图片描述

    (2)rehash index=0

    在这里插入图片描述

    (3)rehash index=1

    在这里插入图片描述

    (4)rehash index=2

    在这里插入图片描述

    (5)rehash index=3

    在这里插入图片描述

    (6)rehash 完成

    在这里插入图片描述


    缩容扩容的思考

    看完了上面的流程,不知道你对 rehash 是否有一个大概了思路呢?

    下面让我们来一起思考下几个缩扩容的问题。

    什么时候扩容呢

    什么时候判断?

    redis 在每次执行 put 操作的时候,就可以检查是否需要扩容。

    其实也很好理解,put 插入元素的时候,判断是否需要扩容,然后开始扩容,是直接的一种思路。

    留一个思考题:我们可以在其他的时候判断吗?

    redis 判断是否需要扩容的源码

    /* Expand the hash table if needed */
    static int _dictExpandIfNeeded(dict *d)
    {
        /* Incremental rehashing already in progress. Return. */
        // 如果正在进行渐进式扩容,则返回OK
        if (dictIsRehashing(d)) return DICT_OK;
    
        /* If the hash table is empty expand it to the initial size. */
        // 如果哈希表ht[0]的大小为0,则初始化字典
        if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);
    
        /* If we reached the 1:1 ratio, and we are allowed to resize the hash
         * table (global setting) or we should avoid it but the ratio between
         * elements/buckets is over the "safe" threshold, we resize doubling
         * the number of buckets. */
        /*
         * 如果哈希表ht[0]中保存的key个数与哈希表大小的比例已经达到1:1,即保存的节点数已经大于哈希表大小
         * 且redis服务当前允许执行rehash,或者保存的节点数与哈希表大小的比例超过了安全阈值(默认值为5)
         * 则将哈希表大小扩容为原来的两倍
         */
        if (d->ht[0].used >= d->ht[0].size &&
            (dict_can_resize ||
             d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
        {
            return dictExpand(d, d->ht[0].used*2);
        }
        return DICT_OK;
    }
    

    扩容的条件总结下来就是两句话:

    (1)服务器目前没有在执行 BGSAVE/BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 1;

    (2)服务器目前正在执行 BGSAVE/BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 5;

    这里其实体现了作者的一种设计思想:如果负载因子超过5,说明信息已经很多了,管你在不在保存,都要执行扩容,优先保证服务可用性。如果没那么高,那就等持久化完成再做 rehash。

    我们自己在实现的时候可以简化一下,比如只考虑情况2。


    扩容到原来的多少

    知道了什么时候应该开始扩容,但是要扩容到多大也是值得思考的一个问题。

    扩容的太小,会导致频繁扩容,浪费性能。

    扩容的太大,会导致资源的浪费。

    其实这个最好的方案是结合我们实际的业务,不过这部分对用户是透明的。

    一般是扩容为原来的两倍。


    为什么需要扩容

    我们在实现 ArrayList 的时候需要扩容,因为数据放不下了。

    我们知道 HashMap 的底层是数组 + 链表(红黑树)的数据结构。

    那么会存在放不下的情况吗?

    个人理解实际上不会。因为链表可以一直加下去。

    那为什么需要扩容呢?

    实际上更多的是处于性能的考虑。我们使用 HashMap 就是为了提升性能,如果一直不扩容,可以理解为元素都 hash 到相同的 bucket 上,这时就退化成了一个链表。

    这会导致查询等操作性能大大降低。


    什么时候缩容呢

    何时判断

    看了前面的扩容,我们比较直观地方式是在用户 remove 元素的时候执行是否需要缩容。

    不过 redis 并不完全等同于传统的 HashMap,还有数据的淘汰和过期,这些是对用户透明的。

    redis 采用的方式实际上是一个定时任务。

    个人理解内存缩容很重要,但是没有那么紧急,我们可以 1min 扫描一次,这样可以节省机器资源。

    实际工作中,一般 redis 的内存都是逐步上升的,或者稳定在一个范围内,很少去大批量删除数据。(除非数据搞错了,我就遇到过一次,数据同步错地方了)。

    所以数据删除,一般几分钟内给用户一个反馈就行。

    知其然,知其所以然。

    我们懂得了这个道理也就懂得了为什么有时候删除 redis 的几百万 keys,内存也不是直接降下来的原因。

    缩容的条件

    /* If the percentage of used slots in the HT reaches HASHTABLE_MIN_FILL
     * we resize the hash table to save memory */
    void tryResizeHashTables(int dbid) {
        if (htNeedsResize(server.db[dbid].dict))
            dictResize(server.db[dbid].dict);
        if (htNeedsResize(server.db[dbid].expires))
            dictResize(server.db[dbid].expires);
    }
    
    /* Hash table parameters */
    #define HASHTABLE_MIN_FILL        10      /* Minimal hash table fill 10% */
    int htNeedsResize(dict *dict) {
        long long size, used;
    
        size = dictSlots(dict);
        used = dictSize(dict);
        return (size > DICT_HT_INITIAL_SIZE &&
                (used*100/size < HASHTABLE_MIN_FILL));
    }
    
    /* Resize the table to the minimal size that contains all the elements,
     * but with the invariant of a USED/BUCKETS ratio near to <= 1 */
    int dictResize(dict *d)
    {
        int minimal;
    
        if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;
        minimal = d->ht[0].used;
        if (minimal < DICT_HT_INITIAL_SIZE)
            minimal = DICT_HT_INITIAL_SIZE;
        return dictExpand(d, minimal);
    }
    

    和扩容类似,不过这里的缩容比例不是 5 倍,而是当哈希表保存的key数量与哈希表的大小的比例小于 10% 时需要缩容。


    缩容到多少

    最简单的方式是直接变为原来的一半,不过这么做有时候也不是那么好用。

    redis 是缩容后的大小为第一个大于等于当前key数量的2的n次方

    这个可能不太好理解,举几个数字就懂了:

    在这里插入图片描述

    主要保障以下3点:

    (1)缩容之后,要大于等于 key 的数量

    (2)尽可能的小,节约内存

    (3)2 的倍数。

    第三个看过 HashMap 源码讲解的小伙伴应该深有体会。

    当然也不能太小,redis 限制的最小为 4。

    实际上如果 redis 中只放 4 个 key,实在是杀鸡用牛刀,一般不会这么小。

    我们在实现的时候,直接参考 jdk 好了,给个最小值限制 8。


    渐进式 ReHash实现的思考

    好了,扩容和缩容就聊到这里,那么这个渐进式 rehash 到底怎么一个渐进法

    扩容前

    不需要扩容时应该有至少需要初始化两个元素:

    hashtable[0] = new HashTable(size);
    hashIndex=-1;
    
    hashtable[1] = null;
    

    hashtable 中存储着当前的元素信息,hashIndex=-1 标识当前没有在进行扩容。


    扩容准备

    当需要扩容的时候,我们再去创建一个 hashtable[1],并且 size 是原来的 2倍。

    hashtable[0] = new HashTable(size);
    
    hashtable[1] = new HashTable(2 * size);
    
    hashIndex=-1;
    

    主要是为了节约内存,使用惰性初始化的方式创建 hashtable。


    扩容时

    调整 hashIndex=0…size,逐步去 rehash 到新的 hashtable[1]

    新的插入全部放入到 hashtable[1]


    扩容后

    扩容后我们应该把 hashtable[0] 的值更新为 hashtable[1],并且释放掉 hashtable[1] 的资源。

    并且设置 hashIndex=-1,标识已经 rehash 完成

    hashtable[0] = hashtable[1];
    hashIndex=-1;
    
    hashtable[1] = null;
    

    这样整体的实现思路就已经差不多了,光说不练假把式,我们下一节就来自己实现一个渐进式 rehash 的 HashMap。


    本文参考

    • 《Redis 设计与实现》

    本文小结

    本节我们对 redis rehash 的原理进行了讲解,其中也加入了不少自己的思考。下一节我们将一起手写一个渐进式 rehash 的 HashMap。

    展开全文
  • Redis之渐进式rehash

    2020-07-28 13:21:47
    渐进式 rehash 上一节说过, 扩展或收缩哈希表需要将ht[0]里面的所有键值对 rehash 到ht[1]里面, 但是, 这个 rehash 动作并不是一次性、集中式地完成的, 而是分多次、渐进式地完成的。 这样做的原因在于, 如果...

    渐进式 rehash

    上一节说过, 扩展或收缩哈希表需要将 ht[0] 里面的所有键值对 rehash 到 ht[1] 里面, 但是, 这个 rehash 动作并不是一次性、集中式地完成的, 而是分多次、渐进式地完成的。

    这样做的原因在于, 如果 ht[0] 里只保存着四个键值对, 那么服务器可以在瞬间就将这些键值对全部 rehash 到 ht[1] ; 但是, 如果哈希表里保存的键值对数量不是四个, 而是四百万、四千万甚至四亿个键值对, 那么要一次性将这些键值对全部 rehash 到 ht[1] 的话, 庞大的计算量可能会导致服务器在一段时间内停止服务。

    因此, 为了避免 rehash 对服务器性能造成影响, 服务器不是一次性将 ht[0] 里面的所有键值对全部 rehash 到 ht[1] , 而是分多次、渐进式地将 ht[0] 里面的键值对慢慢地 rehash 到 ht[1] 。

    以下是哈希表渐进式 rehash 的详细步骤:

    1. 为 ht[1] 分配空间, 让字典同时持有 ht[0] 和 ht[1] 两个哈希表。
    2. 在字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始。
    3. 在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增一。
    4. 随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。

    渐进式 rehash 的好处在于它采取分而治之的方式, 将 rehash 键值对所需的计算工作均滩到对字典的每个添加、删除、查找和更新操作上, 从而避免了集中式 rehash 而带来的庞大计算量。

    图 4-12 至图 4-17 展示了一次完整的渐进式 rehash 过程, 注意观察在整个 rehash 过程中, 字典的 rehashidx 属性是如何变化的。

    digraph {      label = "\n 图 4-12    准备开始 rehash";      rankdir = LR;      node [shape = record];      // 字典      dict [label = " <head> dict | type | privdata | <ht> ht | rehashidx \n -1 "];      // 哈希表      dictht0 [label = " <head> dictht | <table> table | <size> size \n 4 | <sizemask> sizemask \n 3 | <used> used \n 4"];      dictht1 [label = " <head> dictht | <table> table | <size> size \n 8 | <sizemask> sizemask \n 7 | <used> used \n 0"];      table0 [label = " <head> dictEntry*[4] | <0> 0 | <1> 1 | <2> 2 | <3> 3 "];      table1 [label = " <head> dictEntry*[8] | <0> 0 | <1> 1 | <2> 2 | ... | <7> 7 "];      // 哈希表节点      kv0 [label = " <head> dictEntry | { k0 | v0 } "];     kv1 [label = " <head> dictEntry | { k1 | v1 } "];     kv2 [label = " <head> dictEntry | { k2 | v2 } "];     kv3 [label = " <head> dictEntry | { k3 | v3 } "];      //      node [shape = plaintext, label = "NULL"];      //      dict:ht -> dictht0:head [label = "ht[0]"];     dict:ht -> dictht1:head [label = "ht[1]"];      dictht0:table -> table0:head;     dictht1:table -> table1:head;      table0:0 -> kv2:head -> null0;     table0:1 -> kv0:head -> null1;     table0:2 -> kv3:head -> null2;     table0:3 -> kv1:head -> null3;      table1:0 -> null10;     table1:1 -> null11;     table1:2 -> null12;     table1:7 -> null17;  }

    digraph {      label = "\n 图 4-13    rehash 索引 0 上的键值对";      rankdir = LR;      node [shape = record];      // 字典      dict [label = " <head> dict | type | privdata | <ht> ht | rehashidx \n 0 "];      // 哈希表      dictht0 [label = " <head> dictht | <table> table | <size> size \n 4 | <sizemask> sizemask \n 3 | <used> used \n 3"];      dictht1 [label = " <head> dictht | <table> table | <size> size \n 8 | <sizemask> sizemask \n 7 | <used> used \n 1"];      table0 [label = " <head> dictEntry*[4] | <0> 0 | <1> 1 | <2> 2 | <3> 3 "];      table1 [label = " <head> dictEntry*[8] | ... | <4> 4 | ... "];      // 哈希表节点      kv0 [label = " <head> dictEntry | { k0 | v0 } "];     kv1 [label = " <head> dictEntry | { k1 | v1 } "];     kv2 [label = " <head> dictEntry | { k2 | v2 } "];     kv3 [label = " <head> dictEntry | { k3 | v3 } "];      //      node [shape = plaintext, label = "NULL"];      //      dict:ht -> dictht0:head [label = "ht[0]"];     dict:ht -> dictht1:head [label = "ht[1]"];      dictht0:table -> table0:head;     dictht1:table -> table1:head;      table0:0 -> null0;     table0:1 -> kv0:head -> null1;     table0:2 -> kv3:head -> null2;     table0:3 -> kv1:head -> null3;      table1:4 -> kv2:head -> null14  }

    digraph {      label = "\n 图 4-14    rehash 索引 1 上的键值对";      rankdir = LR;      node [shape = record];      // 字典      dict [label = " <head> dict | type | privdata | <ht> ht | rehashidx \n 1 "];      // 哈希表      dictht0 [label = " <head> dictht | <table> table | <size> size \n 4 | <sizemask> sizemask \n 3 | <used> used \n 2"];      dictht1 [label = " <head> dictht | <table> table | <size> size \n 8 | <sizemask> sizemask \n 7 | <used> used \n 2"];      table0 [label = " <head> dictEntry*[4] | <0> 0 | <1> 1 | <2> 2 | <3> 3 "];      table1 [label = " <head> dictEntry*[8] | ... | <4> 4 | <5> 5 | ... "];      // 哈希表节点      kv0 [label = " <head> dictEntry | { k0 | v0 } "];     kv1 [label = " <head> dictEntry | { k1 | v1 } "];     kv2 [label = " <head> dictEntry | { k2 | v2 } "];     kv3 [label = " <head> dictEntry | { k3 | v3 } "];      //      node [shape = plaintext, label = "NULL"];      //      dict:ht -> dictht0:head [label = "ht[0]"];     dict:ht -> dictht1:head [label = "ht[1]"];      dictht0:table -> table0:head;     dictht1:table -> table1:head;      table0:0 -> null0;     table0:1 -> null1;     table0:2 -> kv3:head -> null2;     table0:3 -> kv1:head -> null3;      table1:4 -> kv2:head -> null14     table1:5 -> kv0:head -> null15;  }

    digraph {      label = "\n 图 4-15    rehash 索引 2 上的键值对";      rankdir = LR;      node [shape = record];      // 字典      dict [label = " <head> dict | type | privdata | <ht> ht | rehashidx \n 2 "];      // 哈希表      dictht0 [label = " <head> dictht | <table> table | <size> size \n 4 | <sizemask> sizemask \n 3 | <used> used \n 1"];      dictht1 [label = " <head> dictht | <table> table | <size> size \n 8 | <sizemask> sizemask \n 7 | <used> used \n 3"];      table0 [label = " <head> dictEntry*[4] | <0> 0 | <1> 1 | <2> 2 | <3> 3 "];      table1 [label = " <head> dictEntry*[8] | ... | <1> 1 | ... | <4> 4 | <5> 5 | ... "];      // 哈希表节点      kv0 [label = " <head> dictEntry | { k0 | v0 } "];     kv1 [label = " <head> dictEntry | { k1 | v1 } "];     kv2 [label = " <head> dictEntry | { k2 | v2 } "];     kv3 [label = " <head> dictEntry | { k3 | v3 } "];      //      node [shape = plaintext, label = "NULL"];      //      dict:ht -> dictht0:head [label = "ht[0]"];     dict:ht -> dictht1:head [label = "ht[1]"];      dictht0:table -> table0:head;     dictht1:table -> table1:head;      table0:0 -> null0;     table0:1 -> null1;     table0:2 -> null2;     table0:3 -> kv1:head -> null3;      table1:1 -> kv3:head -> null11;     table1:4 -> kv2:head -> null14     table1:5 -> kv0:head -> null15;  }

    digraph {      label = "\n 图 4-16    rehash 索引 3 上的键值对";      rankdir = LR;      node [shape = record];      // 字典      dict [label = " <head> dict | type | privdata | <ht> ht | rehashidx \n 3 "];      // 哈希表      dictht0 [label = " <head> dictht | <table> table | <size> size \n 4 | <sizemask> sizemask \n 3 | <used> used \n 0"];      dictht1 [label = " <head> dictht | <table> table | <size> size \n 8 | <sizemask> sizemask \n 7 | <used> used \n 4"];      table0 [label = " <head> dictEntry*[4] | <0> 0 | <1> 1 | <2> 2 | <3> 3 "];      table1 [label = " <head> dictEntry*[8] | ... | <1> 1 | ... | <4> 4 | <5> 5 | ... | <7> 7 "];      // 哈希表节点      kv0 [label = " <head> dictEntry | { k0 | v0 } "];     kv1 [label = " <head> dictEntry | { k1 | v1 } "];     kv2 [label = " <head> dictEntry | { k2 | v2 } "];     kv3 [label = " <head> dictEntry | { k3 | v3 } "];      //      node [shape = plaintext, label = "NULL"];      //      dict:ht -> dictht0:head [label = "ht[0]"];     dict:ht -> dictht1:head [label = "ht[1]"];      dictht0:table -> table0:head;     dictht1:table -> table1:head;      table0:0 -> null0;     table0:1 -> null1;     table0:2 -> null2;     table0:3 -> null3;      table1:1 -> kv3:head -> null11;     table1:4 -> kv2:head -> null14     table1:5 -> kv0:head -> null15;     table1:7 -> kv1:head -> null17;  }

    digraph {      label = "\n 图 4-17    rehash 执行完毕";      rankdir = LR;      node [shape = record];      // 字典      dict [label = " <head> dict | type | privdata | <ht> ht | rehashidx \n -1 "];      // 哈希表      dictht0 [label = " <head> dictht | <table> table | <size> size \n 8 | <sizemask> sizemask \n 7 | <used> used \n 4"];      dictht1 [label = " <head> dictht | <table> table | <size> size \n 0 | <sizemask> sizemask \n 0 | <used> used \n 0"];      table0 [label = " <head> dictEntry*[8] | ... | <1> 1 | ... | <4> 4 | <5> 5 | ... | <7> 7 "];      table1 [label = "NULL", shape = plaintext];      // 哈希表节点      kv0 [label = " <head> dictEntry | { k0 | v0 } "];     kv1 [label = " <head> dictEntry | { k1 | v1 } "];     kv2 [label = " <head> dictEntry | { k2 | v2 } "];     kv3 [label = " <head> dictEntry | { k3 | v3 } "];      //      node [shape = plaintext, label = "NULL"];      //      dict:ht -> dictht0:head [label = "ht[0]"];     dict:ht -> dictht1:head [label = "ht[1]"];      dictht0:table -> table0:head;     dictht1:table -> table1;      table0:1 -> kv3:head -> null11;     table0:4 -> kv2:head -> null14;     table0:5 -> kv0:head -> null15;     table0:7 -> kv1:head -> null17;  }

    渐进式 rehash 执行期间的哈希表操作

    因为在进行渐进式 rehash 的过程中, 字典会同时使用 ht[0] 和 ht[1] 两个哈希表, 所以在渐进式 rehash 进行期间, 字典的删除(delete)、查找(find)、更新(update)等操作会在两个哈希表上进行: 比如说, 要在字典里面查找一个键的话, 程序会先在 ht[0] 里面进行查找, 如果没找到的话, 就会继续到 ht[1] 里面进行查找, 诸如此类。

    另外, 在渐进式 rehash 执行期间, 新添加到字典的键值对一律会被保存到 ht[1] 里面, 而 ht[0] 则不再进行任何添加操作: 这一措施保证了 ht[0] 包含的键值对数量会只减不增, 并随着 rehash 操作的执行而最终变成空表。

     

    总结

    • 字典被广泛用于实现 Redis 的各种功能, 其中包括数据库和哈希键。
    • Redis 中的字典使用哈希表作为底层实现, 每个字典带有两个哈希表, 一个用于平时使用, 另一个仅在进行 rehash 时使用。
    • 当字典被用作数据库的底层实现, 或者哈希键的底层实现时, Redis 使用 MurmurHash2 算法来计算键的哈希值。
    • 哈希表使用链地址法来解决键冲突, 被分配到同一个索引上的多个键值对会连接成一个单向链表。
    • 在对哈希表进行扩展或者收缩操作时, 程序需要将现有哈希表包含的所有键值对 rehash 到新哈希表里面, 并且这个 rehash 过程并不是一次性地完成的, 而是渐进式地完成的。
    展开全文
  • Redis的rehash机制

    千次阅读 2020-03-14 13:00:10
    在redis的具体实现中,使用了一种叫做渐进式哈希(rehashing)的机制来提高字典的缩放效率,避免 rehash 对服务器性能造成影响,渐进式 rehash 的好处在于它采取分而治之的方式, 将 rehash 键值对所需的计算工作均摊...
  • 3、rehash 步骤 扩展和收缩哈希表的工作可以通过执行rehash(重新散列)操作来完成,Redis 中执行 rehash 的步骤如下: 给哈希表2 ( ht[1] )分配的空间。这个哈希表的空间大小取决于要执行的操作,以及ht[0]当前...
  • redis之rehash原理

    2020-12-11 19:25:41
    hash 值的低位,高位都被设置为0,接下来我们看看 rehash 前后元素槽位的变化 假设当前的字典的数组长度由 8 位扩容到 16 位,那么 3 号槽位 011 将会被 rehash 到 3 号槽位和 11 号槽位,也就是说该槽位链表中大约...
  • Redis之rehash操作

    2020-07-28 12:16:49
    rehash 随着操作的不断执行, 哈希...扩展和收缩哈希表的工作可以通过执行 rehash (重新散列)操作来完成, Redis 对字典的哈希表执行 rehash 的步骤如下: 为字典的ht[1]哈希表分配空间, 这个哈希表的空间大小..
  • redis渐进式rehash机制

    千次阅读 2019-06-26 09:51:28
    在redis的具体实现中,使用了一种叫做渐进式哈希(rehashing)的机制来提高字典的缩放效率,避免 rehash 对服务器性能造成影响,渐进式 rehash 的好处在于它采取分而治之的方式, 将 rehash 键值对所需的计算工作均摊...
  • 在redis的具体实现中,使用了一种叫做渐进式哈希(rehashing)的机制来提高字典的缩放效率,避免 rehash 对服务器性能造成影响,渐进式 rehash 的好处在于它采取分而治之的方式, 将 rehash 键值对所需的计算工作均摊...
  • 1.Redis的rehash的策略 背景:redis字典(hash表)当数据越来越多的时候,就会发生扩容,也就是rehash。 对比:Java中的HashMap,当数据数量达到阈值的时候(0.75),就会发生rehash,hash表长度变为原来的2倍,将原hash...
  • 目录 一、HashMap的数据结构 解决hash冲突的办法 二、源码分析 1.位桶数组 2.数组元素Node实现了Entry接口,v> 3.HashMap如何put(key,value) 4.HashMap如何getValue...三、再谈ReHash 单线程下的ReHash ...
  • 《闲扯Redis八》Redis字典的哈希表执行Rehash过程分析 Redis 渐进式 rehash 源码分析 总结: 结构: /* * 字典 */ typedef struct dict { // 类型特定函数 dictType *type; // 私有数据 void *privdata; // ...
  • 渐进式rehash 数量较少的情况下,rehash可以短时间内一次性完成,几百个、几千个对现在的计算机来说这都不是事儿。但是如果是百万、千万、亿以上这个级别。可能就需要一段时间了。而这个过程中,又要保证服务不中断...
  • } 从上边我们可以看出,在redis的每个数据库的定时任务中会出发rehash操作,每次rehahs操作会触发1ms的rehash操作,也就是所即便是定时任务的rehash操作,也只会最多阻塞redis其他命令1ms。而在服务器定时任务的...
  • 2、渐进式rehash 1、缩容 扩容 随着redis的操作的不断执行,哈希表保存的键值会逐渐地增多或者减少,为了让哈希表的负载因子(ratio)维持在一个合理的范围之内,当哈希表保存的键值对数量太多或者太少时,程序...
  • 1、准备rehash 这时候是ga
  • rehash3. 哈希表的扩展和收缩4. 解决哈希冲突5. 渐进式rehash 1. hash算法 学过数据结构的知道,当要将一个新的键值对添加到字典里面时, 程序需要先根据键值对的键计算出哈希值和索引值, 然后再根据索引值, 将...
  • redis的rehash工作原理

    千次阅读 2019-06-18 09:38:44
    为了减少哈西表中的地址冲突次数,redis会增加键值空间,重新定义键值对的映射地址,也就是进行所谓的rehash。本文主要通过redis源码分析rehash原理。主要解决如下几个问题。 1,redis中hash表的数据结构是怎样的?...
  • 文章目录 前言 一、哈希表 二、为什么进行rehash 三、渐进式rehash 四、源码实现 总结 前言 本章会讲述Redis到底是怎样设计rehash来减少对主线程的阻塞,它的设计实现绝对会让你大开眼界,在面试官面前展示你的知识...
  • Redis 源码解读之 Rehash 的调用时机

    千次阅读 热门讨论 2021-03-10 09:37:00
    Redis 源码解读之 Rehash 的调用时机 背景和问题 本文想要解决的问题 什么时机触发 Rehash 操作 什么时机实际执行 Rehash 函数 结论 背景问题的结论 源码分析 渐进式 Rehash rehash 算法描述 触发 Rehash ...
  • 前面讲到了Redis底层数据结构dict(字典)的结构设计,本篇文章继续深入了解dict(字典)数据结构的扩容过程(即rehash过程);如果不了解dict(字典)的数据结构,可以先看看这篇文章:图解Redis底层数据结构实现原理。 ...
  • ② 集中式重新rehash是非常耗性能的,hashMap中rehash的优化点在哪里? 扩容 以前下面这段代码我没有重点看,只知道这是在扩容; 今天重点看了下,不过红黑树没看: final Node<K,V>[] resize() { Node<K,...
  • Redis渐进式rehash的原理是什么? 下面就来深入的解析这些问题。 一、字典 字典是Redis中存在最广泛的一种数据结构不仅在哈希对象,集合对象和有序结合对象中都有使用,而且Redis所有的Key,Value都是存在db->dict...
  • 浅谈Redis中的Rehash机制

    万次阅读 多人点赞 2018-05-23 10:45:29
    /* More to rehash... */ return 1 ; } 了解一个函数功能最好的入口就是它的注释。我们可以大致了解到: rehash 是以 bucket(桶) 为基本单位进行渐进式的数据迁移的,每步完成一个 bucket 的迁移,...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 44,905
精华内容 17,962
关键字:

rehash