redis源码阅读—dict(字典结构)

前言

Redis 的字典使用哈希表作为底层实现, 一个哈希表里面可以有多个哈希表节点, 而每个哈希表节点就保存了字典中的一个键值对。
接下来将介绍 Redis 的哈希表、哈希表节点、以及字典的实现,包括字典实现的源码分析。

常用API介绍

#hset  插入数据
hset myhash name zhangsan age 22

#hget 获取数据
hget myhash name

#hgetall 获取所有数据
hgetall myhash

#hkeys 获取所有的key
hkeys myhash

#hvals 获取所有的值
hvals myhash

#hlen 获取长度
hlen myhash

逻辑结构图

由于在C语言中没有字典这种数据类型,所以redis自己构建了这种结构

首先dict有四个部分组成,分别是dictType(类型,不咋重要),dictht(核心),rehashidx(渐进式hash的标志),iterators(迭代器),这里面最重要的就是dicthtrehashidx

接下来是dictht,其有两个数组构成,一个是真正的数据存储位置,还有一个用于hash过程,包括的变量分别是真正的数据table和一些常见变量。

最后数据节点,和上篇说的双向链表一样,每个节点都有next指针,方便指向下一个节点,这样目的是为了解决hash碰撞。具体的可以看下图。

在这里插入图片描述

结构定义

字典

我们先看字典结构体dict,其包括四个部分,重点是dictht[2](真正的数据)和rehashidx(渐进式hash的标志)。具体图如下。
在这里插入图片描述

//字典结构体
typedef struct dict {
	// 类型 包括一些自定义函数 这些自定义函数使得key和value能够存储
    dictType *type;
    // 私有数据
    void *privdata;
    // 两张hash表
    dictht ht[2];
    // 渐进式hash标记  如果为-1  说明没有在hash
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    // 正在迭代的迭代器数量
    unsigned long iterators; /* number of iterators currently running */
} dict;
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;

在这里插入图片描述

哈希表

dictht主要包括四个部分,1是真正的数据dictEntry类型的数组,里面存放的是数据节点;2是数组长度size;3是进行hash运算的参数sizemask(掩码),只要记住等于size-1;4是数据节点数量used,当前有多少个数据节点。
在这里插入图片描述

typedef struct dictht {
    //存放一个数组的地址,数组存放着哈希表节点dictEntry的地址。
    dictEntry **table;     
    //哈希表table的大小,初始化大小为4
    unsigned long size;     
    //用于将哈希值映射到table的位置索引。它的值总是等于(size-1)。
    unsigned long sizemask; 
    //记录哈希表已有的节点(键值对)数量。
    unsigned long used;     
} dictht;

在这里插入图片描述

哈希表节点

在这里插入图片描述

