深度解析 Redis 存储结构及其高效性背后的机制

server/2024/10/21 18:28:16/

目录

      • 1. Redis 存储结构
        • 存储结构
        • 存储转换
      • 2. 字典实现
        • 数据结构
        • 冲突处理
        • 负载因子
      • 3. 扩容
        • 扩容步骤
        • 影响与优化
      • 4. 缩容
        • 缩容步骤
        • 优化策略
      • 5. 渐进式 Rehash
        • **渐进式 Rehash 的工作原理**
        • Rehash 规则
        • 优势
      • 6. SCAN 命令
        • SCAN 的实现原理
        • 遍历顺序
        • 避免重复和遗漏
        • 使用场景
      • 7. 过期(Expire)机制
        • 过期管理方式
        • 过期键的存储
        • 过期策略优化
      • 8. 大 Key 处理
        • 内存分配问题
        • 性能问题
        • 性能优化策略
      • 9. 跳表实现
        • 跳表的基本概念
        • 理想跳表 vs. Redis 跳表
        • 跳表的操作
        • 跳表的优势
      • 参考

1. Redis 存储结构

Redis 是一个高性能的内存数据库,主要通过多种数据结构来实现高效的数据存储和快速的查询操作。这部分将深入探讨 Redis 的不同存储结构及其内部的转换机制。

存储结构

Redis 以键值对(Key-Value)的形式存储数据,但不仅限于简单的字符串类型。它支持多种复杂的数据类型,每种数据类型都有其特定的应用场景和内部实现机制:
在这里插入图片描述

  • 字符串(String):最基本的数据类型,可以存储任何类型的数据,如文本、数字等。适用于缓存单一值或简单的计数器。

  • 哈希表(Hash):用于存储对象,例如用户信息。哈希表适合存储多个字段和值的集合,类似于关系数据库中的行。

  • 列表(List):一个双向链表,支持从两端高效地插入和删除元素。常用于实现消息队列、任务列表等。

  • 集合(Set):一个无序的、唯一的元素集合,适用于去重操作、标签系统等。

  • 有序集合(Sorted Set, ZSet):类似于集合,但每个元素都有一个分数(score),元素按照分数有序排列。适用于排行榜、按优先级排序的任务队列等。

  • 位图(Bitmap)HyperLogLog地理空间索引(Geo) 等其他高级数据结构,提供更多特定场景的优化支持。

存储转换

为了在不同的数据量和访问模式下保持高效,Redis 会根据具体情况动态调整内部存储结构:

  • 编码转换:Redis 会根据存储的数据量和访问频率,自动选择最合适的内部表示。例如,较小的哈希表可能使用压缩的 ziplist 编码,以减少内存占用,而当哈希表元素数量超过一定阈值时,会转换为普通的哈希表(dict)以提高访问效率。

  • 动态调整:当数据结构的大小或复杂度发生变化时,Redis 会动态调整底层数据结构,以确保性能和内存使用的最优化。例如,列表在元素较少时可能使用压缩列表(ziplist),元素增多后转换为双向链表。

2. 字典实现

Redis 的字典(hash table)是其核心数据结构之一,用于高效地存储和查找键值对。理解字典的内部实现对于优化 Redis 性能至关重要。

数据结构

在这里插入图片描述

Redis 字典主要由以下两个结构组成:

  • dictEntry:表示字典中的一个节点,包含三个部分:

    • 键(key):可以是任意支持的数据类型。
    • 值(value):与键对应的数据。
    • 下一个指针(next):用于链式解决哈希冲突,指向同一个哈希槽中的下一个 dictEntry
  • dictht:哈希表结构,包含以下字段:

    • 表大小(size):当前哈希表的大小,即槽(bucket)的数量。
    • 使用数量(used):当前哈希表中存储的键值对数量。
    • 指针数组(table):一个指向 dictEntry 链表的指针数组,每个槽对应一个链表的头节点。
冲突处理

哈希冲突在任何哈希表实现中都是不可避免的,Redis 采用链地址法(Separate Chaining)来解决冲突:

  • 链地址法:当多个键通过哈希函数映射到同一个槽时,这些键值对会被链接到一个链表中。dictEntry 中的 next 指针用于链接这些冲突的节点。

  • 优化链表:为了提高性能,Redis 在处理链表时采用了多种优化策略,如使用更高效的内存分配和缓存友好的数据结构,减少链表遍历的开销。

负载因子

负载因子 = used / size ; used 是数组存储元素的个数, size 是数组的长度;负载因子越小,冲突越小;负载因子越大,冲突越大;redis 的负载因子是 1 ;

