Redis(二)字典介绍及部分源码刨析

本文深入探讨Redis中字典的数据结构,介绍其基于哈希表的实现方式,以及如何通过渐进式rehash机制高效处理数据增删改查。文章详解了字典的初始化、收缩、rehash和元素操作等关键函数,适合Redis开发者和技术爱好者阅读。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一.概述

   Redis中使用字典这种数据结构作为数据库的底层实现,对数据库的增删改查也是基于对字典的操作之上进行的,当进行持久化时再将字典中的数据保存至磁盘中。

 

二.Redis中的字典结构

       Redis中的字典是使用hashtable实现的,因为当hash表的负载因子不高且hash算法可以较均匀分配时,hashtable可以在常数时间内查找到数据,且若采用链式地址法处理冲突时,用头插法操作新来的元素,可以在O(1)时间复杂度内完成数据的插入操作。转而观之,rb_tree等却需要O(logn)的时间复杂度。

        其字典结构定义如下:

// hash节点
typedef struct dictEntry{
    // 键 (注意为void*, 因此可以指向任意类型)
    void *key;

    // 值(是一个共用体,可以保存指向任意类型的指针,或64位整型)
    union {
        void *val;
        uint64_t u64;
        int64_t  s64;
    } v;

    // hash节点使用链式地址法处理冲突
    struct dictEntry *next;
} dictEntry;

// hashtable结构体
typedef struct dictht {
    // hashtable数组,table指向一个hashtable,其中每个元素为一个桶(指向hash节点的指针)
    dictEntry **table;

    // hashtable大小,即桶的个数
    unsigned long size;

    // hashtable大小掩码,为size - 1,用于计算索引(idx = hashIdx & sizemask)
    unsigned long sizemask;

    // hashtable中已有节点的数量
    unsigned long used;
} dictht;

// 字典结构体
typedef struct dict {
    // 字典操作的函数指针集合,可以根据用途不同的字典设置不同的函数(很类似操作系统中的写法)
    dictType *type;

    // 私用数据,用于作为上述函数的可选参数
    // 参下方【注2】
    void *privatedata;

    // 两个hashtable,平时使用ht[0],在rehash时使用ht[1]进行辅助
    dictht ht[2];

    // 渐进式rehash时使用的索引,当未进行rehash时为-1
    int trehashidx;
};

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;

【注1】:Redis的Hash表似乎借鉴了由华人内核维护者Herbert Xu为Linux内核开发的Hashtable, 在深入理解并行编程一书的第10章对这种Hashtable进行了说明。

【注2】:关于dict::privatedate的作用,在深入理解并行编程一书中给出了如下解释:用于扰乱hash函数,以防止通过对hash函数所使用的参数进行统计分析来发起拒绝服务攻击(DDos)。

三.hash算法

     Redis采用MurmurHash2(不甚了解)算法,使用链地址法解决键冲突。

 

四.渐进式rehash

        当要进行rehash,会先为ht[1]分配一个新的空间,接着采用渐进式rehash算法将ht[0]中的键-值对拷贝到ht[1]中,当拷贝完成后释放ht[0],将ht[1]赋给ht[0],ht[1]指向空哈希表(ht[1]->table = NULL)。

1.何时进行rehash

       当以下两个条件之一被满足时会扩展hash表并进行rehash

  • 当没有在执行BGSAVE(异步持久化)或BGREWRITEEAOF(异步重写一个AOF)命令时,哈希表的负载因子大于等于1。
  • 当正在执行BGSAVE或BGBGREWRITEEAOF命令时,哈希表的负载因子大于等于5。
  • 当负载因子小于0.1时进行哈希表的收缩

负载因子 = 哈希表已保存节点数量 / 哈希表大小 = ht[0].used / ht[0].size