typedef struct dictEntry {
    void *key; //键
    union {   
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v; //值 
    struct dictEntry *next; //指向下一个元素指针
} dictEntry;

在这里插入图片描述

在分析hash之前想一个问题

提一个问题:dictht[2]为什么会要2个数组存放,真正的数据只要一个数组就够了?
这其实和Java的HashMap相似,都是数据加链表的结构,随着数据量的增加,hash碰撞发生的就越频繁,每个数组后面的链表就越长,整个链表显得非常累赘。如果业务需要大量查询操作,因为是链表,只能从头部开始查询,等一个数组的链表全部查询完才能开始下一个数组,这样查询时间将无线拉长。

这无疑是要进行扩容,所以第一个数组存放真正的数据,第二个数组用于扩容用。第一个数组中的节点经过hash运算映射到第二个数组上,然后依次进行。那么过程中还能对外提供服务吗?答案是可以的,因为他可以随时停止,这就到了下一个变量rehashidx。

rehashidx其实是一个标志量,如果为-1说明当前没有扩容,如果不为-1则表示当前扩容到哪个下标位置,方便下次进行从该下标位置继续扩容。

rehash图解

rehash,重新散列,或者扩容。

rehash条件

根据不同的场景,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 时, 程序自动开始对哈希表执行收缩操作。

  1. 扩展空间以dictAdd函数为入口。在适当的条件下,最终调用dictExpand实现。而dictExpand会为ht[1]重新分配空间,并重置rehashidx索引值,为后面的rehash迁移做铺垫。
    触发条件:
    ht[0].used/ht[0].size>=1 && (dict_can_resize=1 || d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
    used/size: 负载因子
    dict_can_resize: 是否开启resize标识 1:开启 0:关闭 
    dict_force_resize_ratio:强制resize的条件(默认值为5)
    
  2. 缩减空间的入口函数为dictResize,内部同样调用了dictExpand。redis的server.db定期检查有使用到(于redis.c/htNeedsResize)

rehash实现

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

  1. 为字典的 ht[1] 哈希表分配空间, 这个哈希表的空间大小取决于要执行的操作, 以及 ht[0] 当前包含的键值对数量 (也即是ht[0].used 属性的值):

    如果执行的是扩展操作, 那么 ht[1] 的大小为第一个大于等于 ht[0].used * 2 的 2^n (2 的 n 次方幂);
    如果执行的是收缩操作, 那么 ht[1] 的大小为第一个大于等于 ht[0].used 的 2^n 。
    
  2. 将保存在 ht[0] 中的所有键值对 rehash 到 ht[1] 上面: rehash 指的是重新计算键的哈希值和索引值, 然后将键值对放置到 ht[1] 哈希表的指定位置上。

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

案例
假设程序要对图 4-8 所示字典的 ht[0] 进行扩展操作, 那么程序将执行以下步骤:
在这里插入图片描述

  • ht[0].used 当前的值为 4 , 4 * 2 = 8 , 而 8 (2^3)恰好是第一个大于等于 4 的 2 的 n 次方, 所以程序会将 ht[1] 哈希表的大小设置为 8 。 图 4-9 展示了 ht[1] 在分配空间之后, 字典的样子。
    在这里插入图片描述

  • 将 ht[0] 包含的四个键值对都 rehash 到 ht[1] , 如图 4-10 所示。
    在这里插入图片描述

  • 释放 ht[0] ,并将 ht[1] 设置为 ht[0] ,然后为 ht[1] 分配一个空白哈希表,如图 4-11 所示。
    在这里插入图片描述

渐进式rehash图解

扩展和收缩都需要将ht[0]里面的所有键值对散列到ht[1]中,但是这个动作并不是一次性完成的,而是分多次,渐进式完成的。

这么做的原因在于,如果ht[0]如果只保存了四个键值对,那么服务器可以在瞬间完成,但是如果里面保存的是四百万,四千万的键值对,那么一次性将这些键值对全部散列到ht[1]中,这个计算量还是很庞大的。

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

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

注 意 观 察 在 整 个 r e h a s h 过 程 中 , 字 典 的 r e h a s h i d x 属 性 是 如 何 变 化 的 \color{#ef246f}{注意观察在整个 rehash 过程中, 字典的 rehashidx 属性是如何变化的} rehashrehashidx

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

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

源码阅读

创建并初始化字典

/* 创建并初始化字典 */
dict *dictCreate(dictType *type,
        void *privDataPtr)
{
    dict *d = zmalloc(sizeof(*d));
    _dictInit(d,type,privDataPtr);
    return d;
}
 
/* Initialize the hash table */
int _dictInit(dict *d, dictType *type,
        void *privDataPtr)
{
    _dictReset(&d->ht[0]);
    _dictReset(&d->ht[1]);
    d->type = type;
    d->privdata = privDataPtr;
    d->rehashidx = -1;//赋值为-1,表示未进行hash
    d->iterators = 0;
    return DICT_OK;
}

//重置hash表
static void _dictReset(dictht *ht)
{
    ht->table = NULL;
    ht->size = 0;
    ht->sizemask = 0;
    ht->used = 0;
}

由dictCreate创建一个字典d,并将d传入_dictInit函数。而_dictInit函数将负责d初始化操作。在_dictInit内部调用 _dictReset初始化ht[0]和ht[1]数据结构。

从_dictReset函数我们可以看到,新建dict时未对ht[0]、ht[1]分配空间,那么系统会在什么时候进行分配操作呢? 答案是在调用dictAdd操作时.

字典添加

int dictAdd(dict *d, void *key, void *val)
{
    dictEntry *entry = dictAddRaw(d,key);
    if (!entry) return DICT_ERR;
    dictSetVal(d, entry, val);
    return DICT_OK;
}

dictAddRaw会检查d是否存在key,如果存在,则返回NULL,否则创建key节点。
dictSetVal:顾名思义,设置节点的值。

dictEntry *dictAddRaw(dict *d, void *key)
{
    int index;
    dictEntry *entry;
    dictht *ht;
    //判断是否在进行rehash操作
    if (dictIsRehashing(d)) _dictRehashStep(d);
    //检查key是否存在,如果存在,则返回NULL
    if ((index = _dictKeyIndex(d, key)) == -1)
        return NULL;

    //判断rehash是否正在进行,如果正在进行,则往ht[1]添加数据,否则添加至ht[0]
    ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
    //创建key节点
    entry = zmalloc(sizeof(*entry));
    //将节点的指针指向对应的链表头部
    entry->next = ht->table[index];
    //添加节点至链表头部
    ht->table[index] = entry;
    //更新used值
    ht->used++;

    //设置节点信息
    dictSetKey(d, entry, key);
    return entry;
}

从上面可以看出,代码执行顺序:dictIsRehashing->_dictKeyIndex->dictIsRehashing->dictSetKey.细心的童鞋可能注意到,该函数内部调用两次dictIsRehashing。难道在_dictKeyIndex函数期间dict结构会发生变化么?
追踪下_dictKeyIndex代码:

static int _dictKeyIndex(dict *d, const void *key)
{
    unsigned int h, idx, table;
    dictEntry *he;
    if (_dictExpandIfNeeded(d) == DICT_ERR)
        return -1;
    //计算key hash值
    h = dictHashKey(d, key);
    //查找key,如果存在,则返回-1,否则返回hash索引
    for (table = 0; table <= 1; table++) {
        //计算hash索引
        idx = h & d->ht[table].sizemask;
        //从hash索引对应的链表中搜索
        he = d->ht[table].table[idx];
        while(he) {
            if (key==he->key || dictCompareKeys(d, key, he->key))
                return -1;
            he = he->next;
        }
        //如果rehash未进行,则只需搜索ht[0]
        if (!dictIsRehashing(d)) break;
    }
    return idx;
}

从_dictKeyIndex内部,可以看到_dictExpandIfNeeded函数。根据字面意思推测,这个应该与dict空间有关联(即ht->size)。继续追踪_dictExpandIfNeeded代码

//判断dict是否需要扩展空间
static int _dictExpandIfNeeded(dict *d)
{
    //rehash正在进行,则不进行操作
    if (dictIsRehashing(d)) return DICT_OK;

    //如果size=0,则设置默认大小
    if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);

    //当负载因子(used/size)>=1时,对以下两种情况扩展空间。
    //1. dict_can_resize=1
    //2. 达到强制resize条件时(used/size>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);
    }
    return DICT_OK;
}

对于新建的dict,执行的代码为dictExpand(d, DICT_HT_INITIAL_SIZE)。DICT_HT_INITIAL_SIZE在dict.h文件被定义,值为4.我们再追踪下dictExpand函数。

int dictExpand(dict *d, unsigned long size)
{
    //dict的扩展空间大小:最小一个>=size的2^N数
    unsigned long realsize = _dictNextPower(size);

    ...省略部分代码...

    //设置dict大小
    n.size = realsize;
    //设置hash掩码
    n.sizemask = realsize-1;
    //初始化table空间
    n.table = zcalloc(realsize*sizeof(dictEntry*));
    n.used = 0;
    //如果dict是否为空(初始化操作),则将n设置为ht[0]
    if (d->ht[0].table == NULL) {
        d->ht[0] = n;
        return DICT_OK;
    }

    //将n赋给ht[1],并设置rehash索引
    d->ht[1] = n;
    d->rehashidx = 0;
    return DICT_OK;
}

从上面的代码可以看出,ht[0]和ht[1]的内存分配都是在这里进行的。对于一个为空的dict,系统会为ht[0]分配空间。对于一个非空的dict,系统则为ht[1]分配空间,并重置rehashidx标识。

现在应该知道dictAddRaw函数内部执行_dictKeyIndex之后再次调用 dictIsRehashing的原因了吧。

好了,总结下dictAdd的流程:dictAdd->dictAddRaw->_dictKeyIndex->_dictExpandIfNeeded->dictExpand。

字典替换

dictReplace:顾名思义,替换功能,分为两种情形:当key不存在,则进行创建;当key存在,则修改key的值。代码执行流程:dictAdd->dictFind->dictSetVal

int dictReplace(dict *d, void *key, void *val)
{
    dictEntry *entry, auxentry;
    //如果key不存在,则进行创建
    if (dictAdd(d, key, val) == DICT_OK)
        return 1;
    //如果key存在,则找到相应的节点
    entry = dictFind(d, key);
    //修改节点的值
    dictSetVal(d, entry, val);
    ...省略部分代码...
}

字典删除

dictDelete

/* Remove an element, returning DICT_OK on success or DICT_ERR if the
 * element was not found. */
int dictDelete(dict *ht, const void *key) {
    return dictGenericDelete(ht,key,0) ? DICT_OK : DICT_ERR;
}

扩大或者缩小空间

dictResize—>dictExpand

/* Resize the table to the minimal size that contains all the elements,
 * but with the invariant of a USED/BUCKETS ratio near to <= 1 */
 // 缩小hashtable空间  触发rehash的条件
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;  // 4
    return dictExpand(d, minimal);
}
/* Expand or create the hash table */
int dictExpand(dict *d, unsigned long size)
{
    /* 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)
        return DICT_ERR;

    dictht n; /* the new hash table */
    // 它不断计算 2 的乘幂,直到遇到大于等于 size 参数的乘幂,就返回这个乘幂作为哈希表的大小 这个地方有点意思的
    unsigned long realsize = _dictNextPower(size);  //比size大的2**n

    /* Rehashing to the same table size is not useful. */
    if (realsize == d->ht[0].size) return DICT_ERR;

    /* 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. */
    // 如果0号哈希表为空 那么这是第一次初始化
    // 程序将新的哈希表赋给0号哈希表的指针  然后字典处理键值对
    if (d->ht[0].table == NULL) {
        d->ht[0] = n;
        return DICT_OK;
    }

    /* Prepare a second hash table for incremental rehashing */
    d->ht[1] = n;
    d->rehashidx = 0;  // 标记可以rehash 
    return DICT_OK;
}

