Redis源码:dict数据结构(声明)

本文详细解析Redis的dict数据结构,基于哈希表实现,采用开放寻址法处理冲突,通过dictEntry结构体存储键值对。文章介绍了dictEntry的key、value(union类型)和next(解决冲突的链表节点)成员。哈希表结构包括数组、size、sizemask和used等属性,用于动态调整和优化性能。增量重哈希策略用于避免一次性复制所有元素,提高效率。
摘要由CSDN通过智能技术生成

Redis的dict,译为“字典”,犹如Python里的字典,底层是用哈希表实现的,好的实现可以达到O(1)的时间复杂度。众所周知,哈希表其实就是一个数组,一个元素插入到哈希表时,先通过一个哈希函数将键值映射到一个下标。存储和读取都是如此。(ps,如果对哈希表这个数据结构一无所知,最好先自己学习完、实现一遍,再来看Redis的源码)

由于可能会有多个元素映射到同一个下标,即会产生“冲突问题”,那就需要选择一种方案来解决,总的有两种方案,一种是开放寻址法,也就是在同一个位置做一个链表;一种是重新在表里找一个位置来放置它,比如线性探查法、平方探查法、双散列法、再散列法。

哈希表的元素:dictEntry

如上所说,哈希表就是一个数组,那么问题来了,这个数组里的元素的类型是什么?

值得注意的是,一个元素一般是(键: 值)的形式,比如(“jacket”: 666)。我们要能容纳各种类型的元素,无论是键的类型,还是值的类型。这在Python这种弱类型语言里写起来很方便,但是Redis是用C语言写的,要支持任意类型,该怎么实现呢?

typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;

哈哈,答案是void*
dictEntry这个结构体只有三个数据成员,key很直观。

value成员(代码里简写为v)是一个union,如果不清楚union是什么的话,请自行搜索,这里简单介绍一下。它相当于全部成员里的值都是一样的,只有一份,但是这个值有多种解释。具体一点,别看v有“四个数据成员”就认为它所占用的字节数是8 byte * 4 = 32 byte,这里的v所占的字节数其实只有一份,即8 byte。假设v = 0x 0000 0000 0000 0001,按照上面的定义,可以解释为一个任意类型的指针值,或者说它是一个无符号的64位整数,或者说它是一个有符号的64位整数,又或者说它是一个double类型的浮点数。如果你到现在还无法理解“一份值,有多种解释”的话,那你读多几遍上面这个分析吧,或者先google一圈“C语言的union”再继续往下读。

最后是next成员,它的类型是struct dictEntry *,说明它是一个单链表的结点,也就是它解决哈希表冲突的方法是开放寻址法。

哈希表的结构

先请你再读一遍这句话:“dict底层是用哈希表实现的”,也就是说,我们看到dict的真面目之前,还需要先通过哈希表这一关,先看代码:

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

dictht就是dict hash table的简写,往下看,哎,是不是很纳闷呢,怎么一个哈希表需要这么多元素啊,不就一个数组(dictEntry **table),再加一个大小(size)就可以了吗?怎么还有后面两个成员sizemask和used?

首先是used,这个意思很清晰,就是已经插入了多少元素,为了计算负载因子(load factor,设元素数量为k,哈希表大小为n,那么load factor=k/n),至于为什么要计算这个值,很快会谈到。

然后sizemask是用来干嘛的?这个涉及到哈希表的具体实现,一般是这样的,哈希函数将键值映射到一个很大的范围,比如[0, 999999999999997],然后哈希表的大小比如只有100,那么大于等于100的数字该怎么办呢?取个模呗,比如233 % 100 = 33。但是我们知道,取模运算的效率并不是跟加减法一样快速的,它相对来说是一个很慢的操作!那怎么办呢?

嗯哼,这当然难不倒聪明的程序员们!可以把哈希表的实际大小放大到128(也就是大于等于100的最小的2的整数次幂),然后给定一个二进制的mask,0000 0000 0111 1111,将数值与mask位与一下,就可以实现跟“取模”一样的效果了,而位与操作相对来说是超级快速的。

注意到上面的注释里提到了“incremental rehashing”,什么是“增量重哈希”(直译的名字,别介意)呢?查询维基百科可以知道:
incremental rehashing

