Redis哈希表:从dict字典结构到万亿级数据存储的秘密

Redis哈希表:从dict字典结构到万亿级数据存储的秘密

【免费下载链接】redis Redis 是一个高性能的键值对数据库,通常用作数据库、缓存和消息代理。* 缓存数据,减轻数据库压力;会话存储;发布订阅模式。* 特点:支持多种数据结构,如字符串、列表、集合、散列、有序集等;支持持久化存储;基于内存,性能高。 【免费下载链接】redis 项目地址: https://gitcode.com/GitHub_Trending/re/redis

你是否曾好奇Redis如何在毫秒级处理百万级键值对?当系统面临流量峰值时,Redis如何保持稳定高效?答案藏在其核心数据结构——哈希表(Hash Table)中。本文将带你深入Redis哈希表的dict字典结构,揭开从理论设计到工程实现的层层奥秘,让你彻底掌握这一支撑Redis高性能的关键技术。

哈希表基础:从理论到Redis实现

哈希表(Hash Table)是一种通过键直接访问内存存储位置的数据结构,它通过哈希函数将键映射到表中的某个位置,从而实现O(1)平均时间复杂度的查找、插入和删除操作。在Redis中,哈希表不仅用于实现数据库键空间(Key Space),还广泛应用于数据库内部的各种功能模块,如字典、集合等。

Redis的哈希表实现主要由dictdictTypedictEntry三个核心结构体构成,定义在src/dict.h中。这种设计将数据存储与操作逻辑分离,既保证了代码的模块化,又为不同类型的键值对提供了灵活的定制能力。

dict结构体:哈希表的管理中心

dict结构体是Redis哈希表的管理核心,它维护了两个哈希表(ht_table[0]和ht_table[1]),用于实现渐进式rehash(重哈希)。这种双表结构是Redis实现高并发的关键设计之一,允许在不阻塞读写操作的情况下完成哈希表的扩容与缩容。

struct dict {
    dictType *type;           /* 类型特定函数 */
    dictEntry **ht_table[2];  /* 哈希表数组 */
    unsigned long ht_used[2]; /* 每个哈希表已使用的节点数量 */
    long rehashidx;           /* rehash索引,-1表示未在rehash */
    unsigned pauserehash : 15;/* rehash暂停计数 */
    unsigned useStoredKeyApi : 1; /* 使用存储键API标志 */
    signed char ht_size_exp[2]; /* 哈希表大小的指数表示 */
    int16_t pauseAutoResize;  /* 自动调整大小暂停计数 */
    void *metadata[];         /* 元数据 */
};

src/dict.h的122-137行定义了这一结构,其中ht_size_exp使用指数表示哈希表大小(实际大小为2^exp),这一设计确保了哈希表大小始终为2的幂,便于通过位运算快速计算哈希桶索引。

dictType:定制化哈希表行为

dictType结构体定义了一系列函数指针,用于定制哈希表的行为,如哈希函数、键值复制、比较和销毁等操作。这种设计使得Redis的哈希表可以灵活适配不同类型的键值对,如字符串、整数、复杂对象等。

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)(dictCmpCache *cache, const void *key1, const void *key2); /* 键比较函数 */
    void (*keyDestructor)(dict *d, void *key);       /* 键销毁函数 */
    void (*valDestructor)(dict *d, void *obj);       /* 值销毁函数 */
    int (*resizeAllowed)(size_t moreMem, double usedRatio); /* 调整大小允许函数 */
    void (*rehashingStarted)(dict *d);               /* rehash开始回调 */
    void (*rehashingCompleted)(dict *d);             /* rehash完成回调 */
    void (*bucketChanged)(dict *d, long long delta); /* 桶数量变化回调 */
    size_t (*dictMetadataBytes)(dict *d);            /* 元数据大小函数 */
    void *userdata;                                  /* 用户数据 */
    unsigned int no_value:1;                         /* 无值标志(用于集合) */
    unsigned int keys_are_odd:1;                     /* 键地址为奇数标志 */
    unsigned int force_full_rehash:1;                /* 强制完全rehash标志 */
    uint64_t (*storedHashFunction)(const void *key); /* 存储键哈希函数 */
    int (*storedKeyCompare)(dictCmpCache *cache, const void *key1, const void *key2); /* 存储键比较函数 */
    void (*onDictRelease)(dict *d);                  /* 字典释放回调 */
} dictType;

这一结构定义在src/dict.h的53-117行,其中no_valuekeys_are_odd标志用于优化集合(Set)的存储,避免为只需要键的场景分配额外的value空间,这是Redis内存优化的一个典型例子。

dictEntry:键值对的容器

dictEntry结构体用于存储实际的键值对,它通过链表指针next实现哈希冲突的链式解决。在Redis中,哈希冲突的处理采用链地址法(Separate Chaining),当多个键哈希到同一个桶时,它们会通过next指针形成一个链表。

