0%

## redis数据结构介绍五-第五部分 对象

Posted on 2020-01-20 In Redis , 数据结构 , C , 源码 , Redis Views: Disqus:

前面说了这么些数据结构，其实大家对于 redis 最初的印象应该就是个 key-value 的缓存，类似于 memcache，redis 其实也是个 key-value，key 还是一样的字符串，或者说就是用 redis 自己的动态字符串实现，但是 value 其实就是前面说的那些数据结构，差不多快说完了，还有个 quicklist 后面还有一篇，这里先介绍下 redis 对于这些不同类型的 value 是怎么实现的，首先看下 redisObject 的源码头文件

    /* The actual Redis Object */
    #define OBJ_STRING 0    /* String object. */
    #define OBJ_LIST 1      /* List object. */
    #define OBJ_SET 2       /* Set object. */
    #define OBJ_ZSET 3      /* Sorted set object. */
    #define OBJ_HASH 4      /* Hash object. */
    /*
     * Objects encoding. Some kind of objects like Strings and Hashes can be
     * internally represented in multiple ways. The 'encoding' field of the object
     * is set to one of this fields for this object. */
    #define OBJ_ENCODING_RAW 0     /* Raw representation */
    #define OBJ_ENCODING_INT 1     /* Encoded as integer */
    #define OBJ_ENCODING_HT 2      /* Encoded as hash table */
    #define OBJ_ENCODING_ZIPMAP 3  /* Encoded as zipmap */
    #define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
    #define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
    #define OBJ_ENCODING_INTSET 6  /* Encoded as intset */
    #define OBJ_ENCODING_SKIPLIST 7  /* Encoded as skiplist */
    #define OBJ_ENCODING_EMBSTR 8  /* Embedded sds string encoding */
    #define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
    #define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */

    #define LRU_BITS 24
    #define LRU_CLOCK_MAX ((1<<LRU_BITS)-1) /* Max value of obj->lru */
    #define LRU_CLOCK_RESOLUTION 1000 /* LRU clock resolution in ms */

    #define OBJ_SHARED_REFCOUNT INT_MAX
    typedef struct redisObject {
        unsigned type:4;
        unsigned encoding:4;
        unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                                * LFU data (least significant 8 bits frequency
                                * and most significant 16 bits access time). */
        int refcount;
        void *ptr;
    } robj;

主体结构就是这个 redisObject，

- type： 字段表示对象的类型，它对应的就是 redis 的对外暴露的，或者说用户可以使用的五种类型，OBJ_STRING, OBJ_LIST, OBJ_SET, OBJ_ZSET, OBJ_HASH
- encoding： 字段表示这个对象在 redis 内部的编码方式，由OBJ_ENCODING_开头的 11 种
- lru: 做LRU替换算法用，占24个bit
- refcount: 引用计数。它允许robj对象在某些情况下被共享。
- ptr: 指向底层实现数据结构的指针
  当 type 是 OBJ_STRING 时，表示类型是个 string，它的编码方式 encoding 可能有 OBJ_ENCODING_RAW，OBJ_ENCODING_INT，OBJ_ENCODING_EMBSTR 三种
  当 type 是 OBJ_LIST 时，表示类型是 list，它的编码方式 encoding 是 OBJ_ENCODING_QUICKLIST，对于早一些的版本，2.2这种可能还会使用 OBJ_ENCODING_ZIPLIST，OBJ_ENCODING_LINKEDLIST
  当 type 是 OBJ_SET 时，是个集合，但是得看具体元素的类型，有可能使用整数集合，OBJ_ENCODING_INTSET, 如果元素不全是整型或者数量超过一定限制，那么编码就是 OBJ_ENCODING_HT hash table 了
  当 type 是 OBJ_ZSET 时，是个有序集合，它底层有可能使用的是 OBJ_ENCODING_ZIPLIST 或者 OBJ_ENCODING_SKIPLIST
  当 type 是 OBJ_HASH 时，一开始也是 OBJ_ENCODING_ZIPLIST，然后当数据量大于 hash_max_ziplist_entries 时会转成 OBJ_ENCODING_HT

## redis数据结构介绍四-第四部分 压缩表

Posted on 2020-01-19 In Redis , 数据结构 , C , 源码 , Redis Views: Disqus:

在 redis 中还有一类表型数据结构叫压缩表，ziplist，它的目的是替代链表，链表是个很容易理解的数据结构，双向链表有前后指针，有带头结点的有的不带，但是链表有个比较大的问题是相对于普通的数组，它的内存不连续，碎片化的存储，内存利用效率不高，而且指针寻址相对于直接使用偏移量的话，也有一定的效率劣势，当然这不是主要的原因，ziplist 设计的主要目的是让链表的内存使用更高效

> The ziplist is a specially encoded dually linked list that is designed to be very memory efficient.
> 这是摘自 redis 源码中ziplist.c 文件的注释，也说明了原因，它的大概结构是这样子

    <zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>

其中
`<zlbytes>`表示 ziplist 占用的字节总数，类型是uint32_t，32 位的无符号整型，当然表示的字节数也包含自己本身占用的 4 个
`<zltail>` 类型也是是uint32_t，表示ziplist表中最后一项（entry）在ziplist中的偏移字节数。`<zltail>`的存在，使得我们可以很方便地找到最后一项（不用遍历整个ziplist），从而可以在ziplist尾端快速地执行push或pop操作。
`<uint16_t zllen>` 表示ziplist 中的数据项个数，因为是 16 位，所以当数量超过所能表示的最大的数量，它的 16 位全会置为 1，但是真实的数量需要遍历整个 ziplist 才能知道
`<entry>`是具体的数据项，后面解释
`<zlend>` ziplist 的最后一个字节，固定是255。
再看一下`<entry>`中的具体结构，

    <prevlen> <encoding> <entry-data>

首先这个`<prevlen>`有两种情况，一种是前面的元素的长度，如果是小于等于 253的时候就用一个uint8_t 来表示前一元素的长度，如果大于的话他将占用五个字节，第一个字节是 254，即表示这个字节已经表示不下了，需要后面的四个字节帮忙表示
`<encoding>`这个就比较复杂，把源码的注释放下面先看下

    * |00pppppp| - 1 byte
    *      String value with length less than or equal to 63 bytes (6 bits).
    *      "pppppp" represents the unsigned 6 bit length.
    * |01pppppp|qqqqqqqq| - 2 bytes
    *      String value with length less than or equal to 16383 bytes (14 bits).
    *      IMPORTANT: The 14 bit number is stored in big endian.
    * |10000000|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt| - 5 bytes
    *      String value with length greater than or equal to 16384 bytes.
    *      Only the 4 bytes following the first byte represents the length
    *      up to 32^2-1. The 6 lower bits of the first byte are not used and
    *      are set to zero.
    *      IMPORTANT: The 32 bit number is stored in big endian.
    * |11000000| - 3 bytes
    *      Integer encoded as int16_t (2 bytes).
    * |11010000| - 5 bytes
    *      Integer encoded as int32_t (4 bytes).
    * |11100000| - 9 bytes
    *      Integer encoded as int64_t (8 bytes).
    * |11110000| - 4 bytes
    *      Integer encoded as 24 bit signed (3 bytes).
    * |11111110| - 2 bytes
    *      Integer encoded as 8 bit signed (1 byte).
    * |1111xxxx| - (with xxxx between 0000 and 1101) immediate 4 bit integer.
    *      Unsigned integer from 0 to 12. The encoded value is actually from
    *      1 to 13 because 0000 and 1111 can not be used, so 1 should be
    *      subtracted from the encoded 4 bit value to obtain the right value.
    * |11111111| - End of ziplist special entry.

首先如果 encoding 的前两位是 00 的话代表这个元素是个 6 位的字符串，即直接将数据保存在 encoding 中，不消耗额外的`<entry-data>`，如果前两位是 01 的话表示是个 14 位的字符串，如果是 10 的话表示encoding 块之后的四个字节是存放字符串类型的数据，encoding 的剩余 6 位置 0。
如果 encoding 的前两位是 11 的话表示这是个整型，具体的如果后两位是00的话，表示后面是个2字节的 int16_t 类型，如果是01的话，后面是个4字节的int32_t,如果是10的话后面是8字节的int64_t,如果是 11 的话后面是 3 字节的有符号整型，这些都要最后 4 位都是 0 的情况噢
剩下当是`11111110`时，则表示是一个1 字节的有符号数，如果是 `1111xxxx`，其中`xxxx`在0000 到 1101 表示实际的 1 到 13，为啥呢，因为 0000 前面已经用过了，而 1110 跟 1111 也都有用了。
看个具体的例子(上下有点对不齐，将就看)

    [0f 00 00 00] [0c 00 00 00] [02 00] [00 f3] [02 f6] [ff]
    |**zlbytes***|  |***zltail***|  |*zllen*|  |entry1 entry2|  |zlend|