负载因子是衡量哈希表使用效率的重要指标:

  • 高负载因子:意味着哈希表中每个槽平均存储更多的键值对,增加了冲突发生的概率,导致链表长度增加,从而降低查找效率。

  • 负载因子调整:为了维持哈希表的高效性,Redis 会根据负载因子动态调整哈希表的大小。当负载因子超过设定阈值时,进行扩容;当负载因子过低时,进行缩容。

3. 扩容

当字典的负载因子超过阈值(通常是 1)时,Redis 会自动进行扩容操作,以维持哈希表的高效性。
在这里插入图片描述

扩容步骤
  1. 确定新大小:通常将哈希表的大小翻倍,以减少扩容频率并保持低负载因子。

  2. 内存分配:为新的哈希表分配更大的内存空间,确保有足够的槽来存储增加的键值对。

  3. 数据迁移:将现有的键值对重新哈希并迁移到新的哈希表中。这个过程涉及将每个键重新计算哈希值,并根据新哈希表的大小将其分配到相应的槽中。

  4. 替换旧表:完成数据迁移后,新的哈希表替代旧的哈希表,释放旧表的内存空间。

影响与优化
  • 性能影响:扩容是一个耗时的操作,尤其在字典规模较大时,可能导致短暂的性能下降或阻塞。

  • 渐进式扩容:为了减少扩容对性能的影响,Redis 使用渐进式 rehash(详见第 5 节),将扩容过程分摊到多个操作中,避免一次性完成所有数据迁移。

4. 缩容

当字典的负载因子低于阈值(通常是 0.1)时,Redis 会进行缩容操作,以释放不必要的内存资源。
在这里插入图片描述

缩容步骤
  1. 确定新大小:根据当前键值对的数量,计算出一个最小且适合的哈希表大小,避免过度缩小导致频繁的扩缩容操作。

  2. 内存释放:将哈希表大小调整为新的尺寸,释放多余的内存空间。

  3. 数据迁移:与扩容类似,将现有的键值对重新哈希并迁移到新的哈希表中,以适应缩小后的槽数量。

  4. 替换旧表:新的哈希表替代旧的哈希表,释放旧表的内存。

优化策略
  • 避免频繁缩容:为防止由于负载因子频繁波动导致的频繁扩缩容,Redis 采用了渐进式调整和阈值保护机制。

  • 最小缩容步长:设置最小缩容步长,确保每次缩容都能显著减少内存占用,避免频繁的微调。

5. 渐进式 Rehash

为了避免一次性 rehash 导致的性能问题,Redis 采用了渐进式 rehash 技术,将 rehash 过程分散到多个客户端操作中进行。

渐进式 Rehash 的工作原理
  1. 双哈希表:在进行 rehash 时,Redis 同时维护两个哈希表:旧表和新表。新表的大小通常是旧表的两倍或一半,取决于是扩容还是缩容。

  2. 分步迁移:每次客户端执行字典操作(如插入、删除、查找)时,Redis 会迁移部分数据从旧表移动到新表。这些迁移步骤是渐进的,逐步完成整个 rehash 过程。

  3. 操作兼容:在 rehash 过程中,新的插入操作会直接插入到新表,而查找操作则会同时查询旧表和新表,确保数据的一致性。

  4. 完成迁移:当所有数据迁移完成后,旧表被释放,新的哈希表完全接管数据存储。

Rehash 规则
  • 元素重新映射:每个键通过哈希函数重新计算其在新表中的位置,根据新表的大小将其分配到相应的槽中。

  • 渐进迁移:每次操作迁移一定数量的键值对,确保不会一次性占用过多的 CPU 资源,避免阻塞其他客户端请求。

优势
  • 避免阻塞:渐进式 rehash 将 rehash 过程分散到多个小步骤中,避免了长时间的阻塞,提高了 Redis 的响应性。

  • 平滑过渡:在 rehash 过程中,Redis 依然可以正常处理客户端请求,确保服务的连续性和稳定性。

6. SCAN 命令

SCAN 命令是 Redis 提供的一种用于遍历数据的迭代器,旨在替代 KEYS 命令,以实现更高效和安全的键遍历操作。
在这里插入图片描述

SCAN 的实现原理
  • 游标(Cursor)机制SCAN 使用一个游标来记录遍历的进度。客户端每次调用 SCAN 时,提供上一次返回的游标,服务器根据游标继续遍历剩余的键。

  • 非阻塞遍历:与 KEYS 一次性返回所有键不同,SCAN 分批次返回部分键,避免了阻塞服务器和客户端的情况。