struct dictEntry {
    struct dictEntry *next;  /* 下一个哈希节点,形成链表 */
    void *key;               /* 键 */
    union {                  /* 值的多种表示形式 */
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
};

这一结构定义在src/dict.c的48-57行,联合体v的设计允许值以不同的数据类型存储,避免了为每种类型单独定义结构体的开销,体现了Redis对内存效率的极致追求。

哈希函数:Redis的指纹生成器

哈希函数是哈希表的灵魂,它将任意长度的键映射到固定长度的哈希值。Redis采用SipHash算法作为默认哈希函数,这是一种加密哈希函数,具有良好的雪崩效应和抗碰撞性,特别适合抵御哈希洪水攻击(Hash Flooding Attack)。

SipHash算法:安全与性能的平衡

SipHash算法由Jean-Philippe Aumasson和Daniel J. Bernstein设计,它在提供高强度安全性的同时保持了较高的性能。Redis在src/dict.c的120-126行实现了基于SipHash的哈希函数:

uint64_t dictGenHashFunction(const void *key, size_t len) {
    return siphash(key, len, dict_hash_function_seed);
}

uint64_t dictGenCaseHashFunction(const unsigned char *buf, size_t len) {
    return siphash_nocase(buf, len, dict_hash_function_seed);
}

dictGenHashFunction用于普通键的哈希计算,而dictGenCaseHashFunction则用于大小写不敏感的场景。dict_hash_function_seed是一个全局哈希种子,可通过dictSetHashFunctionSeed函数设置,增加了哈希函数的随机性和安全性。

哈希值到桶索引的映射

计算得到64位哈希值后,Redis需要将其映射到哈希表的某个桶(Bucket)中。由于哈希表大小是2的幂,Redis通过位运算实现这一映射,既高效又保证了哈希值的均匀分布:

#define DICTHT_SIZE_MASK(exp) ((exp) == -1 ? 0 : (DICTHT_SIZE(exp))-1)
h = dictHashKey(d, key, 1) & DICTHT_SIZE_MASK(d->ht_size_exp[1]);

这段代码来自src/dict.c的338行,DICTHT_SIZE_MASK宏生成一个掩码,通过与哈希值进行按位与运算,得到桶索引。这种方法比取模运算更高效,且在哈希表大小为2的幂时,能保证哈希值的低阶位均匀分布。

渐进式Rehash:Redis的并发魔法

随着键值对数量的增减,哈希表的负载因子(Load Factor)会随之变化。当负载因子过高时,哈希冲突概率增加,导致链表变长,查询性能下降;当负载因子过低时,内存利用率降低。为解决这一问题,Redis引入了Rehash(重哈希)机制,动态调整哈希表大小。

负载因子与Rehash触发条件

负载因子定义为哈希表中键值对数量与桶数量的比值。Redis通过以下条件判断是否需要触发Rehash:

  • 扩容(Expand):当负载因子 > 1,且未处于禁止Rehash状态时触发;或者负载因子 > dict_force_resize_ratio(默认4)时强制触发。
  • 缩容(Shrink):当负载因子 < 1/HASHTABLE_MIN_FILL(HASHTABLE_MIN_FILL默认8,即负载因子 < 1/8)时触发。

这些条件在src/dict.c的39-45行和307-325行中定义和实现,确保了哈希表在性能和内存利用率之间的平衡。

渐进式Rehash的实现

传统的Rehash操作需要一次性将所有键值对从旧表迁移到新表,这在大数据量下会导致长时间阻塞。Redis采用渐进式Rehash,将迁移过程分散到多次操作中,避免了单次操作的性能抖动。

int dictRehash(dict *d, int n) {
    int empty_visits = n*10; /* 最大空桶访问次数 */
    if (!dictIsRehashing(d)) return 0;

    while(n-- && d->ht_used[0] != 0) {
        while(d->ht_table[0][d->rehashidx] == NULL) {
            d->rehashidx++;
            if (--empty_visits == 0) return 1;
        }
        rehashEntriesInBucketAtIndex(d, d->rehashidx);
        d->rehashidx++;
    }

    if (dictCheckRehashingCompleted(d)) return 0;
    return 1;
}

这段代码来自src/dict.c的400-430行,dictRehash函数每次处理n个桶,通过rehashidx记录当前迁移进度。在实际操作中,Redis会在每次哈希表操作(如查找、插入、删除)时调用_dictRehashStep函数,执行少量Rehash工作,从而实现"不知不觉"的哈希表迁移。

Rehash期间的键值访问

在Rehash过程中,哈希表同时存在新旧两个表(ht_table[0]和ht_table[1])。此时,所有写操作都会在新表上进行,而读操作则会同时检查两个表,保证数据的一致性和可用性:

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

