Redis 3.0中的数据结构设计:基于gh_mirrors/re/redis-3.0-annotated的字典实现

Redis 3.0中的数据结构设计:基于gh_mirrors/re/redis-3.0-annotated的字典实现

【免费下载链接】redis-3.0-annotated 带有详细注释的 Redis 3.0 代码(annotated Redis 3.0 source code)。 【免费下载链接】redis-3.0-annotated 项目地址: https://gitcode.com/gh_mirrors/re/redis-3.0-annotated

引言:Redis字典的核心地位

在Redis(Remote Dictionary Server)这款高性能的键值对数据库中,字典(Dictionary)是最为核心的数据结构之一。无论是存储用户数据、实现数据库索引,还是作为其他复杂数据结构的底层支撑,字典都扮演着不可或缺的角色。本文将深入剖析Redis 3.0版本中字典的实现细节,基于带详细注释的源码仓库gh_mirrors/re/redis-3.0-annotated,帮助读者理解其设计思想与高效运作的原理。

字典的整体架构

Redis中的字典,也常被称为哈希表(Hash Table),采用了链地址法(Separate Chaining)来解决哈希冲突,并通过渐进式Rehash(Rehashing)机制来维持其在高负载下的性能。

核心数据结构定义

字典的实现主要涉及以下几个关键的结构体,定义在src/dict.h中:

  1. 哈希表节点 (dictEntry): 这是存储键值对的基本单元。

    typedef struct dictEntry {
        void *key;                     // 键
        union {                        // 值(支持多种类型)
            void *val;
            uint64_t u64;
            int64_t s64;
        } v;
        struct dictEntry *next;        // 指向下一个哈希表节点,形成链表以解决冲突
    } dictEntry;
    

    可以看到,值v是一个联合体(Union),这使得字典能够灵活地存储指针、无符号64位整数或有符号64位整数,在src/dict.c的操作函数中会根据具体情况使用不同的成员。

  2. 哈希表 (dictht): 哈希表本身是一个数组,数组中的每个元素都是一个指向dictEntry结构的指针,即一个桶(Bucket)。

    typedef struct dictht {
        dictEntry **table;             // 哈希表数组
        unsigned long size;            // 哈希表大小(桶的数量)
        unsigned long sizemask;        // 哈希表大小掩码,用于计算索引值,总是等于 size - 1
        unsigned long used;            // 该哈希表已有节点的数量
    } dictht;
    

    sizemask的设计非常巧妙,它使得通过哈希值计算索引的操作可以简化为位运算 hash & sizemask,这比取模运算 hash % size 效率更高。

  3. 字典 (dict): Redis的字典结构包含了两个哈希表,这是为了支持渐进式Rehash操作。

    typedef struct dict {
        dictType *type;                // 类型特定函数,用于多态操作
        void *privdata;                // 私有数据,传给类型特定函数的参数
        dictht ht[2];                  // 两个哈希表,ht[0] 是主表,ht[1] 用于Rehash
        int rehashidx;                 // Rehash索引,当 rehashidx == -1 时表示不在Rehash过程中
        int iterators;                 // 目前正在运行的安全迭代器的数量
    } dict;
    

    dictType结构体定义了一系列用于操作键值对的函数指针,如哈希函数、键值复制函数、键值比较函数以及键值销毁函数等,这使得字典可以通用地处理不同类型的键和值。

字典的创建与初始化

字典的创建通过dictCreate函数完成,该函数在src/dict.c中实现。它会分配内存,并调用_dictInit函数对字典进行初始化。_dictInit函数会重置两个哈希表的属性,设置类型特定函数和私有数据,并将rehashidx初始化为-1,表示未进行Rehash。

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;
    d->rehashidx = -1;
    d->iterators = 0;
    return DICT_OK;
}

哈希函数与索引计算

哈希函数的质量直接影响哈希表的性能。Redis字典默认使用MurmurHash2算法(由Austin Appleby设计)作为字符串键的哈希函数,定义在src/dict.cdictGenHashFunction函数中。

unsigned int dictGenHashFunction(const void *key, int len) {
    uint32_t seed = dict_hash_function_seed;
    const uint32_t m = 0x5bd1e995;
    const int r = 24;
    uint32_t h = seed ^ len;
    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;
    }
    // ... 处理剩余字节
    h ^= h >> 13;
    h *= m;
    h ^= h >> 15;
    return (unsigned int)h;
}

dict_hash_function_seed是一个静态全局变量,可以通过dictSetHashFunctionSeed函数设置,这为哈希表提供了一定的随机性,有助于避免某些特定类型的攻击或哈希冲突。

计算得到哈希值后,通过 hash & ht->sizemask 即可得到该键在哈希表数组中的索引位置。

冲突解决:链地址法

当两个不同的键通过哈希函数计算得到相同的索引时,就会发生哈希冲突。Redis采用链地址法来解决冲突,即每个哈希表节点都有一个next指针,多个哈希冲突的节点可以通过这个指针连接成一个单向链表。

src/dict.cdictAddRaw函数中可以看到,新节点总是被添加到链表的表头:

entry->next = ht->table[index];
ht->table[index] = entry;

这种头插法的好处是插入效率高,时间复杂度为O(1)。

动态扩容与Rehash机制

为了在数据量变化时保持高效的访问性能,Redis字典会根据负载因子(used / size)自动进行扩容(expand)或缩容(shrink)。这一过程主要通过Rehash操作完成。