遍历顺序
  • 伪随机顺序SCAN 并不保证键的遍历顺序,而是以伪随机的方式返回键。这是因为 Redis 使用哈希槽和渐进式 rehash,遍历顺序可能会因哈希表的动态变化而变化。

  • 高位进位加法:为了在不同调用之间有效地管理游标,SCAN 使用一种高位进位加法的方式来确保遍历的连续性和完整性。

避免重复和遗漏
  • 一致性保证:Redis 通过特定的算法确保在遍历过程中尽量避免重复和遗漏键,即使在遍历过程中发生了哈希表的变化。

  • 极端情况:在某些极端情况下,如 SCAN 过程中发生多次缩容或扩容,可能会导致部分键重复返回或遗漏。这种情况虽然罕见,但需要在应用层进行适当的处理。

使用场景
  • 大规模数据遍历:适用于需要遍历大量键但不希望影响服务器性能的场景,如数据导出、统计分析等。

  • 实时应用:由于 SCAN 是非阻塞的,适合在实时应用中使用,避免长时间的阻塞操作影响用户体验。

7. 过期(Expire)机制

Redis 提供了键的过期机制,允许在指定时间后自动删除键,以实现数据的自动过期和内存的自动回收。

过期管理方式

Redis 通过两种主要方式来管理过期键:

  • 惰性删除(Lazy Deletion)

    • 工作原理:当客户端访问一个键时,Redis 会检查该键是否设置了过期时间。如果过期时间已到,Redis 会立即删除该键,并返回不存在的结果。
    • 优点:节省资源,只在需要时进行检查和删除,适用于不经常访问的键。
    • 缺点:如果过期键长时间不被访问,可能会占用内存,导致内存泄漏。
  • 定时删除(Active Deletion)

    • 工作原理:Redis 定期随机扫描一部分设置了过期时间的键,并删除过期的键。这种扫描过程由 Redis 的后台线程自动执行,不依赖于客户端的访问操作。
    • 优点:能够及时回收过期键,避免内存被无效数据占用。
    • 缺点:可能会带来一定的 CPU 开销,特别是在大量键设置了过期时间时。
过期键的存储
  • 单独数据结构:Redis 使用专门的数据结构(如一个有序集合)来存储所有设置了过期时间的键及其对应的过期时间。

  • 高效查找:通过有序集合的特性,Redis 能够高效地查找和删除过期键,确保过期管理的性能。

过期策略优化
  • 最少过期时间的键优先删除:在进行定时删除时,Redis 会优先删除最早过期的键,以最大化内存回收的效率。

  • 批量删除:为了减少每次删除操作的开销,Redis 通常会批量删除多个过期键,而不是一次删除一个。

8. 大 Key 处理

在 Redis 中,“大 Key”指的是包含大量数据的键,如包含大量元素的哈希表、有序集合或列表。这些大 Key 在操作时可能会带来显著的性能问题,需要特别注意和优化。

内存分配问题
  • 扩容时的内存分配:大 Key 在进行扩容(如哈希表扩容或跳表扩容)时,需要一次性分配大量内存。这可能导致 Redis 出现短暂的卡顿或内存分配失败。

  • 删除时的内存回收:一次性删除大 Key 需要释放大量内存,可能会引发内存碎片或 GC(如果使用了内存管理机制)的额外开销。

性能问题
  • 高延迟操作:对大 Key 的操作,如大批量插入、删除或查询,可能会导致高延迟,影响 Redis 的整体性能和响应时间。

  • 阻塞问题:某些操作如重写 AOF(Append-Only File)或 RDB(Redis Database)快照时,如果包含大 Key,可能会导致 Redis 阻塞时间增加。

性能优化策略
  • 分片存储:将大 Key 分片存储为多个较小的 Key,分散存储负载,减少单个 Key 的大小。例如,将一个大型列表分割为多个小列表。

  • 批量操作优化:使用流水线(Pipeline)或事务(Transaction)等机制,批量处理大 Key 的操作,减少网络往返和操作延迟。

  • 异步处理:对于耗时的操作,可以考虑使用异步处理或后台任务,避免阻塞主线程。

  • 内存优化:选择合适的数据结构和编码方式,尽量减少大 Key 的内存占用。例如,使用压缩列表(ziplist)来存储小规模的哈希表或列表。

9. 跳表实现

Redis 使用跳表(Skip List)作为有序集合(Sorted Set, ZSet)的底层实现,以提供高效的有序数据存储和快速的增删查改操作。
在这里插入图片描述

跳表的基本概念