【注】:之所以决定rehash的负载因子阈值随是否执行BGSAVE和BGREWRITEEAOF命令而变化是因为,在这两个命令的执行过程中,当前服务器进程都会fork出子进程来执行操作,而大多操作系统并不在fork出子进程时立即拷贝父进程的地址空间,而是采用写时赋值,而在rehash时要进行改变所以要进行写时赋值,为了减少写时赋值带来的性能影响而将负载因子提升到了5。(连写时赋值都考虑进去了,真的是无所不用其极)(关于写时赋值额具体过程,参考博文《Linux内核——进程的地址空间》

2.hashtable的扩展与收缩规则

  • 扩展:为ht[1]分配新的空间,大小为大于ht[0].used * 2(即现有哈希节点2倍)的第一个2^n。【注】:stl的hashtable是第一个质数
  • 收缩:ht[1]指向的新哈希表空间为大于ht[0].used的第一个2^n。

【注】:哈希表的大小为2^n是有原因的,因为这样便于设置哈希表大小的掩码,即mask = size - 1,这样在计算元素放入哪个篮子时可以使用掩码快速计算,即哈希值 & mask

3.渐进式rehash过程

       由于hash表可能比较庞大,因此若rehash操作一次性,集中性的完成会造成服务器一段时间内的停止服务,因此rehash操作是分多次,渐进式地完成更新的。(【开心】:和论文里想到的分批合并内存块的相法相同,嘻嘻~)。

       rehash的详细步骤如下:

  1. 为ht[1]按规则分配一个新的哈希表,字典此时同时维护两个哈希表。
  2. 将字典中的trehashidx至为0,表示rehash操作开始(-1表示未进行rehash)
  3. 在rehash操作中除了对字典进行增删改查操作外还将ht[0]在rehashidx个桶上的键-值对rehash到ht[0]中。对于添加操作只在hash[0]中进行。【注】:不会在ht[0]与ht[1]中同时维护同一份数据,查找过程会先在ht[0]中查找,再去ht[1]中查找。

      

五.字典部分源码

1.字典初始化

     调用dictCreate函数开辟字典空间,之后调用相应函数初始化内部数据成员,如下所示:

// 创建字典
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]); // 初始化用于键值对存储的hash表
    _dictReset(&d->ht[1]);
    d->type = type;        // 初始化函数集合表
    d->privdata = privDataPtr; // 初始化私有数据
    d->rehashidx = -1;    // 初始化rehash进度,时间事件函数serverCron会定期检查
    d->iterators = 0;     // 初始化当前迭代器数量为0
    return DICT_OK;
}

// 初始化哈希表内部成员
static void _dictReset(dict *ht) {
    ht->table = NULL;
    ht->size = 0;
    ht->sizemask = 0;
    ht->used = 0;
}

2.收缩字典

      将字典收缩至可满足存储数据元素的最小大小,但不小于DICT_HT_INITIAL_SIZE(4) , 亦用于初始化字典后调整哈希表大小。

// 收缩字典大小
int dictResize(dict *d)
{
    int minimal;

    // 若不允许收缩,或该字典当前正在进行rehash,则不可进行收缩
    if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;
    minimal = d->ht[0].used;     // 该字典当前的元素大小
    if (minimal < DICT_HT_INITIAL_SIZE) // 不可小于4
        minimal = DICT_HT_INITIAL_SIZE;
    return dictExpand(d, minimal); // 调用dictExpand函数进行字典大小调整
}

int dictExpand(dict *d, unsigned long size)
{
    dictht n; /* 新的哈希表 */
    // _dictNextPower将哈希表调整至大于申请大小的第一个2的倍数大小(2^n)
    unsigned long realsize = _dictNextPower(size);

    //若当前字典正在进行rehash,或该字典的已有节点数大于要调整的字典大小(即不可容纳当前元素),则不进行调整
    if (dictIsRehashing(d) || d->ht[0].used > size)
        return DICT_ERR;

    // 若调整后的大小与当前字典大小相同则不进行调整
    if (realsize == d->ht[0].size) return DICT_ERR;

    // 设置新哈希表成员
    n.size = realsize;
    n.sizemask = realsize-1;
    n.table = zcalloc(realsize*sizeof(dictEntry*));
    n.used = 0;

    // 若当前字典中的哈希表0为空,说明当前处于初始化状态,则直接将新表赋予哈希表0
    if (d->ht[0].table == NULL) {
        d->ht[0] = n;
        return DICT_OK;
    }

    // 将新表赋予哈希表1,并进行rehash
    d->ht[1] = n;
    d->rehashidx = 0;	// 启动rehash
    return DICT_OK;
}

【注】:在事件事件处理函数serverCron中会调用databasesCron函数对服务器中的一部分数据库进行检查,在其中会检查相应字典是否需要进行rehash操作,而是否要进行rehash就是根据rehashidx是否为-1进行判断的,serverCron函数默认每100ms执行一次

3.rehash函数