_dictKeyIndex——>_dictExpandIfNeeded——>dictExpand

/* Returns the index of a free slot that can be populated with
 * a hash entry for the given 'key'.
 * If the key already exists, -1 is returned
 * and the optional output parameter may be filled.
 *
 * Note that if we are in the process of rehashing the hash table, the
 * index is always returned in the context of the second (new) hash table. */
static long _dictKeyIndex(dict *d, const void *key, uint64_t hash, dictEntry **existing)
{
    unsigned long idx, table;
    dictEntry *he;
    if (existing) *existing = NULL;

    /* Expand the hash table if needed */
    if (_dictExpandIfNeeded(d) == DICT_ERR)
        return -1;
    for (table = 0; table <= 1; table++) {
        idx = hash & d->ht[table].sizemask;
        /* Search if this slot does not already contain the given key */
        he = d->ht[table].table[idx];
        while(he) {
            if (key==he->key || dictCompareKeys(d, key, he->key)) {
                if (existing) *existing = he;
                return -1;
            }
            he = he->next;
        }
        if (!dictIsRehashing(d)) break;
    }
    return idx;
}

static int _dictExpandIfNeeded(dict *d)
{
    /* Incremental rehashing already in progress. Return. */
    if (dictIsRehashing(d)) return DICT_OK;

    /* 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);

    /* 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. */
    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;
}