上面的英文都很简单,就不翻译了。意思就是,当哈希表的负载因子达到一定程度时,继续插入元素很容易产生很多冲突,从而影响总体的性能(如上所述,Redis用了拉链法,冲突增多会导致链表越来越长,从而使得平均查找时间也会越来越长),所以需要新建一个更大的哈希表来降低冲突的频率。那么问题来了,新建一个哈希表,如果直接一次性把所有的元素复制过去,是很耗时的,对于实时性要求较高的应用来说根本不能忍!那怎么办呢?

不要一次性复制咯,每次插入一个元素到新的表里时,顺便从旧的表里复制r个元素过去(这个r是自己定的一个常数,不能太大,也不要太小)。具体的细节等到看实现的代码再来了解!

现在再回头看看为什么哈希表里面的数组声明为dictEntry **table就很清晰了,因为这个数组的大小是动态变化的,需要是一个动态数组。那为什么元素类型是dictEntry *而不是单纯的dictEntry呢?

我猜,这是为了在复制的时候节省时间。从上面dictEntry的声明知道,它的大小是24个字节(在64位机器上),而复制一个dictEntry *只需要复制8个字节,突然间省了2/3的时间,是不是很开心?复制发生在插入的时候,还有上面讲到的“incremental rehashing”,涉及到复制整张旧的表。所以dictEntry *是很用心的一个设计细节,给作者赞一个!

dict来了

终于见到了dict的庐山真面目!

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

嘿?dictType是什么鬼?这个在前面跳过了,它的定义如下:

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;

可以看出,它就是一堆函数指针的集合,具体干嘛的,从函数的命名上可以看出个大概,不过暂且不管它,等下一篇分析实现的时候再回头来看!

h接着是void *privdata,就是private data的缩写,私有数据?用来干嘛的?先不告诉你……

再接着是dictht ht[2],这个前面已经谈到了,增量重哈希,所以就需要两个哈系表。

rehashidx这个也是跟增量重哈希有关的,因为我们每次是只复制r个元素到新的表中,所以需要记录当前已经复制到哪个位置了。当其值为-1的时候表示当前状态没有进行增量重哈希;当其值为非负整数时就表示当前正在进行增量重哈希,并且下一次从rehashidx这里开始复制r个元素到新的表中。

最后是iterators,这个暂时不懂,等实现篇再来详细介绍。ps,跟这个值相关的,还有以下这个声明:

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

typedef void (dictScanFunction)(void *privdata, const dictEntry *de);

各种define

下面这个是哈希表的初始大小……(废话)

/* This is the initial size of every hash table */
#define DICT_HT_INITIAL_SIZE     4

然后是这个:

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

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

使用宏,对阅读源代码一个很不好的地方就是,你不知道参数是什么类型的……不过自己看看使用它们的代码也能知道,这里的d表示dict,也就是一个字典,entry是一个元素,_val_表示的是void*的值,类型对应dictEntry.v。

(d)->type->valDestructor就是上面谈到的dictType这个结构体里的东西,我认为它就是一堆回调函数,有点像在模拟C++里面的类成员函数。上面代码的意思是,如果某个函数指针不为空,就调用它,否则就……问题是为什么用do … while(0)呢?虽然这个东西的效果跟调用一次是一样的(因为0代表false,所以while(0)就直接退出了)。这个涉及到define的一个深坑,有兴趣可以看这个博客的介绍:do {…} while (0) 在宏定义中的作用,大意就是,这样子写更好更安全!

接下来这几个很容易理解啦,就是以多种方式设置dictEntry.v的值:

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

#define dictSetUnsignedIntegerVal(entry, _val_) \
    do { entry->v.u64 = _val_; } while(0)

#define dictSetDoubleVal(entry, _val_) \
    do { entry->v.d = _val_; } while(0)

再往下都是跟上面差不多的定义,我觉得如果连这些都看不懂……那得好好练习基本功而不是继续看代码了(中性的劝诫):

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

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

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

#define dictHashKey(d, key) (d)->type->hashFunction(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 dictSlots(d) ((d)->ht[0].size+(d)->ht[1].size)
#define dictSize(d) ((d)->ht[0].used+(d)->ht[1].used)
#define dictIsRehashing(d) ((d)->rehashidx != -1)

API

最后这些先略过,在下一篇实现篇里介绍。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值