Redis源码分析(四)字典-dict

概述

Redis中的dict字典可以理解为key和value映射关系的升级版本的散列链表。dict字典的出现是为了解决算法中的查找问题,在实际开发中查找主要使两种结构体,他们分别是使用Map结构的树与Hash表。使用Hash表找到的优势是,在没有Hash碰撞的情况下,查找性能能达到O(1),并且它的内部实现也比较简单。dict字典借鉴了多个经典的算法实现Hash的key计算。dict字典在发生冲突时采用拉链法解决冲突,它的内部维护了两个hash表,当装载因子(节点数和字典大小之间的比率接近 1:1,且已使用节点数和字典大小之间的比率超过 5),会发生增量式重哈希,把表1的数据迁移到表2中,迁移完成后把表1指向表2的地址。

3.0版本Antirez 为了支持懒惰删除,在主线程执行重Hash,但是此做法会使得函数实现臃肿,每个增删改查都要配备一次重Hash,也会导致CPU 资源占用过多,甚至可能阻塞主线程。最新版本中, Antirez 使用了最佳的方案——异步线程,释放内存不用为每种数据结构适配一套渐进式释放策略。

结构体

可以看到dict字典实现的结构体嵌套比较多。dict结构体中,dictType是字典的指针函数,一些对节点通用性操作的定义都在其中;privdata是属性保存了需要传给那些类型特定函数的可选参数;dictht是Hash表,它有两张Hash表,表1是源表,满足一定条件时,数据会把表1迁移到表2中;rehashidx属性用来判断当前是否在进行数据迁移;iterators表示当前正在遍历的迭代器的数量。dictht结构体中,**table是一个数组,它的每一个元素都是dictEntry类型的,它包含了k、v键值对,还有下一个节点的next指针。value是一个union联合体,它在内存中,地址是连在一起的,同时value可以存储三种类型的数据,分别是void类型的串,uint64_t(c99规范种定义的 typedef unsigned long long 类型),int64_tc(99规范种定义的 typedef signed long long 类型)。