触发条件

  • 扩容:当dict_can_resize为真且负载因子大于等于1,或者dict_can_resize为假但负载因子大于等于dict_force_resize_ratio(默认值为5)时,触发扩容。dict_can_resize可以通过dictEnableResizedictDisableResize函数控制,在Redis进行持久化等操作时可能会暂时禁用自动Resize以提高性能。
  • 缩容:当负载因子小于0.1时,可能会触发缩容。

这些检查在_dictExpandIfNeeded函数中进行,该函数会在执行dictAdddictFind等操作前被调用。

Rehash的过程

Rehash简单来说就是将旧哈希表(通常是ht[0])中的所有键值对迁移到新哈希表(通常是ht[1])中。Redis采用的是渐进式Rehash策略,而不是一次性完成所有迁移,以避免在数据量大时造成服务停顿。

  1. 准备阶段:当需要Rehash时,字典会为ht[1]分配足够大的空间(通常是ht[0].used的两倍且为2的幂次方),并将rehashidx设置为0,表示Rehash开始。
  2. 迁移阶段:在后续的字典操作(如dictAdddictFinddictDelete等)中,会调用_dictRehashStep函数,每次迁移一个或多个桶(bucket)中的所有节点。rehashidx记录当前正在迁移的桶索引。
  3. 完成阶段:当ht[0]中的所有节点都迁移到ht[1]后,释放ht[0]的空间,将ht[1]设置为ht[0],并重置ht[1]rehashidx

dictRehash函数(src/dict.c)负责执行实际的Rehash步骤,它接受一个参数n,表示最多迁移n个桶:

int dictRehash(dict *d, int n) {
    if (!dictIsRehashing(d)) return 0;
    while(n--) {
        dictEntry *de, *nextde;
        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;
        }
        // ... 迁移单个桶的节点
        d->rehashidx++;
    }
    return 1;
}

此外,还有dictRehashMilliseconds函数,它会在指定的毫秒数内尽可能多地执行Rehash步骤,这在Redis的定时任务中可能会用到,以确保Rehash过程能够稳步推进。

字典的基本操作

添加键值对 (dictAdd)

dictAdd函数尝试将一个新的键值对添加到字典中。它首先调用dictAddRaw函数来创建并插入新的哈希表节点,dictAddRaw会负责检查键是否已存在、计算索引、处理Rehash等。如果键不存在且插入成功,dictAdd再调用dictSetVal设置节点的值。

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

查找键值对 (dictFind)

dictFind函数根据给定的键查找对应的哈希表节点。它会计算键的哈希值,然后在对应的哈希表(可能是ht[0],如果正在Rehash则还需要检查ht[1])中查找。在查找过程中,如果字典正在Rehash,还会调用_dictRehashStep进行一步Rehash。

dictEntry *dictFind(dict *d, const void *key) {
    dictEntry *he;
    unsigned int h, idx, table;
    if (d->ht[0].size == 0) return NULL;
    if (dictIsRehashing(d)) _dictRehashStep(d);
    h = dictHashKey(d, key);
    for (table = 0; table <= 1; table++) {
        idx = h & d->ht[table].sizemask;
        he = d->ht[table].table[idx];
        while(he) {
            if (dictCompareKeys(d, key, he->key))
                return he;
            he = he->next;
        }
        if (!dictIsRehashing(d)) return NULL;
    }
    return NULL;
}

删除键值对 (dictDelete)

dictDelete函数用于从字典中删除指定键的键值对。它会调用dictGenericDelete函数,该函数会遍历可能的哈希表,找到对应的节点并将其从链表中移除,然后释放节点的键、值(如果设置了相应的析构函数)以及节点本身,并更新used计数。

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

迭代器 (dictIterator)

为了方便遍历字典中的所有键值对,Redis提供了迭代器机制。有两种类型的迭代器:安全迭代器(safe iterator)和不安全迭代器(unsafe iterator)。

  • 不安全迭代器:在迭代过程中,如果字典发生了Rehash或修改操作,可能会导致某些节点被重复遍历或遗漏。
  • 安全迭代器:通过设置iter->safe = 1创建。在安全迭代器活跃期间,字典的iterators计数会增加,这会阻止Rehash操作的进行,从而保证迭代的安全性。

迭代器的使用流程通常是:dictGetIteratordictGetSafeIterator创建迭代器 -> dictNext获取下一个节点 -> dictReleaseIterator释放迭代器。

总结与启示

Redis 3.0中的字典实现展现了高超的工程设计技巧:

  1. 高效的哈希函数:MurmurHash2提供了良好的分布性和计算速度。
  2. 链地址法解决冲突:简单有效,实现方便。
  3. 渐进式Rehash:巧妙地将大数据量的迁移工作分散到多次操作中,避免了性能抖动。
  4. 动态扩容/缩容:根据实际数据量自动调整哈希表大小,平衡空间利用率和访问速度。
  5. 多态设计:通过dictType结构体,使得字典能够处理不同类型的键值对。

这些设计思想不仅保证了Redis作为数据库的高性能,也为我们在日常开发中设计类似的数据结构提供了宝贵的参考。深入理解这些源码细节(如src/dict.h中的结构定义和src/dict.c中的算法实现),有助于我们更好地使用Redis,并将这些优秀的设计模式应用到自己的项目中。

【免费下载链接】redis-3.0-annotated 带有详细注释的 Redis 3.0 代码(annotated Redis 3.0 source code)。 【免费下载链接】redis-3.0-annotated 项目地址: https://gitcode.com/gh_mirrors/re/redis-3.0-annotated

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值