Redis基本数据结构之dict

71 阅读16分钟

1. 前言

本文的内容是基于 redis-7.0.0 的源码,具体的源码可以参阅 redis 7.0.0。本文也不会对源文件 dict.h/dict.c 中所有的源码进行解释,只会讲解其中的主干部分,如果有兴趣可以参阅 dict.h 和 dict.c

2. 基础概念

在计算机科学中,关联数组(Associative Array),又称映射(Map)、字典(Dictionary)是一个抽象的 数据结构(下面统称为字典),它包含着类似于(键,值)的有序对。 字典可以由 哈希表(Hash table,也叫散列表)或者 红黑树 (Red–black tree)实现。从 redis 的源码可以看出,redis 采用了哈希表的实现。

散列表由两大元素组成,数组哈希函数。其中哈希函数对键值对的 key 进行 hash 运算,得到其在数组中的位置(index),然后将键值对的 value存储在数组对应的位置上。其过程示意图如下:

dict_1.png

随着数据的增多,就会出现不同的 key 值经过哈希函数计算得出相同的 index 的情况。也就是产生了冲突。解决冲突的方法有很多,比如:

  • 双散列
  • 再散列
  • 开放定址法
  • 建立一个公共溢出区。
  • 单独链表法:将散列到同一个存储位置的所有元素保存在一个链表中。实现时,一种策略是散列表同一位置的所有冲突结果都是用栈存放的,新元素被插入到表的前端还是后端完全取决于怎样方便。

在 redis 中采用的是 单独链表法(也叫链地址法)。对于散列到同一位置的键值对,以单链表的形式将它们组织起来。其过程示意图如下:

dict_2.png

在上面的图中,Key1 和 Key2 进行散列后的 index 相同,对应的键值对存储在以 index 为起始地址的单链表中。

3. redis 实现

3.1 结构定义

3.1.1 dict

dict.h 定义了结构体 dict。

typedef struct dict dict;

struct dict {
    dictType *type;

    dictEntry **ht_table[2];
    unsigned long ht_used[2];

    long rehashidx; /* rehashing not in progress if rehashidx == -1 */

    /* Keep small vars at end for optimal (minimal) struct padding */
    int16_t pauserehash; /* If >0 rehashing is paused (<0 indicates coding error) */
    signed char ht_size_exp[2]; /* exponent of size. (size = 1<<exp) */
};

主要字段的含义如下:

  • type 类型为dictType 结构体指针,dictType 中定义了特定类型的相关函数,包括 keyvalue 的 compare,dup,destructor等。
  • ht_table[2] 类型为 dictEntry** 代表每一个键值对。这里的 2 代表每一个 dict 类型有两个哈希表,ht_table[0] 是字典主要使用的哈希表,ht_table[1] 是在对 ht_table[0] 进行 rehash 时才使用。
  • ht_used 类型为 unsigned long,表示哈希表中 entry 的数量。
  • rehashidx 类型为 long,表示正在哈希表中位置处于 index 的 entry 正在 rehash。
  • ht_size_exp[2] 表示 hash 表的大小的指数,ht_table[0] size = 1 << ht_size_exp[0]
  • pauserehash 类型为 int16_t,如果其值大于 0,则表示 rehash 暂停的,也就是此时不被允许 rehash。如果小于0,则表示 coding error。

3.1.2 dictType 结构定义

dictType 结构体中定义 key 和 value 的一些相关函数指针,包括 dup,compare,destructor 和 hashFunction等。

dup 函数用于复制,compare 函数用于比较,destructor 用于销毁。

hashFunction 用于获取 key 的哈希值,redis 默认的 hash 函数是 SipHash

typedef struct dictType {
    uint64_t (*hashFunction)(const void *key);
    void *(*keyDup)(dict *d, const void *key);
    void *(*valDup)(dict *d, const void *obj);
    int (*keyCompare)(dict *d, const void *key1, const void *key2);
    void (*keyDestructor)(dict *d, void *key);
    void (*valDestructor)(dict *d, void *obj);
    int (*expandAllowed)(size_t moreMem, double usedRatio);
    /* Allow a dictEntry to carry extra caller-defined metadata.  The
     * extra memory is initialized to 0 when a dictEntry is allocated. */
    size_t (*dictEntryMetadataBytes)(dict *d);
} dictType;

3.1.3 dictEntry 结构定义

typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;     /* Next entry in the same hash bucket. */
    void *metadata[];           /* An arbitrary number of bytes (starting at a
                                 * pointer-aligned address) of size as returned
                                 * by dictType's dictEntryMetadataBytes(). */
} dictEntry;

dictEntry 包括 key 和 value,以及指向下一个 dictEntry 的指针。

  • key 类型为 void *。
  • v 类型为 union
  • next 类型为 dictEntry *。

metadata 字段将在下面进行介绍,从注释来看它的含义是:dictType 的 dictEntryMetadataBytes() 返回的任意大小的字节数。

3.2 dict 创建

到目前为止,我们基本上了解了 dict 的结构定义。接下来可以看下创建一个新的 dict,它的一些字段是如何初始化的。 创建一个 dict 的函数定义如下:

/* Create a new hash table */
dict *dictCreate(dictType *type)
{
    dict *d = zmalloc(sizeof(*d));

    _dictInit(d,type);
    return d;
}

dictCreate 函数的参数类型为一个 dictType *。首先为 dict 分配内存,调用 zmalloc 函数进行内存空间分配,然后调用 int _dictInit(dict *d, dictType *type) 对 dict 中的字段进行初始化。

_dictInit 函数定义如下:

/* Initialize the hash table */
int _dictInit(dict *d, dictType *type)
{
    _dictReset(d, 0);
    _dictReset(d, 1);
    d->type = type;
    d->rehashidx = -1;
    d->pauserehash = 0;
    return DICT_OK;
}

调用 _dictReset 分别初始化 ht_table[0]ht_table[1]。然后再设置 typerehashidxpauserehash 三个字段。

d->type = type; 设置 ditc 的 type 字段

d->rehashidx = -1; 设置 ditc 的 rehashidx 字段,表明没有正在 rehash 的键值对。

d->pauserehash = 0;

_dictRest 函数的定义如下:

static void _dictReset(dict *d, int htidx)
{
    d->ht_table[htidx] = NULL;
    d->ht_size_exp[htidx] = -1;
    d->ht_used[htidx] = 0;
}

_dictRest 分别对 ht_tabel[htidx], ht_size_exp[htidx]ht_used[htidx] 进行初始化。

可以将一个新建的 dict 表示成如下图所示:

dict_3.png

3.3 dict 添加元素

3.3.1 dictAdd

往一个 dict 添加元素的 dictAdd 函数的定义如下,可以看到在其中调用了 dictAddRaw 函数。

/* Add an element to the target hash table */
int dictAdd(dict *d, void *key, void *val)
{
    dictEntry *entry = dictAddRaw(d,key,NULL);

    if (!entry) return DICT_ERR;
    dictSetVal(d, entry, val);
    return DICT_OK;
}

dictAddRaw 函数定义如下:

/* Low level add or find:
 * This function adds the entry but instead of setting a value returns the
 * dictEntry structure to the user, that will make sure to fill the value
 * field as they wish.
 *
 * This function is also directly exposed to the user API to be called
 * mainly in order to store non-pointers inside the hash value, example:
 *
 * entry = dictAddRaw(dict,mykey,NULL);
 * if (entry != NULL) dictSetSignedIntegerVal(entry,1000);
 *
 * Return values:
 *
 * If key already exists NULL is returned, and "*existing" is populated
 * with the existing entry if existing is not NULL.
 *
 * If key was added, the hash entry is returned to be manipulated by the caller.
 */
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{
    long index;
    dictEntry *entry;
    int htidx;

    if (dictIsRehashing(d)) _dictRehashStep(d);

    /* Get the index of the new element, or -1 if
     * the element already exists. */
    if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)
        return NULL;

    /* Allocate the memory and store the new entry.
     * Insert the element in top, with the assumption that in a database
     * system it is more likely that recently added entries are accessed
     * more frequently. */
    htidx = dictIsRehashing(d) ? 1 : 0;
    size_t metasize = dictMetadataSize(d);
    entry = zmalloc(sizeof(*entry) + metasize);
    if (metasize > 0) {
        memset(dictMetadata(entry), 0, metasize);
    }
    entry->next = d->ht_table[htidx][index];
    d->ht_table[htidx][index] = entry;
    d->ht_used[htidx]++;

    /* Set the hash entry fields. */
    dictSetKey(d, entry, key);
    return entry;
}

判断是否正在执行 reshash 的宏定义如下:

#define dictIsRehashing(d) ((d)->rehashidx != -1)