渐进式的rehash

dictRehashMilliseconds:一个定时器调用rehash

/* Rehash for an amount of time between ms milliseconds and ms+1 milliseconds */
int dictRehashMilliseconds(dict *d, int ms) {
    long long start = timeInMilliseconds();
    int rehashes = 0;
    // 在给定的毫秒数 以100为单位 
    while(dictRehash(d,100)) {
        rehashes += 100;
        if (timeInMilliseconds()-start > ms) break;
    }
    return rehashes;
}

_dictRehashStep:在添加 查找 删除的时候会调用这个方法,平摊这dictAddRaw 、dictGetRandomKey 、dictFind 、dictGenericDelete这些函数,这样可以避免集中式的rehash,出现进程阻塞的情况。

/* This function performs just a step of rehashing, and only if there are
 * no safe iterators bound to our hash table. When we have iterators in the
 * middle of a rehashing we can't mess with the two hash tables otherwise
 * some element can be missed or duplicated.
 *
 * This function is called by common lookup or update operations in the
 * dictionary so that the hash table automatically migrates from H1 to H2
 * while it is actively used. */
static void _dictRehashStep(dict *d) {
    if (d->iterators == 0) dictRehash(d,1);
}

注 意 ! 注 意 ! 注 意 ! 最 为 核 心 的 东 西 来 了 。 。 \color{#ef246f}{注意!注意!注意!最为核心的东西来了。。} 西
a). 将ht[0]数据迁移至ht[1]
b). 待迁移完毕,将ht[1]复制给ht[0],重置ht[1]数据及rehashidx索引值