    if (dictSize(d) == 0) return NULL;
    if (dictIsRehashing(d)) _dictRehashStep(d);
    h = dictHashKey(d, key, d->useStoredKeyApi);
    for (table = 0; table <= 1; table++) {
        idx = h & DICTHT_SIZE_MASK(d->ht_size_exp[table]);
        he = d->ht_table[table][idx];
        while(he) {
            if (dictCompareKeys(d, key, dictGetKey(he)))
                return he;
            he = dictGetNext(he);
        }
        if (!dictIsRehashing(d)) return NULL;
    }
    return NULL;
}

这段代码展示了Rehash期间的查找逻辑(简化自src/dict.c的dictFind函数),它首先检查是否需要执行Rehash步骤,然后在两个表中分别查找键,确保在迁移过程中不会丢失数据。

哈希冲突解决:链地址法的优化实践

尽管哈希函数的设计目标是均匀分布键,但哈希冲突(多个键映射到同一个桶)仍然不可避免。Redis采用链地址法解决哈希冲突,将冲突的键值对通过链表连接起来。为了减少长链表对性能的影响,Redis在实现中做了多项优化。

链表结构与遍历优化

每个哈希桶(Bucket)对应一个dictEntry链表,表头指针存储在ht_table数组中。当查找一个键时,Redis需要遍历链表,比较每个节点的键:

he = d->ht_table[table][idx];
while(he) {
    if (dictCompareKeys(d, key, dictGetKey(he)))
        return he;
    he = dictGetNext(he);
}

这段代码来自src/dict.c的dictFind函数,dictCompareKeys使用dictType中定义的比较函数,确保了对不同类型键的正确比较。为了提高比较效率,Redis还引入了dictCmpCache结构体,缓存键比较过程中的临时计算结果,减少重复计算。

小哈希表的优化:直接存储键

对于只存储键(如Set数据结构)的哈希表,Redis通过no_value标志进行优化,当桶中只有一个键时,直接存储键的指针,而非完整的dictEntry结构体:

if (d->type->no_value) {
    if (!d->ht_table[1][h]) {
        /* 目标桶为空,直接存储键指针 */
        if (d->type->keys_are_odd)
            de = key; /* 键地址为奇数,自动设置ENTRY_PTR_IS_ODD_KEY标志 */
        else
            de = encodeMaskedPtr(key, ENTRY_PTR_IS_EVEN_KEY);
    }
    ...
}

这段代码来自src/dict.c的345-355行,通过指针低位的几个比特存储元数据(是否为键指针、是否有值等),避免了为单个键分配dictEntry的开销,显著提高了内存利用率。

实际应用与最佳实践

了解Redis哈希表的内部实现后,我们可以通过以下最佳实践优化Redis的使用:

合理设置哈希表参数

Redis提供了hash-max-ziplist-entrieshash-max-ziplist-value等配置参数,控制哈希表在什么条件下使用压缩列表(ZipList)而非哈希表。对于小数据量的哈希,压缩列表可以节省大量内存,但在数据量增长后,应及时转换为哈希表以保证性能。

避免大key和热key

大key(如包含大量字段的哈希)会导致Rehash过程缓慢,而热key(被频繁访问的key)可能会成为性能瓶颈。通过合理拆分大key、使用分布式缓存分担热key压力,可以充分发挥Redis哈希表的性能优势。

监控哈希表性能指标

Redis提供了丰富的INFO命令,可用于监控哈希表的性能指标,如hash_max_zipf_logrejected_connections等。通过定期分析这些指标,可以及时发现哈希表的潜在问题,如负载因子过高、Rehash阻塞等。

总结与展望

Redis的哈希表实现是理论与工程实践结合的典范,它通过精妙的dict结构设计、高效的哈希函数、渐进式Rehash和冲突解决机制,在保证高性能的同时,兼顾了内存效率和并发安全性。从简单的键值存储到复杂的分布式系统,哈希表始终是Redis的核心竞争力之一。

随着Redis的不断发展,哈希表也在持续优化。未来,我们可能会看到更多针对特定场景的哈希表优化,如结合跳表(Skip List)进一步优化长链表性能,或引入更多智能哈希函数适应不同类型的键。无论如何,深入理解哈希表的工作原理,都是优化Redis应用、排查性能问题的基础。

希望本文能帮助你揭开Redis哈希表的神秘面纱,在实际应用中更好地驾驭这一强大的数据结构。如果你对Redis哈希表还有更多疑问,不妨直接阅读src/dict.hsrc/dict.c的源码,那里有更多细节和惊喜等着你发现!

【免费下载链接】redis Redis 是一个高性能的键值对数据库,通常用作数据库、缓存和消息代理。* 缓存数据,减轻数据库压力;会话存储;发布订阅模式。* 特点:支持多种数据结构,如字符串、列表、集合、散列、有序集等;支持持久化存储;基于内存,性能高。 【免费下载链接】redis 项目地址: https://gitcode.com/GitHub_Trending/re/redis

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

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

抵扣说明:

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

余额充值