首先判断是否正在 rehash。如果是则执行 _dictRehashStep() 函数。

_dictRehashStep()的代码如下,可以看出,如果 d->pauserehash == 0,则调用 dictRehash 函数进行 rehash,这一部分我们下面介绍。


/* This function performs just a step of rehashing, and only if hashing has
 * not been paused for our hash table. When we have iterators in the
 * middle of a rehashing we can't mess with the two hash tables otherwise
 * some elements 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->pauserehash == 0) dictRehash(d,1);
}

在判断是否 rehash 之后,则判断要插入到 dict 中 key 是否存在于 dict 中。代码如下:

// Get the index of the new element, or -1 if
// the element already exists. 

 if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)
        return NULL;

dictHashKey 的宏定义如下,调用的是 dict 的 type 参数中的 hashFunction 函数,来计算 hash 值。

#define dictHashKey(d, key) (d)->type->hashFunction(key)

如果 key 存在于 dict 中,则返回 -1,如果不存在,则返回其在 ht_table 中的 index。此时需要注意的是,在 redsi 7.0 的源码中,默认使用的哈希函数是使用 SipHash 实现的,其源码定义在 siphash.c 中。

/* -------------------------- hash functions -------------------------------- */

static uint8_t dict_hash_function_seed[16];

void dictSetHashFunctionSeed(uint8_t *seed) {
    memcpy(dict_hash_function_seed,seed,sizeof(dict_hash_function_seed));
}

uint8_t *dictGetHashFunctionSeed(void) {
    return dict_hash_function_seed;
}

/* The default hashing function uses SipHash implementation
 * in siphash.c. */

uint64_t siphash(const uint8_t *in, const size_t inlen, const uint8_t *k);
uint64_t siphash_nocase(const uint8_t *in, const size_t inlen, const uint8_t *k);

uint64_t dictGenHashFunction(const void *key, size_t len) {
    return siphash(key,len,dict_hash_function_seed);
}

uint64_t dictGenCaseHashFunction(const unsigned char *buf, size_t len) {
    return siphash_nocase(buf,len,dict_hash_function_seed);
}

_dictKeyIndex 函数定义如下

/* 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 & DICTHT_SIZE_MASK(d->ht_size_exp[table]);
        /* 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;
}

DICTHT_SIZE 和 DICTHT_SIZE_MASK 宏定义如下:

#define DICTHT_SIZE(exp) ((exp) == -1 ? 0 : (unsigned long)1<<(exp))

#define DICTHT_SIZE_MASK(exp) ((exp) == -1 ? 0 : (DICTHT_SIZE(exp))-1)

下面的代码是为了防止 idx 的值超过了 ht_table 的大小,数组越界。

idx = hash & DICTHT_SIZE_MASK(d->ht_size_exp[table]);

ht_table 的大小为 1 << exp,因为 DICTHT_SIZE_MASK(exp) 返回的值只有 0 和 (1<<exp)- 1)。所以 idx 的值永远不会大于或者等于 1 << exp。

if ht_size_exp[0] = 2;
so ht_table[0] 的大小为 8;
假如 hashFunction(key) 的值为 100,超过了ht_table[0] 的大小 8。
DICTHT_SIZE_MASK(2) = 7
那么 idx 的值为:100 & 7 = 4

_dictKeyIndex 函数调用 _dictExpandIfNeeded 函数是否需要扩充散列表(下面再介绍这个函数)。

最后的 for 循环遍历两个 ht_table 查找是否有相应的 key 在散列表中。如果存在,返回相应的 idx 以及获得将相应 dictEntry

dictAddRaw函数中使用头插法插入 entry,并且更新 ht_table_used 的值

 entry->next = d->ht_table[htidx][index];
 d->ht_table[htidx][index] = entry;
 d->ht_used[htidx]++;

最后调用 dictSetKey 函数,插入对应的 key value。

void dictSetKey(dict *d, dictEntry* de, void *key) {
    assert(!d->type->no_value);
    if (d->type->keyDup)
        de->key = d->type->keyDup(d, key);
    else
        de->key = key;
}

当 addRaw 函数执行完,在执行 dictSetVal 函数,设置新添加的 key 的 value。代码如下:

void dictSetVal(dict *d, dictEntry *de, void *val) {
    assert(entryHasValue(de));
    de->v.val = d->type->valDup ? d->type->valDup(d, val) : val;
}

下图显示了一个只有 ht_table[0] 中添加了几个元素的 dict 的示意图:

dict4.png

至此向一个 dict 中添加键值对的所涉及的代码已经梳理完了,虽然没有特别详细的解释每一行代码,但是抓住了主干。总结一下就是:

  1. dict 正在 rehash,则进行 rehash 之后再进行插入键值对。
  2. 如果需要对 ht_table 进行扩充,则进行扩充。
  3. 查找添加的 key 是否存在于 ht_table。如果存在,则不进行插入 entry,而是找到 key 值相等 entry,重新设置其 value。
  4. 如果不存在,则新建entry,计算要插入到 ht_table 中 index。获取到散列到该 index 的所有 entry 组成的单链表的表头,然后将新建的 entry 利用头插法插入到链表中。
  5. 更新 dict 中 相关的字段。比如 ht_table_used[0]++,表示 ht_table[0] 中的 dictEntry 加 1。

3.3.3 dictAddAndFind

dictAddOrFind() 只是 dictAddRaw() 的一个简单版本,它始终返回指定键的哈希条目,即使该键已经存在并且无法添加(在这种情况下,返回已存在键的条目)。

/* Add or Find:
 * dictAddOrFind() is simply a version of dictAddRaw() that always
 * returns the hash entry of the specified key, even if the key already
 * exists and can't be added (in that case the entry of the already
 * existing key is returned.)
 *
 * See dictAddRaw() for more information. */