//重新hash这个哈希表
// Redis的哈希表结构公有两个table数组,t0和t1,平常只使用一个t0,当需要重hash时则重hash到另一个table数组中
//参数列表
// 1. d: 待移动的哈希表,结构中存有目前已经重hash到哪个桶了
// 2. n: N步进行rehash 
// 返回值 返回0说明整个表都重hash完成了,返回1代表未完成

/* 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 返回1 hash中 返回0 hash完成
int dictRehash(dict *d, int n) {
    int empty_visits = n*10; /* Max number of empty buckets to visit. */
    if (!dictIsRehashing(d)) return 0;  //如果当前rehashidx=-1,则返回0,表示hash完成

    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 */
        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];
        /* Move all the keys in this bucket from the old to the new hash HT */
        // 第二个循环用来将ht[0]表中每次找到的非空桶中的链表(或者就是单个节点)拷贝到ht[1]中
        // 链表中的所有节点迁移到新的hash表
        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;
}

个人思考和感悟

前面在阅读字符和链表的源码的时候,redis源码阅读—sds redis源码阅读—adlist,觉得没有那么的复杂,看到字典这部分其实不是很容易就懂了,在这篇文章中对于迭代器部分我自己看了,但是没有十足的把握能够写清楚,而且许多的地方也没看明白,这次刷源码就先不管这部分了,之后找时间好好的研究一下。

我个人的一些感悟,刷redis源码需要多去看看一些资料,社区、博客、书籍等,只有在先理解大致的原理之后,再去看源码才会有些头绪(大神划过),而且要有一定的C基础,相信学编程的都能看懂C语言,如果不懂可以先简单学一下。此外尽量使用好的环境工具,我个人使用VSCode(比较推荐),这样可以提升你的效率。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值