第一部分代表整个 ziplist 有 15 个字节，zlbytes 自己占了 4 个 zltail 表示最后一个元素的偏移量，第 13 个字节起，zllen 表示有 2 个元素，第一个元素是`00f3`,00表示前一个元素长度是 0，本来前面就没元素(不过不知道这个能不能优化这一字节)，然后是 f3，换成二进制就是11110011,对照上面的注释，是落在|1111xxxx|这个类型里，注意这个其实是用 0001 到 1101 也就是 1到 13 来表示 0到 12，所以 f3 应该就是 2，第一个元素是 2，第二个元素呢，02 代表前一个元素也就是刚才说的这个，占用 2 字节，f6 展开也是刚才的类型，实际是 5，ff 表示 ziplist 的结尾，所以这个 ziplist 里面是两个元素，2 跟 5

## redis数据结构介绍三-第三部分 整数集合

Posted on 2020-01-10 In Redis , 数据结构 , C , 源码 , Redis Views: Disqus:

redis中对于 set 其实有两种处理，对于元素均为整型，并且元素数目较少时，使用 intset 作为底层数据结构，否则使用 dict 作为底层数据结构，先看一下代码先

    typedef struct intset {
        // 编码方式
        uint32_t encoding;
        // 集合包含的元素数量
        uint32_t length;
        // 保存元素的数组
        int8_t contents[];
    } intset;

    /* Note that these encodings are ordered, so:
     * INTSET_ENC_INT16 < INTSET_ENC_INT32 < INTSET_ENC_INT64. */
    #define INTSET_ENC_INT16 (sizeof(int16_t))
    #define INTSET_ENC_INT32 (sizeof(int32_t))
    #define INTSET_ENC_INT64 (sizeof(int64_t))

一眼看，为啥整型还需要编码，然后 int8_t 怎么能存下大整形呢，带着这些疑问，我们一步步分析下去，这里的编码其实指的是这个整型集合里存的究竟是多大的整型，16 位，还是 32 位，还是 64 位，结构体下面的宏定义就是表示了 encoding 的可能取值，INTSET_ENC_INT16 表示每个元素用2个字节存储，INTSET_ENC_INT32 表示每个元素用4个字节存储，INTSET_ENC_INT64 表示每个元素用8个字节存储。因此，intset中存储的整数最多只能占用64bit。length 就是正常的表示集合中元素的数量。最奇怪的应该就是这个 contents 了，是个 int8_t 的数组，那放毛线数据啊，最小的都有 16 位，这里我在看代码和《redis 设计与实现》的时候也有点懵逼，后来查了下发现这是个比较取巧的用法，这里我用自己的理解表述一下，先看看 8，16，32，64 的关系，一眼看就知道都是 2 的 N 次，并且呈两倍关系，而且 8 位刚好一个字节，所以呢其实这里的contents 不是个常规意义上的 int8_t 类型的数组，而是个柔性数组。看下 wiki 的定义

> Flexible array members1 were introduced in the C99 standard of the C programming language (in particular, in section §6.7.2.1, item 16, page 103).2 It is a member of a struct, which is an array without a given dimension. It must be the last member of such a struct and it must be accompanied by at least one other member, as in the following example:

    struct vectord {
        size_t len;
        double arr[]; // the flexible array member must be last
    };