dictEntry *dictAddOrFind(dict *d, void *key) {
    dictEntry *entry, *existing;
    entry = dictAddRaw(d,key,&existing);
    return entry ? entry : existing;
}

3.3.4 dictFind

dictFind 查找 dict 中某个 key 的 dictEntry。如果存在,则返回该 entry。如果不存在,则返回 NULL。

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

    if (dictSize(d) == 0) return NULL; /* dict is empty */
    if (dictIsRehashing(d)) _dictRehashStep(d);
    h = dictHashKey(d, key);
    for (table = 0; table <= 1; table++) {
        idx = h & DICTHT_SIZE_MASK(d->ht_size_exp[table]);
        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;
}

3.4 dict rehash

int dictRehash(dict *d, int n) 中进行 rehash 过程。这个函数的主要目的是将 ht_table[0] 中的键值对 rehash 到 ht_table[1] 中。并且将 ht_table[0] 数据清空,ht_table[1] 替换为新的 ht_table1[0]int dictRehash(dict *d, int n) 代码如下:

/* 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. */
int dictRehash(dict *d, int n) {
    int empty_visits = n*10; /* Max number of empty buckets to visit. */
    unsigned long s0 = DICTHT_SIZE(d->ht_size_exp[0]);
    unsigned long s1 = DICTHT_SIZE(d->ht_size_exp[1]);
    if (dict_can_resize == DICT_RESIZE_FORBID || !dictIsRehashing(d)) return 0;
    if (dict_can_resize == DICT_RESIZE_AVOID && 
        ((s1 > s0 && s1 / s0 < dict_force_resize_ratio) ||
         (s1 < s0 && s0 / s1 < dict_force_resize_ratio)))
    {
        return 0;
    }

    while(n-- && d->ht_used[0] != 0) {
        dictEntry *de, *nextde;

        /* Note that rehashidx can't overflow as we are sure there are more
         * elements because ht[0].used != 0 */
        assert(DICTHT_SIZE(d->ht_size_exp[0]) > (unsigned long)d->rehashidx);
        //找到第一个不为空的 rehashidx。
        while(d->ht_table[0][d->rehashidx] == NULL) {
            d->rehashidx++;
            if (--empty_visits == 0) return 1;
        }
        //获取该 idx 对应的单链表头节点。
        de = d->ht_table[0][d->rehashidx];
        /* Move all the keys in this bucket from the old to the new hash HT */
        //移动该bucket(也指单链表) 所有的 keys(也指单链表中的节点) 到新的 hash table 中。
        while(de) {
            uint64_t h;

            nextde = de->next;
            /* Get the index in the new hash table */
            //获取该 entry 在新ht_table[1] 中的 index
            h = dictHashKey(d, de->key) & DICTHT_SIZE_MASK(d->ht_size_exp[1]);
            //插入新 entry 在 ht_table[1][h] 单链表中
            de->next = d->ht_table[1][h];
            d->ht_table[1][h] = de;
            //更新ht_table 中的 entry 数量。
            d->ht_used[0]--;
            d->ht_used[1]++;
            de = nextde;
        }
        //将ht_table[0]中正在 rehash 的 bucket置空
        d->ht_table[0][d->rehashidx] = NULL;
        d->rehashidx++;
    }

    /* Check if we already rehashed the whole table... */
    //如果ht_table[0] 不存在任何节点,则将ht_table[0]置空。
    if (d->ht_used[0] == 0) {
        zfree(d->ht_table[0]);
        /* Copy the new ht onto the old one */
        d->ht_table[0] = d->ht_table[1];
        d->ht_used[0] = d->ht_used[1];
        d->ht_size_exp[0] = d->ht_size_exp[1];
        _dictReset(d, 1);
        d->rehashidx = -1;
        return 0;
    }

    /* More to rehash... */
    return 1;
}
  • rehash 第一步首先判断是否满足 rehash 的条件。其中 empty_visits 表示最大的能访问的空的 hash 中空的条目的数量。当满足这个条件,则就不进行 rehash。这也意味着该函数可能不会 rehash 整个 ht_table[0]。