跳表是一种基于多层链表的数据结构,通过在多个层级上建立跳跃指针,实现快速的查找和遍历操作。其主要特点包括:

  • 多层结构:每个元素在不同层级上都有可能存在跳跃指针,层级越高,跳跃跨度越大。

  • 随机化:元素在跳表中的层级通常是随机决定的,这种随机化策略保证了跳表在平均情况下具有良好的性能。

  • 时间复杂度:跳表的查找、插入和删除操作的平均时间复杂度为 (O(\log N)),与平衡树相当,但实现更为简单。

理想跳表 vs. Redis 跳表
  • 理想跳表

    • 结构:每隔一个节点增加一个层级,形成类似二叉树的结构。
    • 性能:在理想情况下,跳表能够高效地保持有序性和快速的操作性能。
  • Redis 跳表

    • 扁平化设计:为了节省内存,Redis 的跳表采用了更扁平化的结构,限制了跳表的最大层级为 32 层。这与理想跳表的多层级结构有所不同,但在实际应用中仍能提供高效的操作性能。

    • 层级生成:Redis 使用概率机制(通常为 1/4 的概率提升到更高层级)生成跳表的层级,确保跳表在大多数情况下具有良好的平衡性和性能。

    • 节点结构:每个跳表节点包含指向下一个节点的指针,以及用于排序的分数(score)和对应的成员(member)。

跳表的操作
  • 查找(Search):从最高层级开始,依次向右移动,直到找到第一个大于或等于目标分数的节点,逐层向下直到最低层级。

  • 插入(Insert):首先确定新节点在每个层级上的位置,然后插入新的跳跃指针,维护跳表的有序性。

  • 删除(Delete):查找到要删除的节点,在所有层级上移除其跳跃指针,维护跳表的结构完整性。

跳表的优势
  • 内存效率高:相比于平衡树,跳表的实现更为简单,且内存占用更少。

  • 实现简便:跳表的代码实现相对简单,易于维护和优化。

  • 性能稳定:在大多数情况下,跳表能够提供与平衡树相当的性能,且由于其随机化的结构,避免了最坏情况的性能下降。

参考

0voice · GitHub


http://www.ppmy.cn/server/133689.html

相关文章

实战RAG第一天——llama_index向量索引,查询引擎,搜索知识库问答,全部代码,保姆级教学

一、llama_index简介 llama_index(以前称为 GPT Index)是一个用于构建、查询、索引大型文档和数据集的开源框架。它的核心功能是帮助开发者将大语言模型(LLM)与自己的数据集无缝集成,从而进行知识库的构建、查询等任务。llama_index 使用 Python 编写,并结合了多种大语言…

Github学生包的JetBrains认证过期/idea认证过期如何解决?

官网通过Github更新状态即可JetBrains Account 注意要到邮箱走流程

Oracle+11g+笔记(7)-数据库空间管理

Oracle11g笔记(7)-数据库空间管理 7、数据库空间管理 存储空间是数据库系统中非常重要的资源,无论是数据库中的对象还是数据库中的数据都需要空间进行存储,一旦 数据库空间被全部占用,那么该数据库系统就不能再接受任何对象和数据&#xf…

运维怎么转行网络安全?

经常有人问我:干网工、干运维多年遇瓶颈,想学点新技术给自己涨涨“身价”,应该怎么选择? 聪明人早已经用脚投票:近年来,越来越多运维的朋友寻找新的职业发展机会,将目光聚焦到了网络安全产业。…

CICD 持续集成与持续交付

目录 一 CICD是什么 1.1 持续集成(Continuous Integration) 1.2 持续部署(Continuous Deployment) 1.3 持续交付(Continuous Delivery) 二 git工具使用 2.1 git简介 2.2 git 工作流程 三 部署git …

linux 系统怎么使用

Linux系统的使用涉及多个方面,包括文件管理、目录操作、用户管理、进程管理、网络配置等。以下是对Linux系统基础使用的详细介绍: 一、文件管理 查看文件和目录 ls:列出当前目录的内容。ls -l:以长格式列出当前目录的内容&#x…

vulnhub靶场之JOY

一.环境搭建 1.靶场描述 Does penetration testing spark joy? If it does, this machine is for you. This machine is full of services, full of fun, but how many ways are there to align the stars? Perhaps, just like the child in all of us, we may find joy in …

Vue3中ref和reactive的对比

1. ref 定义 用途: 用于创建基本数据类型或单一值的响应式引用。语法: const myRef ref(initialValue); 特性 返回一个包含 .value 属性的 Proxy 对象。适用于基本数据类型(如数字、字符串、布尔值等)和单一值。 import { ref } from vue;const co…