// 对字典d进行rehash,此次至多更新n个非空篮子
int dictRehash(dict *d, int n) {
    int empty_visits = n*10;  // 最多可访问的空篮子个数,因为访问空篮子也是需要耗费一定时间的
    if (!dictIsRehashing(d)) return 0; // 检查当前字典的rehash进度是否为-1,若是则说明不进行rehash

    while(n-- && d->ht[0].used != 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);
        while(d->ht[0].table[d->rehashidx] == NULL) {
            d->rehashidx++;
            if (--empty_visits == 0) return 1;
        }
        de = d->ht[0].table[d->rehashidx]; // 指向当前要进行rehash的篮子
        
        // 将该篮子中的所有元素从旧表(ht[0])移入新表(ht[1])
        while(de) {
            unsigned int h;
            nextde = de->next;
            // 计算应该放入新表中的第几个篮子
            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++;
    }

    // - 检查旧表中的元素是否已全部更新至新表中
    // - 若是则将将ht[1]赋予ht[0],并重写初始化ht[1]
    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;
}

// 每次至多更新100个非空篮子,直至旧表更新完毕或超出限制时间则停止
int dictRehashMilliseconds(dict *d, int ms) {
    long long start = timeInMilliseconds();
    int rehashes = 0;

    while(dictRehash(d,100)) {
        rehashes += 100;
        if (timeInMilliseconds()-start > ms) break;
    }
    return rehashes;
}

4.添加元素

   将元素添加至字典

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

dictEntry *dictAddRaw(dict *d, void *key)
{
    int index;
    dictEntry *entry;
    dictht *ht;

    // - 若正在进行rehash
    // - 若当前无现存迭代器,则rehash一个篮子到新表中(加快rehash速度)
    // - 若存在现存迭代器,则不进行rehash,因为会造成迭代器失效
    if (dictIsRehashing(d)) _dictRehashStep(d);

    // 检查字典中是否已存在该元素,若已存在则返回-1,不进行添加
    if ((index = _dictKeyIndex(d, key)) == -1)
        return NULL;

    // - 根据是否正在进行rehash判断将新元素放入那个表中
    // - 若正在进行rehash,则新增元素全部放入新表中,否则放入旧表
    ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];

    // 为新元素开辟节点空间,并链入相应篮子首部
    entry = zmalloc(sizeof(*entry));
    entry->next = ht->table[index];
    ht->table[index] = entry;
    ht->used++;

    // dictSetKey函数调用字典中的函数集(type)中的函数设置该新增节点的值
    dictSetKey(d, entry, key);
    return entry;
}

5.替换元素

     将键为key元素的值替换为新值,若该元素已存在,则由于值可能是用void*指向的一个对象,因此需先复制该节点,再更改该节点中的值,随后调用相应函数释放旧值

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

    if (dictAdd(d, key, val) == DICT_OK)
        return 1;
    entry = dictFind(d, key);
    // 复制旧节点(备份指向值的指针)
    auxentry = *entry;
    dictSetVal(d, entry, val);
    // 调用函数集合表中的函数释放旧的节点(包括其中值指针所指向的元素)
    dictFreeVal(d, &auxentry);
    return 0;
}

6.删除元素

      从字典中删除键值为key的元素

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
    // - 若当前无现存迭代器,则rehash一个篮子到新表中(加快rehash速度)
    // - 若存在现存迭代器,则不进行rehash,因为会造成迭代器失效
    if (dictIsRehashing(d)) _dictRehashStep(d);

    // 根据key调用哈希函数
    h = dictHashKey(d, key);

    // 若正在rehash则查找新旧量表,否则再查找ht[0]后结束
    for (table = 0; table <= 1; table++) {
        idx = h & d->ht[table].sizemask;
        he = d->ht[table].table[idx];
        prevHe = NULL;
        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;
        }
        if (!dictIsRehashing(d)) break;
    }
    return DICT_ERR; /* not found */
}

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

7.字典遍历dictScan

    为看明白,待更新!!!

8.随机返回数据节点(dictGetRandomKey)

     

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

    if (dictSize(d) == 0) return NULL;
    if (dictIsRehashing(d)) _dictRehashStep(d);
    if (dictIsRehashing(d)) { // 正在进行rehash
        do {
            // d->rehashidx + 保证了不会查询ht[0]中的0~d->rehashidx-1区间
            h = d->rehashidx + (random() % (d->ht[0].size + d->ht[1].size - d->rehashidx));
            he = (h >= d->ht[0].size) ? d->ht[1].table[h - d->ht[0].size] :
                                      d->ht[0].table[h];
        } while(he == NULL);
    } else {
        do {
            h = random() & d->ht[0].sizemask; // 随机一个篮子
            he = d->ht[0].table[h];  // he指向该篮子的链表头
        } while(he == NULL);
    }

    listlen = 0;
    orighe = he;
    while(he) { // 计算该篮子的长度
        he = he->next;
        listlen++;
    }
    // 随机取该篮子中的第几个数据节点
    listele = random() % listlen;
    he = orighe;
    while(listele--) he = he->next;
    return he;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值