int empty_visits = n*10; /* Max number of empty buckets to visit. */
    unsigned long s0 = DICTHT_SIZE(d->ht_size_exp[0]);
    unsigned long s1 = DICTHT_SIZE(d->ht_size_exp[1]);
    if (dict_can_resize == DICT_RESIZE_FORBID || !dictIsRehashing(d)) return 0;
    if (dict_can_resize == DICT_RESIZE_AVOID && 
        ((s1 > s0 && s1 / s0 < dict_force_resize_ratio) ||
         (s1 < s0 && s0 / s1 < dict_force_resize_ratio)))
    {
        return 0;
    }
  • 找到第一个不为空的 rehashidx。
while(d->ht_table[0][d->rehashidx] == NULL) {
    d->rehashidx++;
    if (--empty_visits == 0) return 1;
}
  • 获取该 rehashidx 对应的单链表头节点。
de = d->ht_table[0][d->rehashidx];

移动该bucket(也指单链表) 所有的 keys(也指单链表中的节点) 到新的 hash table 中。

 while(de) {
     uint64_t h;

     nextde = de->next;
     /* Get the index in the new hash table */
     //获取该 entry 在新ht_table[1] 中的 index
     h = dictHashKey(d, de->key) & DICTHT_SIZE_MASK(d->ht_size_exp[1]);
     //插入新 entry 在 ht_table[1][h] 单链表中
     de->next = d->ht_table[1][h];
     d->ht_table[1][h] = de;
     //更新ht_table 中的 entry 数量。
     d->ht_used[0]--;
     d->ht_used[1]++;
     de = nextde;
}

ht_table[0] 中正在 rehash 的 bucket 置空

d->ht_table[0][d->rehashidx] = NULL;
d->rehashidx++;

如果 ht_table[0] 不存在任何节点,则将 ht_table[0] 置空。

if (d->ht_used[0] == 0) {
    //释放内存
    zfree(d->ht_table[0]);
    
    /* Copy the new ht onto the old one */
    //将 ht_table[1] 复制给 ht_table[0]
    d->ht_table[0] = d->ht_table[1];
    d->ht_used[0] = d->ht_used[1];
    d->ht_size_exp[0] = d->ht_size_exp[1];
    
    //重置 ht_table[1]
    _dictReset(d, 1);
    
    //并且标志 dict 为没有正在 rehash
    d->rehashidx = -1;
    return 0;
}

3.5 dict expand

当需要对 dict 进行扩充容量时,调用 dictExpand 函数,其内部调用的是 _dictExpand 函数,源码如下:

/* return DICT_ERR if expand was not performed */
int dictExpand(dict *d, unsigned long size) {
    return _dictExpand(d, size, NULL);
}
/* Expand or create the hash table,
 * when malloc_failed is non-NULL, it'll avoid panic if malloc fails (in which case it'll be set to 1).
 * Returns DICT_OK if expand was performed, and DICT_ERR if skipped. */
