【Redis技术进阶之路】「底层源码解析」揭秘高效存储模型与数据结构底层实现(字典)(二)

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
云解析 DNS,旗舰版 1个月
简介: 【Redis技术进阶之路】「底层源码解析」揭秘高效存储模型与数据结构底层实现(字典)

【Redis技术进阶之路】「底层源码解析」揭秘高效存储模型与数据结构底层实现(字典)(一)https://developer.aliyun.com/article/1471152


dictEntry模型

Redis的dictEntry 结构体不仅包含了指向键和值的指针,还巧妙地设计了一个指向下一个哈希项的指针next。这个指针 next 的存在,使得当多个键的哈希值发生冲突时,Redis 能够将这些键以链表的形式连接在一起,从而有效地解决了哈希冲突问题。



  • key:用于存储键值对中键的部分
  • value:承载着键值对中对应的值。既可以是一个指向其他数据结构的指针,也可以是一个uint64_t类型的无符号64位整数,或是一个int64_t类型的有符号64位整数。
  • next:扮演着链接哈希表节点的角色,它是一个指向另一个哈希表节点的指针。当多个键值对的哈希值相同时,即发生了所谓的键冲突,这些具有相同哈希值的键值对会通过next指针串联起来,形成一个链表结构。

通过设计哈希函数和链表的维护策略,哈希表能够在平均情况下实现近乎O(1)的查找、插入和删除操作。

dictEntry的结构体源码

dictEntry结构体表示字典中的一个键值对,源码如下所示:

c

复制代码

struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    /* Next entry in the same hash bucket. */    
    struct dictEntry *next;    
};
typedef struct {
    void *key;
    dictEntry *next;
} dictEntryNoValue;
dictEntry **ht_table[2]

dictEntry **ht_table[2]:一个包含两个项的数组,其中每个项都代表一个dict哈希表结构。



在正常情况下,我们主要使用ht[0]这个哈希表进行数据存储和检索操作。而ht[1]哈希表的存在,主要是为了在需要对ht[0]进行rehash操作时提供一个临时的存储空间。

两个哈希表支持rehashing

在 Redis 中,当哈希表的大小需要调整时(例如,因为哈希表已满或空闲空间太多),它不会一次性重新分配整个哈希表,而是会同时使用两个哈希表:一个旧的和一个新的,类似于COW模式进行处理,Copy And Write机制。

随着键值对的插入和删除,旧的哈希表中的数据会逐渐迁移到新的哈希表中,直到旧的哈希表为空,然后旧的哈希表会被释放,新的哈希表成为主哈希表。接下来我们要开始分析和研究hash的机制和原理、最后到了对应的rehashing能力。

哈希算法

我们先来看一下哈希算法,当需要将一个新的键值对添加至字典时,程序会首先根据该键值对的键进行哈希计算,得出相应的哈希值。随后,利用这个哈希值,进一步计算出在哈希表数组中的具体索引位置。

计算哈希值和索引值

使用字典设置的哈希函数,计算键key的哈希值,使用哈希表的mask值和size值,计算出素引值,根据情况不同,ht[x]可以是ht[0]或者ht[1]。

c

复制代码

#define DICTHT_SIZE(exp) ((exp) == -1 ? 0 : (unsigned long)1<<(exp))
#define DICTHT_SIZE_MASK(exp) ((exp) == -1 ? 0 : (DICTHT_SIZE(exp))-1)

上面定义的C语言的两个宏通常一起使用。

c

复制代码

#define dictHashKey(d, key) ((d)->type->hashFunction(key))
#define dictBuckets(d) (DICTHT_SIZE((d)->ht_size_exp[0])+DICTHT_SIZE((d)->ht_size_exp[1]))
#define dictSize(d) ((d)->ht_used[0]+(d)->ht_used[1])

当想要计算一个键值对的哈希值对应的哈希表索引时,会先使用哈希函数计算出一个原始的哈希值,然后使用 DICTHT_SIZE_MASK(exp) 宏将这个哈希值限制在哈希表大小的范围内。这样,就可以确保计算出的索引不会超出哈希表的边界。最终,将包含新键值对的哈希表节点精准地放置在哈希表数组指定索引的位置上。

案例分析

举个例子,对于下图所示的字典来说,如果我们要将一个键值对w添加到字典里面:


那么程序会先使用语句:#define dictHashKey(d, key) ((d)->type->hashFunction(key)),计算键w的哈希值。假设计算得出的哈希值为100,那么程序会继续使用语句:#define dictBuckets(d) (DICTHT_SIZE((d)->ht_size_exp[0])+DICTHT_SIZE((d)->ht_size_exp[1])),计算出键w的索引值6,这表示包含键值对w的节点应该被放置到哈希表数组的索引6位置上。

