Redis 源码解读——字典

Posted WoLannnnn

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis 源码解读——字典相关的知识,希望对你有一定的参考价值。

文章目录

四个数据结构

dictEntry

dictEntry 的结构如下(Redis 7.0):

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;

可以对比 《Redis 设计与实现》中的 dictEntry 结构,发现联合结构 v 中多了一个 double 的浮点数表示,metadata 是一块任意长度的数据,具体的长度由 dictType 中的 dictEntryMetadataBytes() 返回,作用相当于 privdata

dictType

dictType 是一系列操作字典的键和值的操作:

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. */
    /* 允许调用者向条目 (dictEntry) 中添加额外的元信息.
     * 这段额外信息的内存会在条目分配时被零初始化. */
    size_t (*dictEntryMetadataBytes)(dict *d);
 dictType;

该结构是为了实现字典的多态。

dict

7.0 版本的字典结构如下:

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

相比于 《Redis 设计与实现》中的字典实现,改动较大,7.0中去掉了 dictht 结构,即去掉了哈希结构。接下来介绍每个成员:

/* 	type 上面已经解释过了;
*	ht_table 即哈希表数组
*	ht_used 分别表示哈希表数组中各自已经存放键值对的个数
*	rehashidx 是 rehash 时用的,没有 rehash 时值为1
*	pauserehash 则是表示 rehash 的状态,大于0时表示 rehash 暂停了,小于0表示出错了
*	ht_size_exp 则是表示两个哈希表数组的大小,通过 1 << ht_size_exp[0/1] 来计算
*/

我们可以看到一行注释:/* Keep small vars at end for optimal (minimal) struct padding */ ,将小变量放在结构体的后面,为了最佳或最小的填充,即节省空间。

dictIterator

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 表示指向的键值对索引
*	table 是哈希表的号码,ht[0]/ht[1]
*	safe 表示该迭代器是否安全。安全时可以掉用 dictAdd,dictFind等等其他函数,不安全时只能调用 dictNext
*	entry 指向迭代器所指的键值对,nextEntry 指向下一个键值对
*	fingerprint 指纹, 用于检查不安全迭代器的误用
*/

常量与一系列宏

首先是关于哈希表初始化的两个常量:

/* This is the initial size of every hash table */
#define DICT_HT_INITIAL_EXP      2
#define DICT_HT_INITIAL_SIZE     (1<<(DICT_HT_INITIAL_EXP))

很明显,哈希表的初始大小为 4

dictFreeVal

释放 val

#define dictFreeVal(d, entry) \\
    if ((d)->type->valDestructor) \\
        (d)->type->valDestructor((d), (entry)->v.val)

调用 dictType 中提供的 valDestructor 函数释放 val

dictSetVal

设置 val 的值

#define dictSetVal(d, entry, _val_) do  \\
    if ((d)->type->valDup) \\
        (entry)->v.val = (d)->type->valDup((d), _val_); \\
    else \\
        (entry)->v.val = (_val_); \\
 while(0)

如果 dictType 提供了设置 val 值的方法则调用,没有则直接赋值

dictSetSignedIntegerVal

设置有符号的整型 val 值

#define dictSetSignedIntegerVal(entry, _val_) \\
    do  (entry)->v.s64 = _val_;  while(0)

当然还有设置 无符号的整型值、double 值

dictFreeKey

释放 key

#define dictFreeKey(d, entry) \\
    if ((d)->type->keyDestructor) \\
        (d)->type->keyDestructor((d), (entry)->key)

dictSetKey

设置 key 的值

#define dictSetKey(d, entry, _key_) do  \\
    if ((d)->type->keyDup) \\
        (entry)->key = (d)->type->keyDup((d), _key_); \\
    else \\
        (entry)->key = (_key_); \\
 while(0)

dictCompareKeys

key 的比较

#define dictCompareKeys(d, key1, key2) \\
    (((d)->type->keyCompare) ? \\
        (d)->type->keyCompare((d), key1, key2) : \\
        (key1) == (key2))

如果 dictType 中的 keyCompare 指针不为空,则调用它进行比较,否则直接用 == 比较

dictMetadata

获取用户提供的元数据

#define dictMetadata(entry) (&(entry)->metadata)

dictMetadataSize

获取用户提供的元数据的长度

#define dictMetadataSize(d) ((d)->type->dictEntryMetadataBytes \\
                             ? (d)->type->dictEntryMetadataBytes(d) : 0)

获取 key 和 val 的宏

#define dictHashKey(d, key) (d)->type->hashFunction(key) // 获取 key 的哈希值
#define dictGetKey(he) ((he)->key)
#define dictGetVal(he) ((he)->v.val)
#define dictGetSignedIntegerVal(he) ((he)->v.s64)
#define dictGetUnsignedIntegerVal(he) ((he)->v.u64)
#define dictGetDoubleVal(he) ((he)->v.d)

关于哈希表大小的宏

// 获取哈希表的大小
#define DICTHT_SIZE(exp) ((exp) == -1 ? 0 : (unsigned long)1<<(exp))
// 哈希表的掩码(size - 1)
#define DICTHT_SIZE_MASK(exp) ((exp) == -1 ? 0 : (DICTHT_SIZE(exp))-1)

dictSlots

// 获取字典的总大小
#define dictSlots(d) (DICTHT_SIZE((d)->ht_size_exp[0])+DICTHT_SIZE((d)->ht_size_exp[1]))

dictSize

// 获取字典保存键值对的总数
#define dictSize(d) ((d)->ht_used[0]+(d)->ht_used[1])