int _dictExpand(dict *d, unsigned long size, int* malloc_failed)
{
    if (malloc_failed) *malloc_failed = 0;

    /* the size is invalid if it is smaller than the number of
     * elements already inside the hash table */
    if (dictIsRehashing(d) || d->ht_used[0] > size)
        return DICT_ERR;

    /* the new hash table */
    dictEntry **new_ht_table;
    unsigned long new_ht_used;
    signed char new_ht_size_exp = _dictNextExp(size);

    /* Detect overflows */
    size_t newsize = 1ul<<new_ht_size_exp;
    if (newsize < size || newsize * sizeof(dictEntry*) < newsize)
        return DICT_ERR;

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

    /* Allocate the new hash table and initialize all pointers to NULL */
    if (malloc_failed) {
        new_ht_table = ztrycalloc(newsize*sizeof(dictEntry*));
        *malloc_failed = new_ht_table == NULL;
        if (*malloc_failed)
            return DICT_ERR;
    } else
        new_ht_table = zcalloc(newsize*sizeof(dictEntry*));

    new_ht_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_table[0] == NULL) {
        d->ht_size_exp[0] = new_ht_size_exp;
        d->ht_used[0] = new_ht_used;
        d->ht_table[0] = new_ht_table;
        return DICT_OK;
    }

    /* Prepare a second hash table for incremental rehashing */
    d->ht_size_exp[1] = new_ht_size_exp;
    d->ht_used[1] = new_ht_used;
    d->ht_table[1] = new_ht_table;
    d->rehashidx = 0;
    return DICT_OK;
}
  1. 判断是否满足 expand 的条件,如果 dict 正在 rehash 或者形参 size 小于 ht_table[0] 中 entry 的个数,则返回 DICT_ERR。
 /* the size is invalid if it is smaller than the number of
     * elements already inside the hash table */
    if (dictIsRehashing(d) || d->ht_used[0] > size)
        return DICT_ERR;
  1. 初始化新的 hash table, size。判断是否能够满足条件,如果不能,则返回 DICT_ERR。
 /* the new hash table */
    dictEntry **new_ht_table;
    unsigned long new_ht_used;
    signed char new_ht_size_exp = _dictNextExp(size);

    /* Detect overflows */
    size_t newsize = 1ul<<new_ht_size_exp;
    if (newsize < size || newsize * sizeof(dictEntry*) < newsize)
        return DICT_ERR;

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

_dictNextExp 函数获取能大于 size 的ht_table_exp 的值 e。

/* TODO: clz optimization */
/* Our hash table capability is a power of two */
static signed char _dictNextExp(unsigned long size)
{
    unsigned char e = DICT_HT_INITIAL_EXP;

    if (size >= LONG_MAX) return (8*sizeof(long)-1);
    while(1) {
        if (((unsigned long)1<<e) >= size)
            return e;
        e++;
    }
}
  1. 重新分配内存
/* Allocate the new hash table and initialize all pointers to NULL */
    if (malloc_failed) {
        new_ht_table = ztrycalloc(newsize*sizeof(dictEntry*));
        *malloc_failed = new_ht_table == NULL;
        if (*malloc_failed)
            return DICT_ERR;
    } else
        new_ht_table = zcalloc(newsize*sizeof(dictEntry*));

    new_ht_used = 0;
  1. 如果是 dict 初始化后的第一次expand,将新创建的 hash table 赋值给 ht_table[0],并且设置 ht_used[0] 和 ht_size_exp[0] 的值。返回 DICT_OK。否则,执行第 5 步。
/* 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_table[0] == NULL) {
        d->ht_size_exp[0] = new_ht_size_exp;
        d->ht_used[0] = new_ht_used;
        d->ht_table[0] = new_ht_table;
        return DICT_OK;
    }
  1. 将新创建的 hash table 赋值给 ht_table[1],供 rehash 过程使用。
    /* Prepare a second hash table for incremental rehashing */
    d->ht_size_exp[1] = new_ht_size_exp;
    d->ht_used[1] = new_ht_used;
    d->ht_table[1] = new_ht_table;
    d->rehashidx = 0;
    return DICT_OK;

redis 中还定义了一个 dictTryExpand 函数,它与 dictExpand 不同的是,dictTryExpand 当内存分配失败时,会返回 DICT_ERR,而 dictExpand 则是当实际 expand 并没有执行则会返回 DICT_ERR。

/* return DICT_ERR if expand failed due to memory allocation failure */
int dictTryExpand(dict *d, unsigned long size) {
    int malloc_failed;
    _dictExpand(d, size, &malloc_failed);
    return malloc_failed? DICT_ERR : DICT_OK;
}

4. dict iterator

4.1 iterator 结构

迭代器 用于访问容器里元素。redis 也为 dict 中的 entry 遍历设计了 dictIterator,在 dict.h 中定义了 dictIterator 结构体。