解决键冲突

当多个键被哈希函数映射到哈希表数组的同一索引位置时,这种现象被称为键冲突。

链地址法

在Redis的哈希表实现中,采用了链地址法来有效处理这种冲突。每个哈希表节点都包含一个next指针,使得多个哈希表节点能够通过next指针串联成一个单向链表。因此,当多个键被分配到相同的索引位置时,这些节点可以通过这个单向链表相互连接,从而巧妙地解决了键冲突问题。


以图示为例,假设我们拥有一个哈希表,并且程序需要将键值对w插入其中。经过哈希函数的计算,我们得知w的索引值为4。然而,在这个特定的索引位置,已存在其他键(如cd),这就引发了键冲突问题。

Rehash操作和处理执行

为了确保哈希表的负载因子维持在一个适宜的水平,程序会根据哈希表当前的键值对数量来灵活调整其大小,这种动态调整的策略有助于确保哈希表始终保持在高效运行的状态。

扩展和收缩

扩展和收缩哈希表的工作可通过执行rehash(重新散列)操作得以实现。在此过程中,字典巧妙地利用ht[0]和ht[1]这两个哈希表来共同存储键值对,确保了操作的顺畅进行。当满足以下任一条件时,程序将自动触发哈希表的扩展操作:

  • 【键值对数量过多】导致负载因子偏高时,程序会执行扩展操作,增大哈希表容量,以提高查询效率并避免过多的冲突。
  • 【键值对数量过少】负载因子偏低,程序则会进行收缩操作,减小哈希表的大小,以节省内存资源。
负载因子

负载因子 = 即键值对数量/哈希表大小。

c

复制代码

load_factor = ht[0/1].used / ht[0/1].size
案例分析
  • 哈希表的大小为6,包含6个键值对的哈希表来说,这个哈希表的负载因子为:6/6 =1,结果为1。
  • 哈希表的大小为100,包含200个键值对的哈希表来说,这个哈希表的负载因子为:200 / 100=2
  • 哈希表的大小为100,包含50个键值对的哈希表来说,这个哈希表的负载因子为:50 / 100=0.5
RDB和AOF与Rehash的关系

Redis在执行BGSAVE或BGREWRITEAOF命令时,会根据子进程的存在与否调整哈希表扩展操作的负载因子阈值。这主要是为了避免在子进程运行时进行不必要的哈希表扩展,进而减少内存写入,提高内存利用效率。

  • 未执行BGSAVE或BGREWRITEAOF命令,并且哈希表的负载因子达到或超过1时,程序会自动启动哈希表的扩展操作。
  • 正在执行BGSAVE或BGREWRITEAOF命令,且哈希表的负载因子不低于5,程序将自动触发哈希表的扩展流程。

哈希表执行rehash的步骤

  1. 分配h1的哈希表空间:当哈希表需要扩容时,为ht[1]分配新的存储空间,其大小是经过精确计算的,确保至少是当前ht[0]中键值对数量(即ht[0].used的值)的两倍,并且是一个2的n次方幂。
  2. Rehash重新散列,这一过程涉及到将ht[0]中所有的键值对,按照新的哈希算法计算出的哈希值和索引值,精确无误地迁移至ht[1]中。
  3. 数据键值转移:所有键值对成功从ht[0]转移至ht[1]
  4. 释放原有哈希表空间:立即释放ht[0]占用的内存空间,以优化内存使用。

最后,将ht[1]提升为新的ht[0],并初始化一个新的空白哈希表作为新的ht[1],以备将来可能再次触发的重新散列操作。


执行rehash的时候业务操作

在进行rehash操作时,字典的删除、查找和更新等操作都需要在这两个哈希表上进行,注意没有新增哦!具体来说,当我们需要在字典中查找一个键时,程序会首先在ht[0]中进行搜索。如果未能在ht[0]中找到相应的键,程序则会继续转向ht[1]进行查找,以确保不会遗漏任何可能的键值对。


注意,在rehash操作进行的过程中,所有新添加的键值对都会被统一存储于ht[1]哈希表中,而ht[0]则不再承担新键值对的添加任务

最后总结

字典是Redis实现多样化功能的核心组件,尤其在数据库和哈希键的构造中发挥着至关重要的作用。Redis精心设计了其字典结构,以哈希表作为基石,确保高效且稳健的数据存取。

  • 双哈希表:每个Redis字典都巧妙地配备了两个哈希表,一个负责日常运作,而另一个则专门用于rehash操作,这种双表机制显著提升了字典在数据变动时的性能表现。
  • 解决冲突:在哈希表中,为了解决这一冲突,Redis巧妙地运用了next指针机制。它将新键值对w对应的节点通过next指针链接到已存在的d节点之前,从而构建了一个链表结构。通过这种方式,Redis不仅有效地解决了键冲突问题,还保证了哈希表在数据插入时的灵活性和高效性。
  • Rehash控制:当哈希表需要进行扩展或收缩以适应数据量的变化时,Redis并非一蹴而就地完成整个rehash过程。相反,它采取了渐进式的策略,逐步将旧哈希表中的键值对迁移到新表中,从而确保了rehash操作对系统性能的影响最小化。