在初始化这个 intset 的时候，这个contents数组是不占用空间的，后面的反正用到了申请，那么这里就有一个问题，给出了三种可能的 encoding 值，他们能随便换吗，显然不行，首先在 intset 中数据的存放是有序的，这个有部分原因是方便二分查找，然后存放数据其实随着数据的大小不同会有一个升级的过程，看下图
新创建的intset只有一个header，总共8个字节。其中encoding = 2, length = 0, 类型都是uint32_t，各占 4 字节，添加15, 5两个元素之后，因为它们是比较小的整数，都能使用2个字节表示，所以encoding不变，值还是2，也就是默认的 `INTSET_ENC_INT16`，当添加32768的时候，它不再能用2个字节来表示了（2个字节能表达的数据范围是-215~215-1，而32768等于215，超出范围了），因此encoding必须升级到INTSET_ENC_INT32（值为4），即用4个字节表示一个元素。在添加每个元素的过程中，intset始终保持从小到大有序。与ziplist类似，intset也是按小端（little endian）模式存储的（参见维基百科词条Endianness）。比如，在上图中intset添加完所有数据之后，表示encoding字段的4个字节应该解释成0x00000004，而第4个数据应该解释成0x00008000 = 32768

## redis数据结构介绍二-第二部分 跳表

Posted on 2020-01-04 In Redis , 数据结构 , C , 源码 , Redis Views: Disqus:

## 跳表 skiplist

跳表是个在我们日常的代码中不太常用到的数据结构，相对来讲就没有像数组，链表，字典，散列，树等结构那么熟悉，所以就从头开始分析下，首先是链表，跳表跟链表都有个表字（太硬扯了我🤦‍♀️），注意这是个有序链表
如上图，在这个链表里如果我要找到 23，是不是我需要从3，5，9开始一直往后找直到找到 23，也就是说时间复杂度是 O(N),N 的一次幂复杂度，那么我们来看看第二个
这个结构跟原先有点不一样，它给链表中偶数位的节点又加了一个指针把它们链接起来，这样子当我们要寻找 23 的时候就可以从原来的一个个往下找变成跳着找，先找到 5，然后是 10，接着是 19，然后是 28，这时候发现 28 比 23 大了，那我在退回到 19，然后从下一层原来的链表往前找，
这里毛估估是不是前面的节点我就少找了一半,有那么点二分法的意思。
前面的其实是跳表的引子，真正的跳表其实不是这样，因为上面的其实有个比较大的问题，就是插入一个元素后需要调整每个元素的指针，在 redis 中的跳表其实是做了个随机层数的优化，因为沿着前面的例子，其实当数据量很大的时候，是不是层数越多，其查询效率越高，但是随着层数变多，要保持这种严格的层数规则其实也会增大处理复杂度，所以 redis 插入每个元素的时候都是使用随机的方式，看一眼代码

    /* ZSETs use a specialized version of Skiplists */
    typedef struct zskiplistNode {
        sds ele;
        double score;
        struct zskiplistNode *backward;
        struct zskiplistLevel {
            struct zskiplistNode *forward;
            unsigned long span;
        } level[];
    } zskiplistNode;

    typedef struct zskiplist {
        struct zskiplistNode *header, *tail;
        unsigned long length;
        int level;
    } zskiplist;

    typedef struct zset {
        dict *dict;
        zskiplist *zsl;
    } zset;

忘了说了，redis 是把 skiplist 跳表用在 zset 里，zset 是个有序的集合，可以看到 zskiplist 就是个跳表的结构，里面用 header 保存跳表的表头，tail 保存表尾，还有长度和最大层级，具体的跳表节点元素使用 zskiplistNode 表示，里面包含了 sds 类型的元素值，double 类型的分值，用来排序，一个 backward 后向指针和一个 zskiplistLevel 数组，每个 level 包含了一个前向指针，和一个 span，span 表示的是跳表前向指针的跨度，这里再补充一点，前面说了为了灵活这个跳表的新增修改，redis 使用了随机层高的方式插入新节点，但是如果所有节点都随机到很高的层级或者所有都很低的话，跳表的效率优势就会减小，所以 redis 使用了个小技巧，贴下代码

    #define ZSKIPLIST_P 0.25      /* Skiplist P = 1/4 */
    int zslRandomLevel(void) {
        int level = 1;
        while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
            level += 1;
        return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
    }

当随机值跟0xFFFF进行与操作小于ZSKIPLIST_P * 0xFFFF时才会增大 level 的值，因此保持了一个相对递减的概率
可以简单分析下，当 random() 的值小于 0xFFFF 的 1/4,才会 level + 1，就意味着当有 1 - 1/4也就是3/4的概率是直接跳出，所以一层的概率是3/4,也就是 1-P，二层的概率是 P*(1-P),三层的概率是 P² * (1-P) 依次递推。