/* If safe is set to 1 this is a safe iterator, that means, you can call
 * dictAdd, dictFind, and other functions against the dictionary even while
 * iterating. Otherwise it is a non safe iterator, and only dictNext()
 * should be called while iterating. */
typedef struct dictIterator {
    dict *d;
    long index;
    int table, safe;
    dictEntry *entry, *nextEntry;
    /* unsafe iterator fingerprint for misuse detection. */
    unsigned long long fingerprint;
} dictIterator;
  • d 迭代器遍历的字典。
  • index 遍历的 ht_table 的index 。
  • table 当前 dict 遍历的哈希表,ht_table[0] 或者 ht_table[1]。
  • safa 表示迭代器是否是 安全遍历。
  • entry 表示当前迭代器遍历的 entry。
  • nextEntry 表示接下来要遍历的 entry。
  • fingerprint 表示字典当前状态的签名(后面有详细的介绍)。

4.2 dict iterator get

其中 dictGetIterator 函数用于获取一个 dict 的迭代器,而 dictGetSafeIterator 函数用于获取一个安全的迭代器,这是通过设置 dictIterator->safe = 1 来实现的。

dictGetIterator 源码如下:

dictIterator *dictGetIterator(dict *d)
{
    dictIterator *iter = zmalloc(sizeof(*iter));

    iter->d = d;
    iter->table = 0;
    iter->index = -1;
    iter->safe = 0;
    iter->entry = NULL;
    iter->nextEntry = NULL;
    return iter;
}

dictGetSafeIterator 源码如下:

dictIterator *dictGetSafeIterator(dict *d) {
    dictIterator *i = dictGetIterator(d);

    i->safe = 1;
    return i;
}

那么此时就有一个问题,如何保证迭代器的安全是如何实现的呢?在 dictNext(dictIterator *iter) 函数中,通过判断 iter->safe 的值,然后通过 dictPauseRehashing(iter->d),来禁止对 dict 进行 rehash,保证迭代器遍历过程,不会发生 rehash。

#define dictPauseRehashing(d) (d)->pauserehash++

4.3 dict iterator next

dict iterator 的遍历,通过 dictNext 方法返回下一个遍历的元素,如果不存在,则返回 NULL。

dictEntry *dictNext(dictIterator *iter)
{
    while (1) {
        if (iter->entry == NULL) {
            if (iter->index == -1 && iter->table == 0) {
                if (iter->safe)
                    dictPauseRehashing(iter->d);
                else
                    iter->fingerprint = dictFingerprint(iter->d);
            }
            iter->index++;
            if (iter->index >= (long) DICTHT_SIZE(iter->d->ht_size_exp[iter->table])) {
                if (dictIsRehashing(iter->d) && iter->table == 0) {
                    iter->table++;
                    iter->index = 0;
                } else {
                    break;
                }
            }
            iter->entry = iter->d->ht_table[iter->table][iter->index];
        } else {
            iter->entry = iter->nextEntry;
        }
        if (iter->entry) {
            /* We need to save the 'next' here, the iterator user
             * may delete the entry we are returning. */
            iter->nextEntry = iter->entry->next;
            return iter->entry;
        }
    }
   
}

iter->entry == NULL 时,有两种可能,

  1. iterator 初始化后未进行遍历。
  2. 上一次迭代的 entry 的 nextentry 为 NULL,所以当前 iter->entry 为 NULL。

当因为是 iterator 初始化后未进行遍历,判断是否为 safe。

  1. 若 iter->safe 为 1 时,表示是安全的 iter。则调用 dictPauseRehashing 停止 rehashing。
dictPauseRehashing(iter->d);
  1. 若 iter->safe 为 0 时,表示的是不完全的 iter。需要计算此时 dict 的 fingerprint。
 iter->fingerprint = dictFingerprint(iter->d);

判断当前遍历hash table 的 index 是否超过了,当前遍历的 ht_table 的最大长度。

  1. 如果正在 rehashing,且当前正在遍历 ht_table[0],则更换遍历成 ht_table[1]。且设置 iter->index == 0。
  2. 如果不满足 1 的条件,则直接 break 跳出 while 循环,函数返回 NULL。

更新 iter->entry 为下一个要遍历的 entry。

  1. iter->entry == NULL
iter->index++;
iter->entry = iter->d->ht_table[iter->table][iter->index];
  1. iter->entry != NULL