关于 rehash 的宏

// 判断当前是否在 rehash
#define dictIsRehashing(d) ((d)->rehashidx != -1)
// 暂停 rehash
#define dictPauseRehashing(d) (d)->pauserehash++
// 恢复 rehash
#define dictResumeRehashing(d) (d)->pauserehash--

创建/销毁/修改字典

创建字典

创建字典的接口是 dictCreate

dictCreate 为字典结构分配空间,并调用 _dictInit 进行初始化

在 _dictinit 中,又调用了 _dicrReset 进行成员设置

/* Create a new hash table */
dict *dictCreate(dictType *type)

    dict *d = zmalloc(sizeof(*d));

    _dictInit(d,type);
    return d;


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


/* Reset hash table parameters already initialized with _dictInit()*/
static void _dictReset(dict *d, int htidx)

    d->ht_table[htidx] = NULL;
    d->ht_size_exp[htidx] = -1;
    d->ht_used[htidx] = 0;

在 _dictInit 中有一个宏 DICT_OK,它的定义为:

#define DICT_OK 0
#define DICT_ERR 1

用来标识操作是否成功完成

修改字典

一般修改字典都是为 rehash 做准备,或者扩大容量或者缩小容量。

dictResize

缩小字典大小,前提是当前没有在进行 rehash 以及 dict_can_resize 变量不为 0

dict_can_resize 是 dict.c 中定义的一个全局变量,用来指示 resize 能否进行

static int dict_can_resize = 1;

什么时候 dict_can_resize 会为0呢?

当 redis 在后台进行持久化时, 为了最大化地利用系统的 copy on write 机制,会暂时地将 dict_can_resize 设置为0,避免执行自然 rehash,从而减少程序对内存的碰撞。持久化任务完成后,dict_can_resize 又会设置为1

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

    unsigned long minimal;

    if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;
    minimal = d->ht_used[0];
    if (minimal < DICT_HT_INITIAL_SIZE)
        minimal = DICT_HT_INITIAL_SIZE;
    return dictExpand(d, minimal);

缩小哈希表的大小,新的容量刚好能容纳已有元素。minimal 等于已有元素的数量,然后在 _dictExpand 中,会将新容量的大小设置为刚好大于等于 minimal 的 2 的幂。

dictExpand

再来看 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;

_dictExpand 函数不是只为了 rehash,还可以初始化字典。 _dictExpand 函数判断是否是 rehash 是通过判断 ht_table[0] 是否为空来判断的。也就是说如果调用 _dictExpand 的字典是非空的,则增容后的哈希表是放在 ht_table[1] 中的,所以需要调用者手动释放 ht_table[0],将 ht_table[1] 放到 ht_table[0] 位置上。

Rehash

rehash 扩容分为两种:

  1. 自然 rehash:used / size >= 1 时且 dict_can_resize 为1
  2. 强制 rehash:used / size > dict_force_resize_ratio 该版本中 dict_force_resize_ratio 的值为5

另外,当哈希表的负载因子小于 0.1 时,程序自动开始对哈希表执行收缩操作。

Rehash 过程:创建一个新的哈希表 ht[1],rehashidx 变成0,根据 rehashidx 指向的桶进行数据的迁移。当所有数据迁移完毕时,释放 ht[0],将 ht[1] 迁移到 ht[0],ht[1] 置为空。

下面是 Rehash 的一个简单示例:

Rehash 之前:

Rehash 开始:

所有元素重新映射到 ht_table[1] :

表的转移,完成 Rehash:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oOmONTWE-1652675352252)(C:\\Users\\晏思俊\\AppData\\Roaming\\Typora\\typora-user-images\\image-20220516122546788.png)]

Rehash 不是一步就完成的,否则数据量特别大时,迁移数据会是一个很大的工程,可能会导致服务器暂停服务来迁移数据,Rehash 是渐进式的,数据的迁移发生在对数据的增删改查时,这样就将迁移平摊在每个增删改查的操作上。

如果在 rehash 期间要执行添加键的操作,都是在 ht[1] 中进行的,而进行删改查等操作,会同时在两张表中进行。

每次 rehash 都是以 n 个桶为单位的,将每个桶上的链都移到新的哈希表上。一次 rehash 完成以后,如果之后还有桶要 rehash,则返回1,如果 rehash 完成,则返回0。但是实际上,rehashidx 指向的桶可能是空桶,所以为了效率,一次 rehash 最多要遍历 10*n 个空桶,遍历完了 10 * n 个空桶就会返回。

int dictRehash(dict *d, int n) 
    int empty_visits = n*10; /* Max number of empty buckets to visit. */
    if (!dictIsRehashing(d)) return 0;

    while(n-- && d->ht_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);
        while(d->ht_table[0][d->rehashidx] == NULL) 
            d->rehashidx++;
            if (--empty_visits == 0) return 1;
        
        de = d->ht_table[0][d->rehashidx];
        /* Move all the keys in this bucket from the old to the new hash HT */
        while(de) 
            uint64_t h;

            nextde = de->next;
            /* Get the index in the new hash table */
            h = dictHashKey(d, de->key) & DICTHT_SIZE_MASK(d->ht_size_exp[1]Flask 学习-88. jsonify() 函数源码解读深入学习

redis学习记录:字典(dict)源码分析

redis学习记录:字典(dict)源码分析

redis学习记录:字典(dict)源码分析

Redis源码解读——sds

springboot自动配置源码解读以及jdbc和redis配置原理小点