## redis数据结构介绍-第一部分 SDS，链表，字典

Posted on 2019-12-26 In Redis , 数据结构 , C , 源码 , Redis Views: Disqus:

redis是现在服务端很常用的缓存中间件，其实原来还有`memcache`之类的竞品，但是现在貌似 redis 快一统江湖，这里当然不是在吹，只是个人角度的一个感觉，不权威只是主观感觉。
redis 主要有五种数据结构，`Strings`，`Lists`，`Sets`，`Hashes`，`Sorted Sets`，这五种数据结构先简单介绍下，`Strings`类型的其实就是我们最常用的 key-value，实际开发中也会用的最多；`Lists`是列表，这个有些会用来做队列，因为 redis 目前常用的版本支持丰富的列表操作；还有是`Sets`集合，这个主要的特点就是集合中元素不重复，可以用在有这类需求的场景里；`Hashes`是叫散列，类似于 Python 中的字典结构；还有就是`Sorted Sets`这个是个有序集合；一眼看这些其实没啥特别的，除了最后这个有序集合，不过去了解背后的实现方式还是比较有意思的。

## SDS 简单动态字符串

先从`Strings`开始说，了解过 C 语言的应该知道，C 语言中的字符串其实是个 `char[]` 字符数组，redis 也不例外，只是最开始的版本就对这个做了一丢丢的优化，而正是这一丢丢的优化，让这个 redis 的使用效率提升了数倍

    struct sdshdr {
        // 字符串长度
        int len;
        // 字符串空余字符数
        int free;
        // 字符串内容
        char buf[];
    };

这里引用了 redis 在 github 上最早的 2.2 版本的代码，代码路径是`https://github.com/antirez/redis/blob/2.2/src/sds.h`,可以看到这个结构体里只有仨元素，两个 int 型和一个 char 型数组，两个 int 型其实就是我说的优化，因为 C 语言本身的字符串数组，有两个问题，一个是要知道它实际已被占用的长度，需要去遍历这个数组，第二个就是比较容易踩坑的是遍历的时候要注意它有个以`\0`作为结尾的特点；通过上面的两个 int 型参数，一个是知道字符串目前的长度，一个是知道字符串还剩余多少位空间，这样子坐着两个操作从 `O(N)`简化到了`O(1)`了，还有第二个 free 还有个比较重要的作用就是能防止 C 字符串的溢出问题，在存储之前可以先判断 free 长度，如果长度不够就先扩容了，先介绍到这，这个系列可以写蛮多的，慢慢介绍吧

## 链表

链表是比较常见的数据结构了，但是因为 redis 是用 C 写的，所以在不依赖第三方库的情况下只能自己写一个了，redis 的链表是个有头的链表，而且是无环的，具体的结构我也找了 github 上最早版本的代码

    typedef struct listNode {
        // 前置节点
        struct listNode *prev;
        // 后置节点
        struct listNode *next;
        // 值
        void *value;
    } listNode;

    typedef struct list {
        // 链表表头
        listNode *head;
        // 当前节点，也可以说是最后节点
        listNode *tail;
        // 节点复制函数
        void *(*dup)(void *ptr);
        // 节点值释放函数
        void (*free)(void *ptr);
        // 节点值比较函数
        int (*match)(void *ptr, void *key);
        // 链表包含的节点数量
        unsigned int len;
    } list;

代码地址是这个`https://github.com/antirez/redis/blob/2.2/src/adlist.h`
可以看下节点是由listNode承载的，包括值和一个指向前节点跟一个指向后一节点的两个指针，然后值是 void 指针类型，所以可以承载不同类型的值
然后是 list结构用来承载一个链表，包含了表头，和表尾，复制函数，释放函数和比较函数，还有链表长度，因为包含了前两个节点，找到表尾节点跟表头都是 `O(1)`的时间复杂度，还有节点数量，其实这个跟 SDS 是同一个做法，就是空间换时间，这也是写代码里比较常见的做法，以此让一些高频的操作提速。

## 字典