相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
25天前
|
存储 缓存 NoSQL
数据的存储--Redis缓存存储(一)
数据的存储--Redis缓存存储(一)
60 1
|
25天前
|
存储 缓存 NoSQL
数据的存储--Redis缓存存储(二)
数据的存储--Redis缓存存储(二)
37 2
数据的存储--Redis缓存存储(二)
|
22天前
|
消息中间件 缓存 NoSQL
Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。
【10月更文挑战第4天】Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。随着数据增长,有时需要将 Redis 数据导出以进行分析、备份或迁移。本文详细介绍几种导出方法:1)使用 Redis 命令与重定向;2)利用 Redis 的 RDB 和 AOF 持久化功能;3)借助第三方工具如 `redis-dump`。每种方法均附有示例代码,帮助你轻松完成数据导出任务。无论数据量大小,总有一款适合你。
56 6
|
2天前
|
存储 消息中间件 NoSQL
Redis数据结构:List类型全面解析
Redis数据结构——List类型全面解析:存储多个有序的字符串,列表中每个字符串成为元素 Eelement,最多可以存储 2^32-1 个元素。可对列表两端插入(push)和弹出(pop)、获取指定范围的元素列表等,常见命令。 底层数据结构:3.2版本之前,底层采用**压缩链表ZipList**和**双向链表LinkedList**;3.2版本之后,底层数据结构为**快速链表QuickList** 列表是一种比较灵活的数据结构,可以充当栈、队列、阻塞队列,在实际开发中有很多应用场景。
|
1天前
|
存储 NoSQL 关系型数据库
Redis的ZSet底层数据结构,ZSet类型全面解析
Redis的ZSet底层数据结构,ZSet类型全面解析;应用场景、底层结构、常用命令;压缩列表ZipList、跳表SkipList;B+树与跳表对比,MySQL为什么使用B+树;ZSet为什么用跳表,而不是B+树、红黑树、二叉树
|
26天前
|
缓存 NoSQL 关系型数据库
redis和缓存及相关问题和解决办法 什么是缓存预热、缓存穿透、缓存雪崩、缓存击穿
本文深入探讨了Redis缓存的相关知识,包括缓存的概念、使用场景、可能出现的问题(缓存预热、缓存穿透、缓存雪崩、缓存击穿)及其解决方案。
127 0
redis和缓存及相关问题和解决办法 什么是缓存预热、缓存穿透、缓存雪崩、缓存击穿
|
2天前
|
存储 缓存 监控
利用 Redis 缓存特性避免缓存穿透的策略与方法
【10月更文挑战第23天】通过以上对利用 Redis 缓存特性避免缓存穿透的详细阐述,我们对这一策略有了更深入的理解。在实际应用中,我们需要根据具体情况灵活运用这些方法,并结合其他技术手段,共同保障系统的稳定和高效运行。同时,要不断关注 Redis 缓存特性的发展和变化,及时调整策略,以应对不断出现的新挑战。
21 10
|
2天前
|
缓存 监控 NoSQL
Redis 缓存穿透的检测方法与分析
【10月更文挑战第23天】通过以上对 Redis 缓存穿透检测方法的深入探讨,我们对如何及时发现和处理这一问题有了更全面的认识。在实际应用中,我们需要综合运用多种检测手段,并结合业务场景和实际情况进行分析,以确保能够准确、及时地检测到缓存穿透现象,并采取有效的措施加以解决。同时,要不断优化和改进检测方法,提高检测的准确性和效率,为系统的稳定运行提供有力保障。
20 5
|
2天前
|
缓存 监控 NoSQL
Redis 缓存穿透及其应对策略
【10月更文挑战第23天】通过以上对 Redis 缓存穿透的详细阐述,我们对这一问题有了更深入的理解。在实际应用中,我们需要根据具体情况综合运用多种方法来解决缓存穿透问题,以保障系统的稳定运行和高效性能。同时,要不断关注技术的发展和变化,及时调整策略,以应对不断出现的新挑战。
17 4
|
3天前
|
缓存 NoSQL Java
有Redis为什么还要本地缓存?谈谈你对本地缓存的理解?
有Redis为什么还要本地缓存?谈谈你对本地缓存的理解?
12 0
有Redis为什么还要本地缓存?谈谈你对本地缓存的理解?

推荐镜像

更多