iter->entry = iter->nextEntry;

最后判断当前 iter->entry 的值。

  1. iter->entry != NULL,更新 iter->entry 和 iter-nextEntry的值。
if (iter->entry) {
            /* We need to save the 'next' here, the iterator user
             * may delete the entry we are returning. */
            iter->nextEntry = iter->entry->next;
            return iter->entry;
        }
  1. iter->entry == NULL,直接返回 NULL。
 return NULL;

4.4 dict fingerpinter 计算

计算一个 dict fingeprint 是通过 dictFingerprint 函数来进行介绍算的。从注释可以看出:

fingerprint 是一个 64 位数字,表示在给定的时间点的状态,它的值为 dict 的各个字段的通过异或元算得出。当一个不安全的 iterator 初始化,并且第一次遍历时,我们计算其 fingerprint 值。当 iterator 释放时,我们会再对 dict 的 fingerprint 进行计算,如果此时的 fingerprint 与之前计算的不同,则意味着 dict 的遍历过程中,存在对 dict 进行了某些禁止操作(比如 添加元素)。

/* A fingerprint is a 64 bit number that represents the state of the dictionary
 * at a given time, it's just a few dict properties xored together.
 * When an unsafe iterator is initialized, we get the dict fingerprint, and check
 * the fingerprint again when the iterator is released.
 * If the two fingerprints are different it means that the user of the iterator
 * performed forbidden operations against the dictionary while iterating. */
unsigned long long dictFingerprint(dict *d) {
    unsigned long long integers[6], hash = 0;
    int j;

    integers[0] = (long) d->ht_table[0];
    integers[1] = d->ht_size_exp[0];
    integers[2] = d->ht_used[0];
    integers[3] = (long) d->ht_table[1];
    integers[4] = d->ht_size_exp[1];
    integers[5] = d->ht_used[1];

    /* We hash N integers by summing every successive integer with the integer
     * hashing of the previous sum. Basically:
     *
     * Result = hash(hash(hash(int1)+int2)+int3) ...
     *
     * This way the same set of integers in a different order will (likely) hash
     * to a different number. */
    for (j = 0; j < 6; j++) {
        hash += integers[j];
        /* For the hashing step we use Tomas Wang's 64 bit integer hash. */
        hash = (~hash) + (hash << 21); // hash = (hash << 21) - hash - 1;
        hash = hash ^ (hash >> 24);
        hash = (hash + (hash << 3)) + (hash << 8); // hash * 265
        hash = hash ^ (hash >> 14);
        hash = (hash + (hash << 2)) + (hash << 4); // hash * 21
        hash = hash ^ (hash >> 28);
        hash = hash + (hash << 31);
    }
    return hash;
}

4.4 dict iterator release

当对 iterator 进行释放时,首先判断对 iterator 是否进行过遍历,如果进行过遍历,则判断是否为 safe,如果 safe,则需要 resumeRehashing。如果不是 safa,则需要比较 fingerprint。最后调用 zfree(iter) 释放内存。

void dictReleaseIterator(dictIterator *iter)
{
    if (!(iter->index == -1 && iter->table == 0)) {
        if (iter->safe)
            dictResumeRehashing(iter->d);
        else
            assert(iter->fingerprint == dictFingerprint(iter->d));
    }
    zfree(iter);
}

5. 总结

  • 字典是由键值对构成的抽象数据结构。
  • Redis 字典的底层实现为哈希表,每个字典使用两个哈希表,一般情况下只使用 ht_table[0], 只有在 rehash 进行时,才会同时使用 ht_table[0] 和 ht_table[1]。
  • 哈希表使用链地址法来解决键冲突的问题。
  • 对哈希表的 rehash 是分多次、渐进式地进行的。
  • Redis 中安全的 iterator,进行遍历时,会终止 rehash 过程。
  • Redis 中不安全的 iterorator 销毁时使用 fingerprint 来验证 dict 是否被更改。

对于 redis 的 rehash 和 expand 等过程,在 Redis设计与实现之字典 都有具体的示意图表示,有兴趣的可以阅读此 blog。引用中内容不是基于 redis 7.0.0,不过原理大致都是相通的。还有许多函数未进行阅读,不过主体的代码都已经介绍了。

6. 引用

  1. dict.h
  2. dict.c
  3. 数据结构
  4. 哈希表
  5. 双散列
  6. 红黑树
  7. Redis设计与实现之字典