typedef struct dictEntry {
    
    // 键
    void *key;

    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;

    // 指向下个哈希表节点,形成链表
    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;
typedef struct dictht {
    
    // 哈希表数组
    dictEntry **table;

    // 哈希表大小
    unsigned long size;
    
    // 哈希表大小掩码,用于计算索引值,如果当前哈希表的大小为 16,它的掩码就是二进制值 1111 
    // 总是等于 size - 1
    unsigned long sizemask;

    // 该哈希表已有节点的数量
    unsigned long used;

} dictht;
typedef struct dict {

    // 类型特定函数
    dictType *type;

    // 私有数据
    void *privdata;

    // 哈希表
    dictht ht[2];

    // rehash 索引
    // 当 rehash 不在进行时,值为 -1
    int rehashidx; /* rehashing not in progress if rehashidx == -1 */

    // 目前正在运行的安全迭代器的数量
    int iterators; /* number of iterators currently running */

} dict;

内存图

此图是在没有进行重Hash时的的内存结构图

Redis dict结构体

宏定义

遍历字典回调

这个函数是一个回调函数,在遍历字典时,每当匹配到key后,就会执行一次这个函数。

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

初始化大小

哈希表的初始大小宏定义为4

#define DICT_HT_INITIAL_SIZE     4

宏定义函数

宏都用do/while(0)来包围执行逻辑,因为它能确保宏的行为总是相同的,而不用管在调用代码中使用了多少分号和大括号.

// 释放给定字典节点的值
#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)

// 将一个有符号整数设为节点的值
#define dictSetSignedIntegerVal(entry, _val_) \
    do { entry->v.s64 = _val_; } while(0)

// 将一个无符号整数设为节点的值
#define dictSetUnsignedIntegerVal(entry, _val_) \
    do { entry->v.u64 = _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 dictSlots(d) ((d)->ht[0].size+(d)->ht[1].size)
// 返回字典的已有节点数量
#define dictSize(d) ((d)->ht[0].used+(d)->ht[1].used)
// 查看字典是否正在 rehash
#define dictIsRehashing(ht) ((ht)->rehashidx != -1)

哈希表类型

用关键字 extern 对该变量作“外部变量声明”,在此标记存储空间在执行时加载内并初始化为0,就可以从“声明”处起,合法地使用该外部变量。这三用全局变量是Redis在外部文件种构建对象存储时使用.

extern dictType dictTypeHeapStringCopyKey;
extern dictType dictTypeHeapStrings;
extern dictType dictTypeHeapStringCopyKeyValue;

方法实现

全局变量

dict_can_resize用来判断当前是否在进行重Hash,重Hash一般发生在Redis 使用子进程进行保存操作时,这样做可以有效地利用 copy-on-write 机制。如果已使用节点的数量和字典大小之间的比率大于5,那么 rehash 在任何情况下都会(强制)进行。

// 指示字典是否启用 rehash 的标识
static int dict_can_resize = 1;
// 强制 rehash 的比率
static unsigned int dict_force_resize_ratio = 5;

Hash算法

Hash算法在学术界、应用界都是使用很广泛的的存在,算法的思想看似平平无奇,却实现了很魔幻的功能。本文只具体描述dict应用流程,算法方面的具体实现请看相关论文。

dictIntHashFunction Thomas Wang’s算法

Thomas Wang’s算法是dict.c文件中借鉴的,它是用来计算dict的Key的实现方法,我在查阅资料发现是这么形容的:整型的Hash算法使用的是Thomas Wang’s 32 Bit / 64 Bit Mix Function ,这是一种基于位移运算的散列方法。基于移位的散列是使用Key值进行移位操作。通常是结合左移和右移, 每个移位过程的结果进行累加,最后移位的结果作为最终结果,这种方法的好处是避免了乘法运算,从而提高Hash函数本身的性能。

unsigned int dictIntHashFunction(unsigned int key)
{
    key += ~(key << 15);
    key ^=  (key >> 10);
    key +=  (key << 3);
    key ^=  (key >> 6);
    key += ~(key << 11);
    key ^=  (key >> 16);
    return key;
}
dictGenHashFunction MurmurHash2, by Austin Appleby 算法

这个算法对当前的机器的行为做了一些假设:可以从任何地址读取一个 4 字节的值而不会崩溃;sizeof(int) == 4。它有两个限制:不会增量工作;在 little-endian (低位字节排放在内存的低地址端,高位字节排放在内存的高地址端)和big-endian(数据的低位保存在内存的高地址中,而数据的高位保存在内存的低地址中) 机器上不会产生相同的结果。

static uint32_t dict_hash_function_seed = 5381;

void dictSetHashFunctionSeed(uint32_t seed) {
    dict_hash_function_seed = seed;
}

uint32_t dictGetHashFunctionSeed(void) {
    return dict_hash_function_seed;
}
unsigned int dictGenHashFunction(const void *key, int len) {
    /* 'm' and 'r' are mixing constants generated offline.
     They're not really 'magic', they just happen to work well.  */
    uint32_t seed = dict_hash_function_seed;
    const uint32_t m = 0x5bd1e995;
    const int r = 24;

    /* Initialize the hash to a 'random' value */
    uint32_t h = seed ^ len;

    /* Mix 4 bytes at a time into the hash */
    const unsigned char *data = (const unsigned char *)key;

    while(len >= 4) {
        uint32_t k = *(uint32_t*)data;

        k *= m;
        k ^= k >> r;
        k *= m;

        h *= m;
        h ^= k;

        data += 4;
        len -= 4;
    }

    /* Handle the last few bytes of the input array  */
    switch(len) {
    case 3: h ^= data[2] << 16;
    case 2: h ^= data[1] << 8;
    case 1: h ^= data[0]; h *= m;
    };

    /* Do a few final mixes of the hash to ensure the last few
     * bytes are well-incorporated. */
    h ^= h >> 13;
    h *= m;
    h ^= h >> 15;

    return (unsigned int)h;
}
dictGenCaseHashFunction 基于 djb 哈希

基于 djb 哈希的不区分大小写的哈希函数

unsigned int dictGenCaseHashFunction(const unsigned char *buf, int len) {
    unsigned int hash = (unsigned int)dict_hash_function_seed;

    while (len--)
        hash = ((hash << 5) + hash) + (tolower(*buf++)); /* hash * 33 + c */
    return hash;
}

_dictReset 初始化Hash表

_dictReset是一全局使用的函数,它实现了重置(或初始化)给定哈希表的各项属性值,在外部调用dict时,也可以使用。

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

dictCreate _dictInit 创建字典

初始化一个字典,使用zmalloc创建空间,可以看到它没有使用sizeof计算dict,而是直接调用*d计算结构体的大小。_dictInit函数,首先会初始化两张Hash表的属性,再初始化构造dictType函数,privDataPtr是设置需要传给dictType中的特定函数的可选参数,设置重Hash的状态为停止,遍历器数量为0。

返回值:DICT_OK=0;DICT_ERR=1。

dict *dictCreate(dictType *type,
        void *privDataPtr)
{
    dict *d = zmalloc(sizeof(*d));

    _dictInit(d,type,privDataPtr);

    return d;
}

int _dictInit(dict *d, dictType *type,
        void *privDataPtr)
{
    // 初始化两个哈希表的各项属性值
    // 但暂时还不分配内存给哈希表数组
    _dictReset(&d->ht[0]);
    _dictReset(&d->ht[1]);

    // 设置类型特定函数
    d->type = type;

    // 设置私有数据
    d->privdata = privDataPtr;

    // 设置哈希表 rehash 状态
    d->rehashidx = -1;

    // 设置字典的安全迭代器数量
    d->iterators = 0;

    return DICT_OK;
}

dictResize 缩小字典的空间

如果dict_can_resize为假或者d->rehashidx=-1的情况下不能缩小字典,会直接退出函数。如果字典使用的节点数量小于字典最小空间(DICT_HT_INITIAL_SIZE=4),会设置minimal,最小缩小空间为4。

int dictResize(dict *d)
{
    int minimal;

    // 不能在关闭 rehash 或者正在 rehash 的时候调用
    if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;

    // 计算让比率接近 1:1 所需要的最少节点数量
    minimal = d->ht[0].used;
    if (minimal < DICT_HT_INITIAL_SIZE)
        minimal = DICT_HT_INITIAL_SIZE;

    // 调整字典的大小
    // T = O(N)
    return dictExpand(d, minimal);
}

dictExpand 创建一个新的哈希表

_dictNextPower会计算出第一个大于等于 size 的 2 的 N 次方,用来做新Hash表的大小。在重Hash或者Hash表1使用节点长度大于size时会错误退出函数。如果哈希表 0 为空,把新建立的哈希表的地址给0号Hash表,如果不为空,表示此时重Hash结束,把新建立的哈希表的地址给1号Hash表。

int dictExpand(dict *d, unsigned long size)
{
    // 新哈希表
    dictht n; /* the new hash table */

    // 根据 size 参数,计算哈希表的大小
    // T = O(1)
    unsigned long realsize = _dictNextPower(size);

    /* the size is invalid if it is smaller than the number of
     * elements already inside the hash table */
    // 不能在字典正在 rehash 时进行
    // size 的值也不能小于 0 号哈希表的当前已使用节点
    if (dictIsRehashing(d) || d->ht[0].used > size)
        return DICT_ERR;

    /* Allocate the new hash table and initialize all pointers to NULL */
    // 为哈希表分配空间,并将所有指针指向 NULL
    n.size = realsize;
    n.sizemask = realsize-1;
    // T = O(N)
    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 */
    // 如果 0 号哈希表非空,那么这是一次 rehash :
    // 程序将新哈希表设置为 1 号哈希表,
    // 并将字典的 rehash 标识打开,让程序可以开始对字典进行 rehash
    d->ht[1] = n;
    d->rehashidx = 0;
    return DICT_OK;
}

dictRehash 重Hash(表0迁移到表1)

重Hash执行之前,首先要修改d->rehashidx的状态为正在执行。n可以理解为步数(执行次数),de为当前遍历到的哈希节点,nextde为下一个哈希节点。它的实现没有用递归,因为Redis一般使用子线程执行此函数,可能会不停的有新节点加入进行,导致栈溢出。used==0是循环的边界,它表示重Hash执行结束,它会释放原来分配的空间,把新表1的的空间的地址给表0,最后重置表1,关闭重Hash。assert是C语言的断言处理,有点类似与面向对象的手动抛出异常,rehashidx在循环时,用来记录遍历节点的数量,当size的大小大于rehashidx,当前遍历过界,异常中断程序。在遍历链表之前会while循环找到首节点节点,它这样做是因为Hash表是散列的,计算的hash值取到的地址,在内存中不能是连续的存在,可能到第10个,第100个节点才是首节点。dictHashKey函数计算出hash值,hash值再与节点的sizemask进行位运算出节点插入的索引位置,最后更新各个Hash表的used使用情况。当循环完毕后,会把表0置为NULL,rehashidx自增1,下一次 while(n–) 循环时,会置换两张Hash表,最后重置表1退出。

int dictRehash(dict *d, int n) {

    // 只可以在 rehash 进行中时执行
    if (!dictIsRehashing(d)) return 0;

    // 进行 N 步迁移
    // T = O(N)
    while(n--) {
        dictEntry *de, *nextde;

        /* Check if we already rehashed the whole table... */
        // 如果 0 号哈希表为空,那么表示 rehash 执行完毕
        // T = O(1)
        if (d->ht[0].used == 0) {
            // 释放 0 号哈希表
            zfree(d->ht[0].table);
            // 将原来的 1 号哈希表设置为新的 0 号哈希表
            d->ht[0] = d->ht[1];
            // 重置旧的 1 号哈希表
            _dictReset(&d->ht[1]);
            // 关闭 rehash 标识
            d->rehashidx = -1;
            // 返回 0 ,向调用者表示 rehash 已经完成
            return 0;
        }

        /* Note that rehashidx can't overflow as we are sure there are more
         * elements because ht[0].used != 0 */
        // 确保 rehashidx 没有越界
        assert(d->ht[0].size > (unsigned)d->rehashidx);

        // 略过数组中为空的索引,找到下一个非空索引
        while(d->ht[0].table[d->rehashidx] == NULL) d->rehashidx++;

        // 指向该索引的链表表头节点
        de = d->ht[0].table[d->rehashidx];
        /* Move all the keys in this bucket from the old to the new hash HT */
        // 将链表中的所有节点迁移到新哈希表
        // T = O(1)
        while(de) {
            unsigned int 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;
        // 更新 rehash 索引
        d->rehashidx++;
    }

    return 1;
}

timeInMilliseconds 获取当前时间

获取以毫秒为单位的 UNIX 时间戳

long long timeInMilliseconds(void) {
    struct timeval tv;
    gettimeofday(&tv,NULL);
    return (((long long)tv.tv_sec)*1000)+(tv.tv_usec/1000);
}

dictRehashMilliseconds 在时间段内执行重Hash

start变量获取到当前的时间,在ms时间内,以每次100步的执行重Hash,最后返回执行的步长。

int dictRehashMilliseconds(dict *d, int ms) {
    // 记录开始时间
    long long start = timeInMilliseconds();
    int rehashes = 0;

   // 在 0 号哈希表为空的情况下终止循环,表示 rehash 执行完毕
    while(dictRehash(d,100)) {
        rehashes += 100;
        // 如果时间已过,跳出
        if (timeInMilliseconds()-start > ms) break;
    }

    return rehashes;
}

_dictRehashStep 根绝条件执行重Hash

字典有安全迭代器的情况下不能进行 rehash, 因为不同的迭代和修改操作可能会弄乱字典。在dict的增删改查操作中都会被调用。

static void _dictRehashStep(dict *d) {
    if (d->iterators == 0) dictRehash(d,1);
}

dictAdd dictAddRaw 增加节点

dictAdd函数会调用dictAddRaw函数进行增加节点操作,如果d->rehashidx为真,会进行一次重Hash。在Hash表重,key是唯一的存在,如果key存在了会返回NULL。如果字典正在 rehash ,那么将新键添加到 1 号哈希表,否则将新键添加到 0 号哈希表。把新创建的dictEntry插入到Hash表的表头,调用宏函数dictSetKey,设置节点的的键,最后,返回dictEntry指针,在指针不为空的情况下把val设置到Hash表的节点中。

int dictAdd(dict *d, void *key, void *val)
{
    // 尝试添加键到字典,并返回包含了这个键的新哈希节点
    // T = O(N)
    dictEntry *entry = dictAddRaw(d,key);

    // 键已存在,添加失败
    if (!entry) return DICT_ERR;

    // 键不存在,设置节点的值
    // T = O(1)
    dictSetVal(d, entry, val);

    // 添加成功
    return DICT_OK;
}
dictEntry *dictAddRaw(dict *d, void *key)
{
    int index;
    dictEntry *entry;
    dictht *ht;

    // 如果条件允许的话,进行单步 rehash
    // T = O(1)
    if (dictIsRehashing(d)) _dictRehashStep(d);

    /* Get the index of the new element, or -1 if
     * the element already exists. */
    // 计算键在哈希表中的索引值
    // 如果值为 -1 ,那么表示键已经存在
    // T = O(N)
    if ((index = _dictKeyIndex(d, key)) == -1)
        return NULL;

    // T = O(1)
    /* Allocate the memory and store the new entry */
    // 如果字典正在 rehash ,那么将新键添加到 1 号哈希表
    // 否则,将新键添加到 0 号哈希表
    ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
    // 为新节点分配空间
    entry = zmalloc(sizeof(*entry));
    // 将新节点插入到链表表头
    entry->next = ht->table[index];
    ht->table[index] = entry;
    // 更新哈希表已使用节点数量
    ht->used++;

    /* Set the hash entry fields. */
    // 设置新节点的键
    // T = O(1)
    dictSetKey(d, entry, key);

    return entry;
}

dictReplace 添加节点,如果存在删除旧节点

流程比较简单,首先增加键值到字典中,如果存在,将会找个这个节点entry,设置新值,最后释放掉这个旧的auxentry。

int dictReplace(dict *d, void *key, void *val)
{
    dictEntry *entry, auxentry;

    /* Try to add the element. If the key
     * does not exists dictAdd will suceed. */
    // 尝试直接将键值对添加到字典
    // 如果键 key 不存在的话,添加会成功
    // T = O(N)
    if (dictAdd(d, key, val) == DICT_OK)
        return 1;

    /* It already exists, get the entry */
    // 运行到这里,说明键 key 已经存在,那么找出包含这个 key 的节点
    // T = O(1)
    entry = dictFind(d, key);
    /* Set the new value and free the old one. Note that it is important
     * to do that in this order, as the value may just be exactly the same
     * as the previous one. In this context, think to reference counting,
     * you want to increment (set), and then decrement (free), and not the
     * reverse. */
    // 先保存原有的值的指针
    auxentry = *entry;
    // 然后设置新的值
    // T = O(1)
    dictSetVal(d, entry, val);
    // 然后释放旧值
    // T = O(1)
    dictFreeVal(d, &auxentry);

    return 0;
}

dictReplaceRaw 查找或增加一个节点

调用dictFind函数,传递dict与 key 在字典中查找节点,如果找到了就会返回节点,没有找到,会返回一个设置了key但是没有value的节点。

dictEntry *dictReplaceRaw(dict *d, void *key) {
    
    // 使用 key 在字典中查找节点
    // T = O(1)
    dictEntry *entry = dictFind(d,key);

    // 如果节点找到了直接返回节点,否则添加并返回一个新节点
    // T = O(N)
    return entry ? entry : dictAddRaw(d,key);
}

dictGenericDelete 根据key删除节点

h是计算出来的hash值,idx是哈希表节点数组的索引,he是当前节点,prevHe是指向he的前一个节点,table是当前Hash表的下标。通过 *key计算出Hash值,遍历两张Hash表, h & d->ht[table].sizemask相当于给Hash值做截取运算,只取后面的的几位。当定位到idx后,进行遍历Hash节点的链表,通过dictCompareKeys函数对比key是否相等,如果上一个节点是有值的,将会把上一个节点的指针,指向当前的节点的下一个地址,如果上一个节点是没有值的,表示节点不在链表上,会把索引为idx的节点直接指向当前节点的下一个节点。nofree取反为真释放节点的内存空间。如果遍历完毕后还未曾找到key,判断此时是否在重Hash,如果没有重Hash,代表着Hash表1此时没有数据,就不会遍历第二次for (table = 0; table <= 1; table++)。

static int dictGenericDelete(dict *d, const void *key, int nofree)
{
    unsigned int h, idx;
    dictEntry *he, *prevHe;
    int table;

    // 字典(的哈希表)为空
    if (d->ht[0].size == 0) return DICT_ERR; /* d->ht[0].table is NULL */

    // 进行单步 rehash ,T = O(1)
    if (dictIsRehashing(d)) _dictRehashStep(d);

    // 计算哈希值
    h = dictHashKey(d, key);

    // 遍历哈希表
    // T = O(1)
    for (table = 0; table <= 1; table++) {

        // 计算索引值 
        idx = h & d->ht[table].sizemask;
        // 指向该索引上的链表
        he = d->ht[table].table[idx];
        prevHe = NULL;
        // 遍历链表上的所有节点
        // T = O(1)
        while(he) {
        
            if (dictCompareKeys(d, key, he->key)) {
                // 超找目标节点

                /* Unlink the element from the list */
                // 从链表中删除
                if (prevHe)
                    prevHe->next = he->next;
                else
                    d->ht[table].table[idx] = he->next;

                // 释放调用键和值的释放函数?
                if (!nofree) {
                    dictFreeKey(d, he);
                    dictFreeVal(d, he);
                }
                
                // 释放节点本身
                zfree(he);

                // 更新已使用节点数量
                d->ht[table].used--;

                // 返回已找到信号
                return DICT_OK;
            }

            prevHe = he;
            he = he->next;
        }

        // 如果执行到这里,说明在 0 号哈希表中找不到给定键
        // 那么根据字典是否正在进行 rehash ,决定要不要查找 1 号哈希表
        if (!dictIsRehashing(d)) break;
    }

    // 没找到
    return DICT_ERR; /* not found */
}

dictDelete dictDeleteNoFree 删除节点

dictDelete:删除节点并且释放空间

dictDeleteNoFree:删除节点并且释放空间

int dictDelete(dict *ht, const void *key) {
    return dictGenericDelete(ht,key,0);
}

int dictDeleteNoFree(dict *ht, const void *key) {
    return dictGenericDelete(ht,key,1);
}

_dictClear 删除一张Hasn表所有节点

这个函数首先会遍历dictht中的dictEntry **table哈希数组,如果传入了callback回调函数,并且,当前的i没有操出范围,会把私有数据域传递进入callback中。(i & 65535)这条命令中int的范围是大于65535的,但是循环移位时不需要这么长的范围,所以65535的作用是将移位后的高位修剪掉。如果索引中的链表不为空,就会删除键值,最后释放掉整个Hash表分配的空间,并把它重置初始化。

int _dictClear(dict *d, dictht *ht, void(callback)(void *)) {
    unsigned long i;

    /* Free all the elements */
    // 遍历整个哈希表
    // T = O(N)
    for (i = 0; i < ht->size && ht->used > 0; i++) {
        dictEntry *he, *nextHe;

        if (callback && (i & 65535) == 0) callback(d->privdata);

        // 跳过空索引
        if ((he = ht->table[i]) == NULL) continue;

        // 遍历整个链表
        // T = O(1)
        while(he) {
            nextHe = he->next;
            // 删除键
            dictFreeKey(d, he);
            // 删除值
            dictFreeVal(d, he);
            // 释放节点
            zfree(he);

            // 更新已使用节点计数
            ht->used--;

            // 处理下个节点
            he = nextHe;
        }
    }

    /* Free the table and the allocated cache structure */
    // 释放哈希表结构
    zfree(ht->table);

    /* Re-initialize the table */
    // 重置哈希表属性
    _dictReset(ht);

    return DICT_OK; /* never fails */
}

dictRelease 释放字典

释放两张Hash表的内存空间,没有传递回调函数,最后释放掉dict字典。

void dictRelease(dict *d)
{
    // 删除并清空两个哈希表
    _dictClear(d,&d->ht[0],NULL);
    _dictClear(d,&d->ht[1],NULL);
    // 释放节点结构
    zfree(d);
}

dictFind 返回字典中包含键 key 的节点

此函数的查找逻辑与dictGenericDelete函数的查找逻辑实现的一致,具体可以看dictGenericDelete的解读。

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

    // 字典(的哈希表)为空
    if (d->ht[0].size == 0) return NULL; /* We don't have a table at all */

    // 如果条件允许的话,进行单步 rehash
    if (dictIsRehashing(d)) _dictRehashStep(d);

    // 计算键的哈希值
    h = dictHashKey(d, key);
    // 在字典的哈希表中查找这个键
    // T = O(1)
    for (table = 0; table <= 1; table++) {

        // 计算索引值
        idx = h & d->ht[table].sizemask;

        // 遍历给定索引上的链表的所有节点,查找 key
        he = d->ht[table].table[idx];
        // T = O(1)
        while(he) {

            if (dictCompareKeys(d, key, he->key))
                return he;

            he = he->next;
        }

        // 如果程序遍历完 0 号哈希表,仍然没找到指定的键的节点
        // 那么程序会检查字典是否在进行 rehash ,
        // 然后才决定是直接返回 NULL ,还是继续查找 1 号哈希表
        if (!dictIsRehashing(d)) return NULL;
    }

    // 进行到这里时,说明两个哈希表都没找到
    return NULL;
}

dictFetchValue 通过key获取值

此函数调用dictFind函数通过key定位到节点,如果节点he不为空,返回value,否者返回NULL。

void *dictFetchValue(dict *d, const void *key) {
    dictEntry *he;

    // T = O(1)
    he = dictFind(d,key);

    return he ? dictGetVal(he) : NULL;
}

dictFingerprint 获取唯一指纹

翻译它的注释这样写道:指纹是一个 64 位的数字,代表字典在给定时间的状态,它只是几个字典属性异或在一起。当一个不安全的迭代器被初始化时,我们得到dict指纹,当迭代器被释放时再次检查指纹。如果两个指纹不同,则表示迭代器的用户在迭代时对字典执行了禁止操作。

此算法是用来dict在迭代开始和结束时各计算两次,计算通过表1、2的大小和使用情况还有首地址进行Hash算法,用作判断在迭代时是否进行了修改删除的非法操作。

long long dictFingerprint(dict *d) {
    long long integers[6], hash = 0;
    int j;

    integers[0] = (long) d->ht[0].table;
    integers[1] = d->ht[0].size;
    integers[2] = d->ht[0].used;
    integers[3] = (long) d->ht[1].table;
    integers[4] = d->ht[1].size;
    integers[5] = d->ht[1].used;

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

dictGetIterator dictGetSafeIterator 创建一个迭代器

迭代器的创建比较简单,首先掉用zmalloc分配空间,把iter->d指向将要迭代的字典的首地址,table用来标识表0或者表1,索引位置置为-1,当前迭代的到的地址与下个一个节点的地址都置为空。dictGetSafeIterator函数调用dictGetIterator进行创建,创建完毕后,把safe安全状态置为1。fingerprintd的值调用dictFingerprint 获取指纹(密钥)。

typedef struct dictIterator {
        
    // 被迭代的字典
    dict *d;

    // table :正在被迭代的哈希表号码,值可以是 0 或 1 。
    // index :迭代器当前所指向的哈希表索引位置。
    // safe :标识这个迭代器是否安全
    int table, index, safe;

    // entry :当前迭代到的节点的指针
    // nextEntry :当前迭代节点的下一个节点
    //             因为在安全迭代器运作时, entry 所指向的节点可能会被修改,
    //             所以需要一个额外的指针来保存下一节点的位置,
    //             从而防止指针丢失
    dictEntry *entry, *nextEntry;

    long long fingerprint; /* unsafe iterator fingerprint for misuse detection */
} dictIterator;
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;
}
dictIterator *dictGetSafeIterator(dict *d) {
    dictIterator *i = dictGetIterator(d);

    // 设置安全迭代器标识
    i->safe = 1;

    return i;
}

dictNext 获取迭代器指向的下一个节点

这个函数使用while true循环遍历,如果当前的迭代器指向的当前节点entry是空的,就把当前table表的地址给*ht,如果此时索引等于-1并且table表是0号表,那么此次是第一次迭代,就会判断当前是否是安全的迭代器(只读不写)就把iterators的值自增1,否者计算出指纹的值,用作遍历结束,判断是否被危险遍历。如果索引的值的大小超过了当前hash表的大小(signed 无符号) ht->size ,判断此时是否在重Hash并且当前遍历的是表0,那么将会把迭代器的地址给到ht,如果没有将会结束循环。最后,设置entry指向的地址,判断entry是否为空,不为空就记录下迭代器下一个将要遍历的地址。

dictEntry *dictNext(dictIterator *iter)
{
    while (1) {

        // 进入这个循环有两种可能:
        // 1) 这是迭代器第一次运行
        // 2) 当前索引链表中的节点已经迭代完(NULL 为链表的表尾)
        if (iter->entry == NULL) {

            // 指向被迭代的哈希表
            dictht *ht = &iter->d->ht[iter->table];

            // 初次迭代时执行
            if (iter->index == -1 && iter->table == 0) {
                // 如果是安全迭代器,那么更新安全迭代器计数器
                if (iter->safe)
                    iter->d->iterators++;
                // 如果是不安全迭代器,那么计算指纹
                else
                    iter->fingerprint = dictFingerprint(iter->d);
            }
            // 更新索引
            iter->index++;

            // 如果迭代器的当前索引大于当前被迭代的哈希表的大小
            // 那么说明这个哈希表已经迭代完毕
            if (iter->index >= (signed) ht->size) {
                // 如果正在 rehash 的话,那么说明 1 号哈希表也正在使用中
                // 那么继续对 1 号哈希表进行迭代
                if (dictIsRehashing(iter->d) && iter->table == 0) {
                    iter->table++;
                    iter->index = 0;
                    ht = &iter->d->ht[1];
                // 如果没有 rehash ,那么说明迭代已经完成
                } else {
                    break;
                }
            }

            // 如果进行到这里,说明这个哈希表并未迭代完
            // 更新节点指针,指向下个索引链表的表头节点
            iter->entry = ht->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;
        }
    }

    // 迭代完毕
    return NULL;
}

dictReleaseIterator 销毁迭代器

销毁迭代器实现比较简单,首先判断这个迭代器是否使用了,如果使用过了,就会判断是否是安全迭代器,是安全的就会先把安全迭代器的数量减一,否者就使用DEBUG断言机制,判断指纹是否一致,也就是判断字典是否被修改过,如果修改过程序会异常终止。最后调用zfree销毁这个迭代器。

void dictReleaseIterator(dictIterator *iter) {

   if (!(iter->index == -1 && iter->table == 0)) {
      // 释放安全迭代器时,安全迭代器计数器减一
      if (iter->safe) {
         iter->d->iterators--;
         // 释放不安全迭代器时,验证指纹是否有变化
      } else
         assert(iter->fingerprint == dictFingerprint(iter->d));
   }
   zfree(iter);
}

dictGetRandomKey 随机返回一个节点

如果字典的大小为0会执行返回NULL,在查找节点也会单步进行一次重Hash,如果在重Hash,将会使用查询表1,否则只会查询表0。random()是在stdlib.h中的一个宏定义。当随机定位了一个 dictEntry *he节点后,计算出这个链表的长度,再次使用random()取长度的余计算出将要返回节点的索引。

dictEntry *dictGetRandomKey(dict *d) {
   dictEntry *he, *orighe;
   unsigned int h;
   int listlen, listele;

   // 字典为空
   if (dictSize(d) == 0) { return NULL; }

   // 进行单步 rehash
   if (dictIsRehashing(d)) { _dictRehashStep(d); }

   // 如果正在 rehash ,那么将 1 号哈希表也作为随机查找的目标
   if (dictIsRehashing(d)) {
      // T = O(N)
      do {
         h = random() % (d->ht[0].size + d->ht[1].size);
         he = (h >= d->ht[0].size) ? d->ht[1].table[h - d->ht[0].size] :
              d->ht[0].table[h];
      } while (he == NULL);
      // 否则,只从 0 号哈希表中查找节点
   } else {
      // T = O(N)
      do {
         h = random() & d->ht[0].sizemask;
         he = d->ht[0].table[h];
      } while (he == NULL);
   }

   /* Now we found a non empty bucket, but it is a linked
    * list and we need to get a random element from the list.
    * The only sane way to do so is counting the elements and
    * select a random index. */
   // 目前 he 已经指向一个非空的节点链表
   // 程序将从这个链表随机返回一个节点
   listlen = 0;
   orighe = he;
   // 计算节点数量, T = O(1)
   while (he) {
      he = he->next;
      listlen++;
   }
   // 取模,得出随机节点的索引
   listele = random() % listlen;
   he = orighe;
   // 按索引查找节点
   // T = O(1)
   while (listele--) { he = he->next; }

   // 返回随机节点
   return he;
}

dictGetRandomKeys 获取随机key

翻译算法的注释:这是 dictGetRandomKey() 的一个版本,它被修改为通过在哈希表的随机位置跳转并线性扫描条目来返回多个条目。指向哈希表条目的返回指针存储在指向 dictEntry 指针数组的“des”中。数组必须至少有 ‘count’ 个元素的空间,这是我们传递给函数的参数,用于告诉我们需要多少个随机元素。该函数返回存储在 ‘des’ 中的项目数,如果哈希表中的元素少于 ‘count’,则该数量可能小于 ‘count’。请注意,当您需要对返回的项目进行良好分布时,此函数不适合,而仅当您需要“采样”给定数量的连续元素以运行某种算法或生成统计信息时才适合。然而,该函数在生成 N 个元素时比 dictGetRandomKey() 快得多,并且这些元素保证不重复。

int dictGetRandomKeys(dict *d, dictEntry **des, int count) {
   int j; /* internal hash table id, 0 or 1. */
   int stored = 0;

   if (dictSize(d) < count) { count = dictSize(d); }
   while (stored < count) {
      for (j = 0; j < 2; j++) {
         /* Pick a random point inside the hash table 0 or 1. */
         unsigned int i = random() & d->ht[j].sizemask;
         int size = d->ht[j].size;

         /* Make sure to visit every bucket by iterating 'size' times. */
         while (size--) {
            dictEntry *he = d->ht[j].table[i];
            while (he) {
               /* Collect all the elements of the buckets found non
                * empty while iterating. */
               *des = he;
               des++;
               he = he->next;
               stored++;
               if (stored == count) { return stored; }
            }
            i = (i + 1) & d->ht[j].sizemask;
         }
         /* If there is only one table and we iterated it all, we should
          * already have 'count' elements. Assert this condition. */
         assert(dictIsRehashing(d) != 0);
      }
   }
   return stored; /* Never reached. */
}

rev 按位取反

Redis中,这个算法来自于斯坦福大学的edu网站中收录。

http://graphics.stanford.edu/~seander/bithacks.html#ReverseParallel

static unsigned long rev(unsigned long v) {
   unsigned long s = 8 * sizeof(v); // bit size; must be power of 2
   unsigned long mask = ~0;
   while ((s >>= 1) > 0) {
      mask ^= (mask << s);
      v = ((v >> s) & mask) | ((v << s) & ~mask);
   }
   return v;
}

dictScan迭代字典

dictScan() 函数用于迭代给定字典中的元素。这一段的描述借助注释来讲解:迭代所使用的算法是由 Pieter Noordhuis 设计的,算法的主要思路是在二进制高位上对游标进行加法计算也即是说,不是按正常的办法来对游标进行加法计算,而是首先将游标的二进制位翻转(reverse)过来,然后对翻转后的值进行加法计算,最后再次对加法计算之后的结果进行翻转。这样设计是因为在一次完整的迭代过程中,哈希表的大小有可能在两次迭代之间发生改变。哈希表的大小总是 2 的某个次方,并且哈希表使用链表来解决冲突,因此一个给定元素在一个给定表的位置总可以通过 Hash(key) & SIZE-1公式来计算得出,其中 SIZE-1 是哈希表的最大索引值,这个最大索引值就是哈希表的 mask (掩码),举个例子,如果当前哈希表的大小为 16 ,那么它的掩码就是二进制值 1111 ,这个哈希表的所有位置都可以使用哈希值的最后四个二进制位来记录。

因为在 rehash 的时候会出现两个哈希表,它的缺陷在于:

(1) 函数可能会返回重复的元素,不过这个问题可以很容易在应用层解决

(2) 为了不错过任何元素,迭代器需要返回给定字典上的所有键,以及因为扩展哈希表而产生出来的新表,所以迭代器必须在一次迭代中返回多个元素。

unsigned long dictScan(dict *d,
                       unsigned long v,
                       dictScanFunction *fn,
                       void *privdata) {
   dictht *t0, *t1;
   const dictEntry *de;
   unsigned long m0, m1;

   // 跳过空字典
   if (dictSize(d) == 0) { return 0; }

   // 迭代只有一个哈希表的字典
   if (!dictIsRehashing(d)) {

      // 指向哈希表
      t0 = &(d->ht[0]);

      // 记录 mask
      m0 = t0->sizemask;

      /* Emit entries at cursor */
      // 指向哈希桶
      de = t0->table[v & m0];
      // 遍历桶中的所有节点
      while (de) {
         fn(privdata, de);
         de = de->next;
      }

      // 迭代有两个哈希表的字典
   } else {

      // 指向两个哈希表
      t0 = &d->ht[0];
      t1 = &d->ht[1];

      /* Make sure t0 is the smaller and t1 is the bigger table */
      // 确保 t0 比 t1 要小
      if (t0->size > t1->size) {
         t0 = &d->ht[1];
         t1 = &d->ht[0];
      }

      // 记录掩码
      m0 = t0->sizemask;
      m1 = t1->sizemask;

      /* Emit entries at cursor */
      // 指向桶,并迭代桶中的所有节点
      de = t0->table[v & m0];
      while (de) {
         fn(privdata, de);
         de = de->next;
      }

      /* Iterate over indices in larger table that are the expansion
       * of the index pointed to by the cursor in the smaller table */
      // Iterate over indices in larger table             // 迭代大表中的桶
      // that are the expansion of the index pointed to   // 这些桶被索引的 expansion 所指向
      // by the cursor in the smaller table               //
      do {
         /* Emit entries at cursor */
         // 指向桶,并迭代桶中的所有节点
         de = t1->table[v & m1];
         while (de) {
            fn(privdata, de);
            de = de->next;
         }

         /* Increment bits not covered by the smaller mask */
         v = (((v | m0) + 1) & ~m0) | (v & m0);

         /* Continue while bits covered by mask difference is non-zero */
      } while (v & (m0 ^ m1));
   }

   /* Set unmasked bits so incrementing the reversed cursor
    * operates on the masked bits of the smaller table */
   v |= ~m0;

   /* Increment the reverse cursor */
   v = rev(v);
   v++;
   v = rev(v);

   return v;
}

私有方法

_dictExpandIfNeeded 初始化或扩展字典

如果使用的节点大于等于了表0的大小,或者重哈希正在执行(dict_can_resize为真),或者使用的节点数量超过了dict_force_resize_ratio = 5,将会在现有使用节点的基础上扩容2倍。

static int _dictExpandIfNeeded(dict *d) {
   /* Incremental rehashing already in progress. Return. */
   // 渐进式 rehash 已经在进行了,直接返回
   if (dictIsRehashing(d)) { return DICT_OK; }

   /* If the hash table is empty expand it to the initial size. */
   // 如果字典(的 0 号哈希表)为空,那么创建并返回初始化大小的 0 号哈希表
   // T = O(1)
   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. */
   // 一下两个条件之一为真时,对字典进行扩展
   // 1)字典已使用节点数和字典大小之间的比率接近 1:1
   //    并且 dict_can_resize 为真
   // 2)已使用节点数和字典大小之间的比率超过 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)) {
      // 新哈希表的大小至少是目前已使用节点数的两倍
      // T = O(N)
      return dictExpand(d, d->ht[0].used * 2);
   }

   return DICT_OK;
}

_dictNextPower 计算2的幂

static unsigned long _dictNextPower(unsigned long size) {
   unsigned long i = DICT_HT_INITIAL_SIZE;
   if (size >= LONG_MAX) { return LONG_MAX; }
   while (1) {
      if (i >= size) {
         return i;
      }
      i *= 2;
   }
}

_dictKeyIndex 计算根据key计算出索引值

此函数跟如果 key 已经存在于哈希表,那么返回 -1。注意,如果字典正在进行 rehash ,那么总是返回 1 号哈希表的索引,因为在字典进行 rehash 时,新节点总是插入到 1 号哈希表。此函数的查找逻辑与dictGenericDelete函数的查找逻辑实现的一致,再此不做具体阐述,具体可以看dictGenericDelete的解读。

static int _dictKeyIndex(dict *d, const void *key) {
   unsigned int h, idx, table;
   dictEntry *he;

   /* Expand the hash table if needed */
   // 单步 rehash
   // T = O(N)
   if (_dictExpandIfNeeded(d) == DICT_ERR) {
      return -1;
   }

   /* Compute the key hash value */
   // 计算 key 的哈希值
   h = dictHashKey(d, key);
   // T = O(1)
   for (table = 0; table <= 1; table++) {

      // 计算索引值
      idx = h & d->ht[table].sizemask;

      /* Search if this slot does not already contain the given key */
      // 查找 key 是否存在
      // T = O(1)
      he = d->ht[table].table[idx];
      while (he) {
         if (dictCompareKeys(d, key, he->key)) {
            return -1;
         }
         he = he->next;
      }

      // 如果运行到这里时,说明 0 号哈希表中所有节点都不包含 key
      // 如果这时 rehahs 正在进行,那么继续对 1 号哈希表进行 rehash
      if (!dictIsRehashing(d)) { break; }
   }

   // 返回索引值
   return idx;
}

dictEmpty 清空Hash表

调用_dictClear函数删除两个哈希表上的所有节点,最后把重Hash的索引rehashidx值重置为 -1,字典的迭代器数量置为0。

void dictEmpty(dict *d, void(callback)(void *)) {

   // 删除两个哈希表上的所有节点
   // T = O(N)
   _dictClear(d, &d->ht[0], callback);
   _dictClear(d, &d->ht[1], callback);
   // 重置属性
   d->rehashidx = -1;
   d->iterators = 0;
}

dictEnableResize dictDisableResize 开启或关闭重Hash

dict_can_resize是一个static全局变量,默认是开启重Hash的,0表示不开启,1表示开始。

void dictEnableResize(void) {
   dict_can_resize = 1;
}
void dictDisableResize(void) {
	dict_can_resize = 0;
}
  • 6
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值