字典也是个常用的数据结构，其实只是叫法不同，数据结构中叫 hash 散列，Java 中叫 Map，PHP 中是数组 array，Python 中也叫字典 dict，因为纯 C 语言本身不带这些数据结构，所以这也是个痛并快乐着的过程，享受 C 语言的高性能的同时也要接受它只提供了语言的基本功能的现实，各种轮子都需要自己造，redis 同样实现了自己的字典
下面来看看代码

    typedef struct dictEntry {
        void *key;
        void *val;
        struct dictEntry *next;
    } dictEntry;

    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;

    /* 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 **table;
        unsigned long size;
        unsigned long sizemask;
        unsigned long used;
    } dictht;

    typedef struct dict {
        dictType *type;
        void *privdata;
        dictht ht[2];
        int rehashidx; /* rehashing not in progress if rehashidx == -1 */
        int iterators; /* number of iterators currently running */
    } dict;

看了下这个 2.2 版本的代码跟最新版的其实也差的不是很多，所以还是照旧用老代码，可以看到上面四个结构体中，其实只有三个是存储数据用的，dictType 是用来放操作函数的，那么三个存放数据的结构体分别是干嘛的，这时候感觉需要一个图来说明比较好，稍等，我去画个图~
这个图看着应该比较清楚这些都是用来干嘛的了，dict 是我们的主体结构，它有一个指向 dictType 的指针，这里面包含了字典的操作函数，然后是一个私有数据指针，接下来是一个 dictht 的数组，包含两个dictht，这个就是用来存数据的了，然后是 rehashidx 表示重哈希的状态，当是-1 的时候表示当前没有重哈希，iterators 表示正在遍历的迭代器的数量。
首先说说为啥需要有两个 dictht，这是因为字典 dict 这个数据结构随着数据量的增减，会需要在中途做扩容或者缩容操作，如果只有一个的话，对它进行扩容缩容时会影响正常的访问和修改操作，或者说保证正常查询，修改的正确性会比较复杂，并且因为需要高效利用空间，不能一下子申请一个非常大的空间来存很少的数据。当 dict 中 dictht 中的数据量超过 size 的时候负载就超过了 1，就需要进行扩容，这里的其实跟 Java 中的 HashMap 比较类似，超过一定的负载之后进行扩容。这里为啥 size 会超过 1 呢，可能有部分不了解这类结构的同学会比较奇怪，其实就是上图中画的，在数据结构中对于散列的冲突有几类解决方法，比如转换成链表，二次散列，找下个空槽等，这里就使用了链表法，或者说拉链法。当一个新元素通过 hashFunction 得出的 key 跟 sizemask 取模之后的值相同了，那就将其放在原来的节点之前，变成链表挂在数组 dictht.table下面，放在原有节点前是考虑到可能会优先访问。
忘了说明下 dictht 跟 dictEntry 的关系了，dictht 就是个哈希表，它里面是个dictEntry 的二维数组，而 dictEntry 是个包含了 key-value 结构之外还有一个 next 指针，因此可以将哈希冲突的以链表的形式保存下来。
在重点说下重哈希，可能同样写 Java 的同学对这个比较有感觉，跟 HashMap 一样，会以 2 的 N 次方进行扩容，那么扩容的方法就会比较简单，每个键重哈希要不就在原来这个槽，要不就在原来的槽加原 dictht.size 的位置；然后是重头戏，具体是怎么做扩容呢，其实这里就把第二个 ht 用上了，其实这两个hashtable 的具体作用有点类似于 jvm 中的两个 survival 区，但是又不全一样，因为 redis 在扩容的时候是采用的渐进式地重哈希，什么叫渐进式的呢，就是它不是像 jvm 那种标记复制的模式直接将一个 eden 区和原来的 survival 区存活的对象复制到另一个 survival 区，而是在每一次添加，删除，查找或者更新操作时，都会额外的帮忙搬运一部分的原 dictht 中的数据，这里会根据 rehashidx 的值来判断，如果是-1 表示并没有在重哈希中，如果是 0 表示开始重哈希了，然后rehashidx 还会随着每次的帮忙搬运往上加，但全部被搬运完成后 rehashidx 又变回了-1，又可以扯到Java 中的 Concurrent HashMap, 他在扩容的时候也使用了类似的操作。