Skip to content

Redis 面试题

更新: 8/12/2025 字数: 0 字 时长: 0 分钟

Redis 中常见的数据类型有哪些?

Redis 常见的五种基本数据类型:

  1. String(字符串)

    • 大白话:最简单的,就是存文本、数字什么的。可以是一个单词,一句话,一个 JSON 字符串,或者一个整数。
    • 能干啥:存用户信息(用户名、年龄)、文章内容、计数器(点赞数、访问量)。
    • 特点:可以存二进制数据,最大 512MB。对整数有专门的优化,进行加减操作时很快。
  2. Hash(哈希)

    • 大白话:就像一个“字典”或者“对象”。一个键(key)对应一个哈希,这个哈希里又有很多“字段 - 值”对。
    • 能干啥:存一个对象的多个属性,比如用户的 ID 对应一个 Hash,这个 Hash 里有 nameageemail 等字段。
    • 特点:适合存储对象数据,可以一次性获取或修改对象的某个字段,节省内存。
  3. List(列表)

    • 大白话:就像一个“双向链表”或者“队列”。可以从两头(左边或右边)添加或删除元素。
    • 能干啥:
      • 消息队列:生产者往列表右边添加消息,消费者从左边取消息。
      • 最新动态:记录用户最新发布的几条微博、新闻列表。
      • 任务队列:先进先出的任务排队。
    • 特点:顺序存储,可以实现栈、队列等功能。
  4. Set(集合)

    • 大白话:就像数学里的“集合”。里面存了一堆不重复的元素,而且这些元素是无序的。
    • 能干啥:
      • 好友关系:共同好友、关注的人。
      • 抽奖活动:存储参与抽奖的用户 ID,自动去重。
      • 标签系统:一篇文章有多个标签,一个标签对应多篇文章。
    • 特点:元素唯一,支持集合运算(交集、并集、差集)。
  5. ZSet / Sorted Set(有序集合)

    • 大白话:类似于 Set,也是存不重复的元素,但每个元素都会关联一个“分数”(score),然后根据这个分数来排序。分数相同就按字典序排。
    • 能干啥:
      • 排行榜:游戏积分榜、微博热搜榜(分数是热度)。
      • 带权重的任务队列:优先级高的任务先执行。
    • 特点:元素唯一且有序,可以根据分数范围查询。
Redis 为什么这么快?

主要有以下几个关键原因:

  1. 基于内存操作:

    • 大白话:就像你把所有要用的文件都提前放到电脑内存里,而不是每次都去硬盘里找。
    • 解释:Redis 的所有数据都存储在内存中,而内存的读写速度比硬盘快好几个数量级。这是 Redis 高性能的根本原因。
  2. 高效的数据结构:

    • 大白话:Redis 不只是简单地把数据放进内存,它还为每种数据类型设计了非常适合快速操作的“容器”。
    • 解释:Redis 内部使用了如跳跃表(skiplist)、哈希表、压缩列表等经过精心优化的数据结构。这些数据结构在进行增删改查操作时,效率非常高,通常能达到 O(1) 或 O(logN) 的时间复杂度。
  3. 单线程模型(核心处理部分):

    • 大白话:就像一个厨房里只有一位顶尖大厨,他做菜速度飞快,而且不用担心和别人抢炉子。
    • 解释:Redis 的核心处理(处理客户端请求、读写数据)是单线程的。这意味着它省去了多线程场景下常见的上下文切换开销、锁竞争问题、死锁等复杂问题。这简化了设计,避免了同步开销,使得操作流程非常顺畅和高效。
    • 误区:虽然核心是单线程,但 Redis 在处理持久化(RDB/AOF)、异步删除等后台任务时,会使用子进程或额外的线程来处理,避免阻塞主线程。
  4. 非阻塞 I/O 多路复用:

    • 大白话:就像餐馆里只有一个服务员,但他非常聪明,他不是等一个客人点完菜才去服务下一个客人,而是同时留意所有客人的状态,谁叫他他就去谁那里。
    • 解释:Redis 使用 I/O 多路复用技术(如 epoll/kqueue/select)来处理网络连接。它可以在一个线程中同时监听多个客户端连接的 I/O 事件。当某个连接有数据可读或可写时,Redis 才会去处理,而不是为每个连接创建一个线程去等待,从而避免了大量线程创建和上下文切换的开销。
  5. C 语言编写:

    • 大白话:用最高效的语言写的,底层优化做得好。
    • 解释:Redis 使用 C 语言编写,C 语言对内存和系统资源的控制力更强,运行效率更高。
为什么 Redis 设计为单线程?6.0 版本为何引入多线程?

为什么 Redis 早期设计为单线程?

Redis 早期(直到 6.0 版本之前,核心处理部分)设计为单线程,主要基于以下几点考虑:

  1. 避免并发控制开销:

    • 大白话:单线程就不用操心多个员工同时操作一个文件时,怎么防止他们互相覆盖、互相打架的问题了。
    • 解释:多线程编程最大的挑战就是并发控制,需要引入锁、信号量等机制来保护共享资源,而这些机制会带来额外的开销(上下文切换、锁竞争、死锁等),并且会增加程序的复杂性。单线程模型避免了这些问题,代码更简洁,执行效率更高。
  2. 内存访问速度快:

    • 大白话:Redis 最大的瓶颈不是 CPU,而是内存。
    • 解释:Redis 的数据全部在内存中,内存的访问速度非常快。在这种情况下,CPU 往往不是瓶颈,I/O 操作(网络请求、持久化)才是。单线程通过 I/O 多路复用技术,已经能够很好地处理并发连接,避免了不必要的 CPU 等待。
  3. 更强的可维护性:

    • 大白话:一个人干活,出错了更容易排查。
    • 解释:单线程模型使得 Redis 的代码逻辑更简单,更容易理解和维护,也减少了出现复杂并发 Bug 的可能性。
  4. CPU 瓶颈不是主要限制:

    • 大白话:Redis 的速度太快了,一般情况下,CPU 还没忙完,网络和内存就已经把数据准备好了。
    • 解释:对于 Redis 这种内存型数据库,它的瓶颈通常在于内存容量、网络带宽,而非 CPU 的计算能力。单线程足以应对绝大多数场景,而且可以通过部署多个 Redis 实例(Redis Cluster)来利用多核 CPU。

Redis 6.0 版本为何引入多线程?

虽然单线程有很多优点,但随着网络硬件的升级(万兆网卡普及)和 CPU 核心数的增加,网络 I/O 的处理逐渐成为了 Redis 单线程模型的瓶颈

  • 大白话:以前网速慢,一个大厨就能忙过来。现在网速太快了,数据像洪水一样涌进来,大厨一个人处理网络请求和数据操作就有点忙不过来了,尤其是在数据量特别大、网络带宽又充足的情况下。
  • 解释:以前 Redis 瓶颈更多是 CPU 核数不够用或网络带宽不足。但现在,CPU 核心数普遍增多,网卡性能大幅提升,导致主线程在处理网络读写(数据包解析、发送)时,成为了新的性能瓶颈。

Redis 6.0 引入多线程的目的:

Redis 6.0 引入的多线程不是为了让数据操作(命令执行)变成多线程,而是为了分担网络 I/O 读写和解析的工作

  1. 多线程处理网络 I/O:Redis 6.0 将网络数据的读写和解析任务,从主线程中剥离出来,交给多个 I/O 线程去处理。
  2. 主线程依然处理核心命令:主线程依然负责执行 Redis 命令(例如 SET, GET, LPUSH 等),这意味着数据结构的访问和操作仍然是单线程的,这继续保留了单线程模型简单、无锁竞争的优点。
  3. 提升吞吐量:通过将网络 I/O 任务并行化,Redis 可以在处理相同数量的客户端连接时,显著提高吞吐量,尤其是在高并发和大数据量传输的场景下。

总结:Redis 6.0 引入多线程是对单线程模型的一种优化和增强,旨在解决网络 I/O 瓶颈,而不是改变其核心命令执行的单线程特性。它在保持原有的简单高效优势的同时,更好地利用了现代多核 CPU 的性能。

Redis 中跳表的实现原理是什么?

大白话理解:

想象一下你有一本厚厚的、按字母顺序排列的电话簿。如果你要找一个人的电话,你当然可以从头开始一页一页翻(这就是普通链表)。但这太慢了!

为了加速查找,你在电话簿上加了几层目录:

  • 第一层目录:最详细的目录,就是电话簿本身,按字母顺序排列所有名字。
  • 第二层目录:粗略目录,每隔几页标上“A 字头”、“B 字头”等等。
  • 第三层目录:更粗略的目录,可能只有“A-D”、“E-H”这样的范围。
  • 第四层目录:最粗略的,比如“上册”、“下册”。

当你要找一个名字时,你不是从头翻,而是先从最粗略的目录开始,快速跳到大概的范围,然后进入下一层目录,再跳到更小的范围,直到最终在最详细的目录中找到。

Redis 跳表(SkipList)的实现原理就是这个思想:

Redis 的 ZSet (有序集合) 底层就是用跳表来实现的。

  1. 数据结构:

    • 节点 (Node):跳表由多个节点组成,每个节点存储:
      • 值 (value/member):ZSet 中实际存储的元素(比如排行榜上的用户名称)。
      • 分数 (score):决定排序的数值(比如积分)。
      • 层 (level):每个节点都有一个随机生成的层数(高度)。层数越高,这个节点在“目录”中出现的频率就越低(跳跃的距离就越大)。
      • 前进指针 (forward pointer):每个节点在每一层都有一个指向下一个节点的指针。
    • 头节点 (Header):一个特殊的节点,不存储实际数据,它的层数是跳表的最高层数。
    • 尾节点 (Nil):一个特殊的节点,表示跳表的结束。
  2. 查找过程:

    • 当查找一个元素时,从跳表的最高层开始。
    • 在当前层,沿着前进指针向右遍历,如果当前节点的分数小于或等于要查找的分数,就继续向右。
    • 如果当前节点的分数大于要查找的分数,或者已经到了当前层的末尾,就向下一层。
    • 重复这个过程,直到到达最底层,就能找到目标元素或其插入位置。
  3. 插入过程:

    • 首先通过查找过程,找到新元素在每一层应该插入的位置。
    • 随机生成新节点的层数。
    • 修改相关层中节点的前进指针,将新节点插入到相应的位置。
  4. 删除过程:

    • 通过查找过程定位到要删除的节点。
    • 更新受影响的层中节点的前进指针,跳过要删除的节点。
    • 释放被删除节点的内存。

跳表的优点:

  • 查找、插入、删除的平均时间复杂度都是 O(logN)。 和红黑树、B+ 树等平衡二叉树类似,但实现起来更简单。
  • 维护简单:相比平衡二叉树,跳表的插入和删除操作只需要修改局部指针,不需要进行复杂的旋转或重新平衡操作。
  • 空间换时间:通过增加额外的指针(层数),来减少查找时遍历的节点数量。

为什么 Redis 不用红黑树而用跳表?

虽然红黑树的性能和跳表类似,但跳表在以下方面可能更具优势:

  • 实现简单:跳表的实现比红黑树更简单、更直观,Bug 更少。
  • 范围查找:跳表在进行范围查找时非常高效,例如查找分数在某个范围内的所有元素,只需在最底层遍历。红黑树需要中序遍历,效率可能略低。
  • 并发性能:在多线程环境下,跳表的并发性能可能更好,因为其局部性更强,加锁粒度可以更小(但 Redis 是单线程处理命令,这点不明显)。
Redis 的 hash 是什么?

大白话理解:

Redis 的 Hash 类型就像一个“哈希表”或者“字典”(在编程语言中常被称为 Map 或 Dictionary)。你可以把它想象成一个箱子,这个箱子有自己的名字(Redis key),箱子里面又分了很多小格子,每个小格子都有自己的标签(field)和里面放的东西(value)。

结构:

key (顶层 Redis key) → Hash (哈希表)

  • field1value1
  • field2value2
  • field3value3
  • ...

能干啥?

最典型的应用是存储对象

  • 传统方式(String):存储一个用户对象,你可能需要存多个 String key:

    • user:1:nameAlice
    • user:1:age30
    • user:1:email[email protected]
    • 这样不仅占用了更多 Redis key,而且获取用户所有信息需要多次网络请求。
  • Redis Hash 方式:存储一个用户对象:

    • user:1 (Hash key)
    • 一次 HGETALL user:1 就能获取用户所有信息,大大减少了网络开销。

底层实现:

Redis 的 Hash 类型底层使用了两种数据结构来优化存储,会根据存储的数据量和字段长度自动切换:

  1. ziplist (压缩列表):

    • 大白话:如果你箱子里的小格子(field-value 对)很少,而且格子里的东西也不大,Redis 会把它们紧凑地挨在一起放,非常省空间。
    • 条件:当 Hash 存储的字段数量较少(hash-max-ziplist-entries 配置,默认 512)且所有字段和值的大小都较小(hash-max-ziplist-value 配置,默认 64 字节)时,Redis 会使用 ziplist
    • 特点:内存效率极高,但在增删改时可能需要重新分配内存,效率会略低。
  2. hashtable (哈希表):

    • 大白话:如果箱子里的小格子很多,或者格子里的东西很大,Redis 就会用一个真正的哈希表(像 Java 的 HashMap),这样查找速度更快。
    • 条件:当 Hash 满足不了 ziplist 的使用条件时(字段数量或字段值大小超过阈值),Redis 会将底层存储从 ziplist 转换为 hashtable
    • 特点:查找效率高(O(1)),但会占用更多内存。

优点:

  • 节省内存:尤其在存储大量小对象时,使用 Hash 比多个 String 更节省内存,因为每个 Hash 只需要一个顶层 Key。
  • 减少网络开销:获取或修改对象的多个属性时,一次操作就能完成,减少了客户端与 Redis 服务器之间的网络往返次数。
Redis Zset 的实现原理是什么?

大白话理解:

Redis 的 ZSet(有序集合)就像一个带分数的排行榜。每个榜单项目(member)都有一个独一无二的名字,并且有一个对应的分数(score)。Redis 会根据这个分数把它们从小到大排好。分数相同的话,就按名字的字母顺序排。

结构:

key (顶层 Redis key) → ZSet (有序集合)

  • member1 (score1)
  • member2 (score2)
  • member3 (score3)
  • ... (按 score 排序)

能干啥?

最典型的应用就是各种排行榜,比如游戏积分榜、商品销售榜、微博热搜榜等等。你还可以根据分数范围查询,比如找出积分在 100 到 200 之间的玩家。

底层实现:

Redis 的 ZSet 也是根据存储的数据量和元素大小,以及 ZSet 的具体操作特点,自动切换底层的数据结构:

  1. ziplist (压缩列表):

    • 大白话:当你的排行榜项目很少,而且每个项目的名字和分数也都不长时,Redis 会把它们紧凑地放在一起,非常省内存。
    • 条件:当 ZSet 存储的元素数量较少(zset-max-ziplist-entries 配置,默认 128)且每个元素的值和分数都较小(zset-max-ziplist-value 配置,默认 64 字节)时,Redis 会使用 ziplist
    • 存储方式:在 ziplist 中,member 和 score 是紧挨着存放的,通过有序插入保证了顺序。
    • 特点:内存效率高,但在元素较多时,增删改查需要移动大量数据,效率会下降。
  2. skiplist (跳表) + hashtable (哈希表):

    • 大白话:当排行榜项目增多,或者单个项目内容变大时,Redis 会切换到更强大的组合拳:跳表用来快速查找和范围查询,哈希表用来快速根据项目名字找到它的分数。
    • 条件:当 ZSet 不满足 ziplist 的条件时,Redis 会转换为 skiplisthashtable 的组合结构。
    • skiplist (跳表):负责根据 score 来排序和查找。它能以 O(logN) 的时间复杂度进行插入、删除、查找和范围查找(通过分数)。
    • hashtable (哈希表):负责根据 member(元素名称)快速查找其对应的 score。这样,你可以根据元素名直接获取其分数,时间复杂度为 O(1)。
    • 特点:这种组合实现了高效的插入、删除、查找和范围查询。跳表保证了有序性,哈希表保证了根据 member 的快速访问。

总结:

Redis 巧妙地利用了多种底层数据结构,并根据数据的特性和操作的频繁程度进行自动切换,以达到在不同场景下内存占用和性能的最佳平衡。这种设计是 Redis 如此高效和灵活的关键之一。

Redis 中如何保证缓存与数据库的数据一致性?

缓存(Redis)是数据库(MySQL)的“副本”,为了保证数据不“脱节”,它们需要保持同步。

常见方案(及其优缺点):

  1. 先更新数据库,再删除缓存 (推荐)

    • 步骤修改数据库 → 删除缓存
    • 大白话:先把真实数据改了,然后告诉缓存“你那里是旧数据了,删掉吧,下次有人问我直接给你最新版”。
    • 优点:这是最常用且相对安全的方案。
    • 缺点:可能会有短暂的不一致(删除缓存失败时)。可以配合消息队列(MQ)来重试删除,保证最终一致性。
  2. 先删除缓存,再更新数据库 (不推荐)

    • 步骤删除缓存 → 修改数据库
    • 大白话:先告诉缓存“你那里没用了,清空”,然后才去改真实数据。
    • 缺点:并发场景下,非常容易导致数据库和缓存不一致,因为在删除缓存后,数据库更新前,可能有读请求来了,把旧数据又加载到了缓存。
  3. 其他方案:

    • 先更新缓存,再更新数据库:复杂,容易不一致。
    • 先更新数据库,再更新缓存:复杂,更新缓存的开销大。
    • 缓存双删策略(延迟双删):针对“先更新数据库,再删除缓存”的优化,删除缓存后,再延迟一小段时间(如几百毫秒)再次删除缓存,以应对极端并发场景。
    • 使用 Binlog 异步更新缓存:
      • 大白话:让数据库自己通知缓存。数据库的所有修改都会记录在 Binlog(操作日志)里。我们可以弄一个程序,专门“监听”这些 Binlog 的变化,一旦发现数据库有改动,就通过异步方式去更新或删除缓存。
      • 优点:数据库事务和缓存更新解耦,可靠性高,能保证最终一致性。
      • 缺点:实现相对复杂,需要引入额外的组件(如 Canal)。

选择原则:

  • 如果追求实时一致性(即时可见新数据):

    • 推荐先写 MySQL(数据库),再删除 Redis(缓存)。虽然短期内可能存在短暂不一致(比如删除失败或在删除瞬间有旧数据被读取到),但配合消息队列或延迟双删,能最大限度保证数据一致。
  • 如果追求最终一致性(数据迟早会一致):

    • 推荐:使用 Binlog + 消息队列的方式。这种方案更健壮,可以重试和顺序消费,能最大程度地保证缓存与数据库的最终一致性。

总结:最常用的方法是“先更新数据库,再删除缓存”,并结合其他机制(如消息队列、延迟双删)来增强其可靠性。对于更高一致性或复杂业务,可以考虑基于 Binlog 的异步同步方案。

Redis 中的缓存击穿、缓存穿透和缓存雪崩是什么?

这三个是使用缓存时常见的“灾难”,需要我们提前预防。

缓存击穿 (Cache Breakdown)

  • 大白话:某个热点数据在缓存中失效了,但此时有大量并发请求同时来访问这个数据。这些请求都会穿过缓存,直接打到数据库上,导致数据库瞬间压力过大,甚至崩溃。
  • 场景:某个明星的微博数据,本来缓存着。突然缓存过期了,成千上万的用户同时访问这条微博。
  • 如何解决:
    1. 设置热点数据永不过期:对于特别热点且不经常变化的数据,可以考虑永不过期(或设置一个非常长的过期时间)。
    2. 使用互斥锁 (Mutex Lock):当第一个请求发现缓存失效时,它先去获取一个分布式锁。获取到锁的请求才去数据库查询,其他请求则等待。等获取到锁的请求把数据从数据库加载到缓存后,再释放锁,其他等待的请求就可以从缓存中获取数据了。
      • 大白话:就好像电梯坏了,大家一窝蜂去爬楼梯。现在大家约定,谁先到电梯口,谁去拿维修工具修电梯,其他人先在外面等着,修好了大家再排队坐电梯。
    3. 异步更新:不直接淘汰热点数据,而是后台线程定时刷新或在数据即将过期时异步加载新数据。

缓存穿透 (Cache Penetration)

  • 大白话:大量请求去查询一个根本不存在的数据(比如,用户 ID 为 -1 的数据)。缓存和数据库里都没有这个数据。每次请求都会穿透缓存,直接打到数据库,而且因为数据不存在,缓存也不会存储结果,导致每次都穿透,数据库压力倍增。
  • 场景:恶意攻击者不断用不存在的商品 ID 来查询。
  • 如何解决:
    1. 布隆过滤器 (Bloom Filter):在缓存层和数据库之间加一个布隆过滤器。布隆过滤器可以判断某个数据“可能存在”或“一定不存在”。如果布隆过滤器判断“一定不存在”,就直接返回空,不再查询数据库。
      • 大白话:就像在查询前加个“黑名单”,如果请求查的是黑名单上的数据(不存在的),直接拒绝。
      • 注意:布隆过滤器有误判率,可能会把存在的误判为不存在,但可以通过增大空间来降低误判率。
    2. 缓存空值或默认值:如果从数据库查询的结果为空,也把这个空值或一个默认值缓存起来,并设置一个较短的过期时间。这样下次再有请求来查询这个不存在的数据时,就可以直接从缓存中返回空,避免了对数据库的压力。
      • 大白话:查不到就给个“查无此人”的牌子,下次再有人问同样的人,直接把牌子给他看。

缓存雪崩 (Cache Avalanche)

  • 大白话:大量缓存数据在同一时间集中失效(比如,缓存服务重启,或者某个时间点设置了大量缓存一起过期)。由于缓存中没有数据,所有的请求都像雪崩一样,瞬间涌向数据库,导致数据库压力激增甚至宕机。
  • 场景:双十一零点,所有商品缓存都在 24 小时后同时过期。或者 Redis 服务器宕机了。
  • 如何解决:
    1. 缓存过期时间随机化:给缓存的过期时间加上一个小的随机值,避免大量缓存同时失效。
      • 大白话:大家约定好的过期时间,别都设成零点,错开个几分钟,分批过期。
    2. 构建多级缓存:引入多级缓存体系(如本地缓存 + Redis 缓存),当 Redis 出现问题时,请求可以先打到本地缓存。
    3. 熔断、降级、限流:
      • 熔断:当数据库压力过大时,直接拒绝一部分请求,保护数据库。
      • 降级:某些非核心业务可以暂停使用缓存,直接返回静态数据或默认数据,减轻数据库压力。
      • 限流:控制进入数据库的请求 QPS(每秒查询数),超过阈值就排队或拒绝。
    4. 使用高可用架构:Redis 集群、主从复制 + 哨兵模式等,确保 Redis 服务本身的稳定性和可用性。
    5. 数据预热:在业务高峰期到来之前,提前将热点数据加载到缓存中,避免集中失效。
Redis String 类型的底层实现是什么?(SDS)

虽然 Redis String 看起来就是简单的字符串,但它在底层并不是直接使用 C 语言的字符串(以 \0 结尾的字符数组)。Redis 为它设计了一种更聪明、更高效的结构,叫做 SDS (Simple Dynamic String),简单动态字符串。

为什么不用 C 语言字符串(char*)?

C 语言字符串有几个缺点:

  1. 获取长度 O(N):C 字符串要遍历到 \0 才能知道长度。
  2. 不安全:字符串拼接时如果空间不够容易发生缓冲区溢出。
  3. 修改字符串:频繁修改(追加、截断)字符串时,需要频繁地重新分配内存,效率低。

SDS (Simple Dynamic String) 的结构:

SDS 是一个结构体,它比普通的 char* 多了一些信息:

c
struct sdshdr {
    long len;      // 已使用的字节数 (当前字符串的长度)
    long alloc;    // 总分配的字节数 (字符串总容量,不包括\0)
    unsigned char flags; // 3 位用来表示类型
    char buf[];    // 存储字符串数据,以 \0 结尾
};

SDS 相对于 C 字符串的优点:

  1. 获取长度 O(1):直接通过 len 字段就可以获取字符串长度,效率非常高。

    • 大白话:字符串自己知道自己有多长,不用每次都去数一遍。
  2. 杜绝缓冲区溢出:

    • 大白话:在修改(比如追加)字符串时,SDS 会提前检查空间是否足够。如果不够,它会先进行扩容,然后才进行操作,避免了溢出。
    • 空间预分配:在扩容时,SDS 不仅仅分配“刚好够用”的空间,而是会额外预留一些空间。比如,如果你要追加字符串,当空间不足时,如果当前字符串长度小于 1MB,会预分配和当前长度相等的额外空间;如果大于 1MB,则预分配 1MB 的空间。
    • 惰性空间释放:当字符串缩短时,SDS 不会立即释放多余的空间,而是把这些空间标记为“可用的”,留着下次扩容用。
    • 优点:减少了内存重新分配的次数,提高了修改操作的效率。
  3. 二进制安全:

    • 大白话:C 字符串只能存文本,遇到 \0 就认为字符串结束了。但 SDS 可以存任意二进制数据(图片、视频、序列化的对象),它只通过 len 字段来判断字符串的结束,而不是 \0
    • 优点:使得 Redis 可以存储各种类型的数据,不仅仅是文本。
  4. 兼容 C 字符串函数:

    • 虽然 SDS 内部有 len 字段,但 buf 数组仍然以 \0 结尾,这使得它可以直接作为 C 字符串传递给 C 语言标准库函数使用,增强了兼容性。

总结:SDS 是 Redis 字符串类型高性能和灵活性的重要基石。它通过牺牲一点点内存空间(额外存储 lenallocflags 和预留空间)来换取操作的高效和安全。

Redis 中如何实现分布式锁?

大白话理解:

分布式锁就像是你在一个大办公室里(分布式系统),有一份绝无仅有的重要文件(共享资源)。为了防止多个人同时去修改这份文件导致错误,你需要一个“门卫”(分布式锁)。谁想修改文件,就得先找门卫要“钥匙”。门卫把唯一的钥匙给一个人,其他人就只能等着。等拿到钥匙的人改完文件,把钥匙还给门卫,其他人才能继续去拿钥匙。

Redis 实现分布式锁的基本原理:

Redis 提供了一些命令,可以很巧妙地实现这个“门卫”和“钥匙”的机制。最核心的命令是 SETNX (Set if Not eXists) 和 SET 命令的扩展。

实现步骤:

  1. 加锁 (Acquire Lock):

    • 使用 SET key value NX PX milliseconds 命令。
    • key:锁的名称。
    • value:一个随机字符串,用于标识当前请求(比如一个 UUID),这是为了在释放锁时识别是不是自己的锁
    • NX:表示“只在键不存在时才设置”。如果键已经存在(说明锁已经被别人持有),则设置失败,返回 nil
    • PX milliseconds:设置锁的过期时间(毫秒),这是一个非常重要的步骤,用于防止死锁。即使持有锁的客户端崩溃了,锁也能在一定时间后自动释放。
    • 大白话:“门卫,把‘商品库存锁’的钥匙给我,钥匙上写上我的名字和独一无二的编号。如果钥匙已经被别人拿走了,你就别理我了。另外,这把钥匙只在我手里呆 10 秒钟,10 秒后你自动收回。”
  2. 执行业务逻辑:

    • 如果 SET 命令返回成功,表示加锁成功,客户端可以执行需要保护的业务逻辑(比如扣减库存)。
  3. 释放锁 (Release Lock):

    • 为了防止误删(即删除了别人的锁),需要先获取锁的 value,判断是不是自己设置的那个 value,如果是,才执行 DEL key 命令删除锁。

    • 这个操作必须是原子性的! 否则可能出现:A 判断是自己的锁 → 锁过期自动释放 → B 拿到锁 → A 执行 DEL 删除了 B 的锁。

    • 解决方案:使用 Lua 脚本 来保证“判断”和“删除”的原子性。

      lua
      if redis.call("get", KEYS[1]) == ARGV[1] then
          return redis.call("del", KEYS[1])
      else
          return 0
      end
    • 大白话:“门卫,我把‘商品库存锁’还给你。你看看钥匙上是不是我自己的编号?如果是,你再收回,不是就别动。”(这个“看是不是自己的钥匙”和“收回钥匙”的动作,必须在一个步骤里完成,不能被别人打断)

Redis 的 Red Lock 是什么?你了解吗?

大白话理解:

RedLock (Redlock Algorithm) 是 Redis 作者 Antirez 提出的一种更高级、更严格的分布式锁算法,主要是为了解决单点 Redis 分布式锁在宕机时可能出现的安全性问题

  • 单点 Redis 的问题:如果你只有一个 Redis 实例作为锁服务,当这个 Redis 实例突然宕机了,并且它的 RDB 持久化还没来得及保存锁信息,那么在它重启后,之前的所有锁都可能丢失。这样,多个客户端就可能同时获得锁,导致数据不一致。
  • RedLock 的目标:即使在 Redis 节点部分宕机的情况下,也能保证锁的安全性(同一时刻只有一个客户端持有锁)。

RedLock 的核心思想:

RedLock 不依赖于单个 Redis 实例,而是依赖于 N 个独立的 Redis Master 节点(通常是奇数,如 5 个)。客户端需要从大多数(N/2 + 1 个) Redis 实例上成功获取锁,才算真正获取到锁。

RedLock 的加锁过程:

  1. 获取时间戳:客户端获取当前系统时间 T1(毫秒)。
  2. 逐个尝试加锁:客户端尝试按顺序在 N 个独立的 Redis Master 节点上获取锁。
    • 使用的命令依然是 SET key value NX PX timeout
    • 这里的 value 也是一个唯一标识符。
    • timeout 是锁的过期时间(例如 10 秒)。
    • 每次尝试加锁时,都设置一个小的超时时间,避免某个节点卡住。
  3. 计算加锁耗时:客户端获取所有锁花费的时间 T2(毫秒)。
  4. 判断是否成功:
    • 如果客户端成功从大多数(N/2 + 1 个)节点获取到锁。
    • 并且,获取所有锁的总耗时 (T2 - T1) 小于锁的有效时间timeout)。
    • 那么,客户端才认为自己成功获取了分布式锁
  5. 失败处理:如果未能从大多数节点获取锁,或者总耗时超过了锁的有效时间,客户端会立即向所有已经获取到锁的 Redis 实例发送 DEL 命令,释放这些锁。

RedLock 的释放锁过程:

客户端向所有 N 个 Redis 实例都发送 DEL 命令,无论是否在上面成功获取了锁。这是因为,即使某个实例没有成功加锁,也可能是网络问题导致,为了确保万无一失。

Redis 实现分布式锁时可能遇到的问题有哪些?

虽然 Redis 实现分布式锁非常方便,但也伴随着一些坑,需要注意:

  1. 死锁 (Deadlock):

    • 问题:客户端获取锁后,如果在业务逻辑执行过程中崩溃了,没有来得及释放锁,那么这个锁就会一直存在,导致其他客户端永远无法获取锁。
    • 解决方案:
      • 设置过期时间 (TTL/EX/PX):这是必须的。在加锁时就给锁设置一个过期时间,即使客户端崩溃,锁也会在一段时间后自动释放。
      • 续期机制 (Redisson):如果业务逻辑执行时间不确定或可能超过锁的过期时间,可以引入一个续期线程。在锁即将过期时,续期线程会自动为锁续期,直到业务逻辑执行完毕。
  2. 锁误删 (Misdeletion):

    • 问题:客户端 A 获取了锁,但由于网络延迟或业务执行时间过长,锁过期了。这时客户端 B 获取了同一个锁。随后客户端 A 的业务逻辑执行完毕,去释放锁,结果删除了客户端 B 持有的锁。
    • 解决方案:
      • 使用唯一标识符:在加锁时,为锁设置一个随机且唯一的 value(比如 UUID)。在释放锁时,先检查锁的 value 是否与自己设置的 value 一致,只有一致才删除。
      • 使用 Lua 脚本保证原子性:“检查 value”和“删除锁”这两个操作必须是原子性的,否则依然可能出现误删。使用 Lua 脚本可以将这两个操作捆绑在一起,确保原子执行。
  3. 非原子操作导致的问题:

    • 问题:如果加锁或解锁操作不是原子性的(例如,SETNXEXPIRE 分开执行),在执行过程中 Redis 宕机,可能导致锁没有过期时间,变成“永生锁”,从而引发死锁。
    • 解决方案:
      • 使用 SET key value NX PX milliseconds:这是 Redis 2.6.12 及以后版本提供的原子性设置锁和过期时间的命令,强烈推荐使用
      • 使用 Lua 脚本:对于更复杂的原子操作(如释放锁时的判断和删除),使用 Lua 脚本。
  4. 锁的粒度问题:

    • 问题:锁的粒度过大,会导致并发度降低;锁的粒度过小,会增加锁的开销和管理的复杂性。
    • 解决方案:根据业务需求,合理设计锁的 key,精确锁定需要保护的资源。例如,扣减库存,锁的 key 可以是 product:stock:item_id
  5. Redis 实例宕机导致锁丢失 (单点故障):

    • 问题:如果你只用一个 Redis 实例做分布式锁,当这个实例突然宕机,并且它的数据还没来得及持久化,那么所有未释放的锁都会丢失。重启后,多个客户端可能会同时获取到锁,导致数据不一致。
    • 解决方案:
      • Redis Sentinel (哨兵模式):主从切换后,新的主节点仍然能提供服务,但主从切换期间可能出现锁丢失(因为旧主节点上的锁可能还没同步到新主节点,或者同步过去后,新主节点成为主节点时,如果旧主节点又恢复了,会出现双主)。
      • Redis Cluster (集群模式):提高了可用性和扩展性,但跨槽位的分布式锁实现更复杂。
      • RedLock 算法:专门设计来解决多 Redis 实例下的安全性问题,但增加了复杂性和存在一些争议。
  6. 时钟跳跃问题:

    • 问题:如果 Redis 服务器的系统时间突然跳变(比如 NTP 同步导致时间回拨),可能导致锁的过期时间计算错误,提前释放或延后释放。
    • 解决方案:在 RedLock 算法中会考虑这个问题,但在单点 Redis 锁中,一般依赖 NTP 同步的稳定性,或者在业务层面增加额外的校验。

总的来说:对于大多数业务场景,基于 SET key value NX PX 和 Lua 脚本的单点 Redis 分布式锁,配合过期时间、唯一标识符和续期机制(如果需要),已经能够满足需求。如果对数据一致性有极高要求,且能接受更高复杂度,可以考虑 RedLock 或其他更成熟的分布式锁框架(如 Zookeeper 实现的 Curator 锁)。

Redis 的持久化机制有哪些?

Redis 是一个内存数据库,数据都存在内存里,速度快。但内存有个缺点:服务器一关机,数据就没了。所以,为了防止数据丢失,Redis 需要把内存里的数据“备份”到硬盘上,这个过程就叫持久化。Redis 有两种主要的备份方式:

方式一:RDB (Redis Database) 快照

  • 大白话:就像给你的数据拍个“快照”或“照片”。在某个时间点,把 Redis 内存里的所有数据,一下子完整地保存到一个文件里(.rdb 文件)。
  • 如何触发:
    • 手动触发:
      • SAVE 命令:阻塞 Redis 主进程,直到保存完成。生产环境慎用,因为它会阻塞所有客户端请求。
      • BGSAVE 命令:后台异步保存。Redis 会 fork 一个子进程去执行保存操作,主进程可以继续处理请求。这是常用的手动方式。
    • 自动触发:在 redis.conf 配置中设置策略,例如:
      • save 900 1 (900 秒内至少有 1 次改动就保存)
      • save 300 10 (300 秒内至少有 10 次改动就保存)
      • save 60 10000 (60 秒内至少有 10000 次改动就保存)
      • 当达到任一条件时,Redis 会自动执行 BGSAVE
  • 恢复数据:启动 Redis 时,会自动加载最新的 .rdb 文件。
  • 优点:
    • RDB 文件紧凑,适合备份:文件小,方便传输,适合做数据备份和灾难恢复。
    • 恢复速度快:直接加载 .rdb 文件,恢复数据效率高。
    • 对性能影响小BGSAVE 是子进程操作,对主进程影响较小。
  • 缺点:
    • 可能丢失数据:RDB 是周期性快照,如果在两次快照之间 Redis 宕机,这期间的数据就会丢失。数据越重要,丢失的风险越高。
    • Fork 子进程的开销:在数据量大时,fork 子进程会消耗一定的系统资源,并可能导致短暂的阻塞。

方式二:AOF (Append Only File) 只追加文件

  • 大白话:就像写日志。你对 Redis 做的每一个“写”操作,Redis 都会把这个操作命令原封不动地记录到一个日志文件里(appendonly.aof)。
  • 如何触发:在 redis.conf 中开启 appendonly yes
  • 写入策略:AOF 提供了三种写入磁盘的策略,通过 appendfsync 配置:
    • always:每个写命令都立即同步到磁盘。最安全,但性能最低。
    • everysec:每秒同步一次。推荐,兼顾安全性和性能。 最多丢失 1 秒数据。
    • no:由操作系统决定何时同步。性能最高,但最不安全。
  • 恢复数据:启动 Redis 时,会重新执行 AOF 文件中的所有命令,来重建内存数据。
  • AOF 重写 (Rewrite):
    • 大白话:随着日志文件越来越大,会有很多重复或无效的命令(比如先 SET A 1,再 SET A 2,再 DEL A)。AOF 重写就是把这些冗余命令压缩,生成一个新的、更小的 AOF 文件。
    • 原理:Redis 会 fork 一个子进程,把当前内存中的数据,转换成一系列简化的命令写入新的 AOF 文件,完成后替换旧的 AOF 文件。
    • 触发:可以手动 BGREWRITEAOF,也可以配置自动触发(例如 auto-aof-rewrite-percentage 100auto-aof-rewrite-min-size 64mb)。
  • 优点:
    • 数据丢失少:根据 appendfsync 策略,可以做到最多丢失 1 秒数据(everysec)。
    • AOF 文件可读性好:都是 Redis 命令,方便调试和恢复。
  • 缺点:
    • AOF 文件通常比 RDB 大:记录的是命令而不是压缩后的数据。
    • 恢复速度相对慢:需要重新执行所有命令,在大文件时可能耗时。

混合持久化 (Redis 4.0+)

  • 大白话:结合了 RDB 和 AOF 的优点。
  • 原理:在 AOF 重写时,不再是把所有内存数据转换成命令,而是先以 RDB 格式写入 AOF 文件的开头,然后再将重写期间发生的增量命令以 AOF 格式追加到 RDB 数据的后面
  • 恢复数据:Redis 启动时,先加载 AOF 文件中的 RDB 部分,然后追加执行 AOF 部分的命令。
  • 优点:
    • 兼顾启动速度和数据安全性:RDB 部分快速加载,AOF 部分保证数据最新。
    • 文件大小适中。
  • 推荐:生产环境通常会同时开启 AOF 和 RDB,并使用 AOF 的 everysec 策略,并开启混合持久化。
Redis 主从复制的实现原理是什么?

主从复制就像是“老师和学生”的关系。一台 Redis 服务器(Master,老师)负责处理所有写请求,并把它的数据变化实时同步给其他多台 Redis 服务器(Slave,学生)。学生们只负责读请求。这样做的目的是为了:

  • 读写分离:老师负责写,学生负责读,分担压力。
  • 高可用:万一老师累倒了(Master 宕机),可以快速从学生中选一个出来当新老师,保证服务不中断。
  • 数据备份:学生们手里都有老师的完整“笔记”,相当于多了一份备份。

实现原理步骤:

  1. 建立连接:

    • 当一个 Slave 启动时,或者在配置中指定了 Master 地址 (replicaof masterip masterport),Slave 会向 Master 发送 PSYNC 命令请求同步。
    • 大白话:学生刚进教室,就跟老师说:“老师,我要抄你的笔记!”
  2. 全量复制 (Full Resynchronization):

    • 如果 Slave 是第一次连接 Master,或者 Master 发现 Slave 的数据版本和自己差异太大,就会进行全量复制。
    • Master 过程:
      1. 执行 BGSAVE 命令,生成当前的 RDB 快照文件。
      2. 在生成 RDB 期间,Master 会把这期间的所有写命令都缓存起来。
      3. 将生成的 RDB 文件发送给 Slave。
    • Slave 过程:
      1. 接收 Master 发送的 RDB 文件。
      2. 清空自己原有的所有数据。
      3. 加载 RDB 文件到内存中。
      4. 接收 Master 缓存的那些增量写命令,并在内存中执行这些命令。
    • 大白话:老师说:“好,你先拿我最新的完整笔记(RDB 文件)去抄,抄完后,我再把你在抄笔记期间我新讲的知识点(增量命令)告诉你。”
  3. 增量复制 (Partial Resynchronization):

    • 全量复制完成后,Master 和 Slave 之间会保持一个长连接。
    • Master 会将所有后续发生的写命令实时地发送给所有连接的 Slave。
    • Slave 接收到命令后,立即在自己的内存中执行这些命令,保持与 Master 的数据一致。
    • 复制偏移量复制积压缓冲区:Master 和 Slave 都会维护一个复制偏移量,记录已经复制的数据量。Master 还会维护一个环形缓冲区,存储最近执行的命令。如果 Slave 短暂断开连接后又重新连接,它会告诉 Master 自己已经复制到哪个偏移量了,Master 就会从复制积压缓冲区中找到缺失的命令发送给 Slave,实现断点续传,避免不必要的全量复制。
    • 大白话:抄完完整笔记后,学生和老师就保持实时同步了。老师每讲一个新知识点,就立刻写给学生,学生立马抄到自己的笔记上。即使学生中间打了个盹,醒来后也能快速补上漏掉的部分。

Master 宕机后的处理:

  • 如果 Master 宕机,Redis Sentinel (哨兵) 或 Redis Cluster (集群) 可以自动进行主从切换(故障转移),从现有的 Slave 中选举一个新的 Master,保证服务的高可用。
Redis 数据过期后的删除策略是什么?

Redis 里的数据可以设置一个“保质期”(过期时间)。数据到了保质期后,就应该被删除。但 Redis 不是一到时间就立马删除,它有自己的“删除策略”,因为如果所有数据都到了时间就立即删除,会很耗费 CPU 资源。

Redis 主要采用两种策略配合使用:

策略一:惰性删除 (Lazy Deletion)

  • 大白话:就像一个很懒的清洁工。他不主动去检查哪里脏了,只有当有人来问他“这个地方干净吗?”时,他才去看看,如果发现脏了,就顺手清理掉。
  • 原理:
    • 当客户端(你)尝试访问(GETHGET 等命令)某个带有过期时间的 Key 时。
    • Redis 会在返回数据之前,先检查这个 Key 是否已经过期。
    • 如果过期了,Redis 会立即删除这个 Key,并返回空。
    • 如果没过期,就正常返回数据。
  • 优点:
    • 对 CPU 最友好:只有访问时才进行删除,不占用额外的 CPU 资源去检查过期 Key。
  • 缺点:
    • 内存浪费:如果某个 Key 已经过期了,但一直没有被访问,它就会一直存在内存中,直到被动触发删除,造成内存浪费。

策略二:定期删除 (Active Deletion)

  • 大白话:就像一个有固定工作时间的清洁工。他每隔一段时间(比如每分钟),就会去一些随机的地方转一圈,看看有没有过期的垃圾,发现了就清理掉。
  • 原理:
    • Redis 内部有一个定时任务,默认每 100 毫秒(每秒 10 次)运行一次。
    • 每次运行,它会做以下事情:
      1. 从设置了过期时间的 Key 集合中,随机抽取一部分 Key 进行检查。
      2. 删除其中所有已经过期的 Key。
      3. 如果删除的 Key 数量超过了某个比例(例如 25%),会重复这个过程,直到低于这个比例,或者达到一定的 CPU 时间限制(例如 25 毫秒)。
  • 优点:
    • 内存回收及时:相较于惰性删除,可以更及时地回收一部分过期 Key 占用的内存。
    • CPU 开销可控:每次只检查一部分 Key,并有时间限制,避免长时间占用 CPU。
  • 缺点:
    • 仍有内存浪费:毕竟是随机抽样,有些过期 Key 可能长时间没有被抽到,仍然会占用内存。
    • 无法保证所有过期 Key 都被删除。

额外补充:内存淘汰策略 (Eviction Policy)

  • 大白话:当 Redis 内存快满了,而又没有足够多的过期 Key 可供删除时,Redis 就会采取“壮士断腕”的方式,按照一定的策略,主动删除一些没有过期时间或者还没到过期时间的 Key,以腾出空间。
  • 触发条件:当 Redis 的内存使用达到 maxmemory 限制,并且有新的数据写入时。
  • 常见策略:
    • noeviction:不删除,新写入会报错。
    • volatile-lru:从设置了过期时间的 Key 中,淘汰最近最少使用的。
    • allkeys-lru:从所有 Key 中,淘汰最近最少使用的(不管有没有设置过期时间)。
    • volatile-ttl:从设置了过期时间的 Key 中,淘汰即将过期的。
    • allkeys-random:从所有 Key 中,随机淘汰。等等...
  • 作用:内存淘汰策略是 Redis 的“最后防线”,确保 Redis 在内存不足时依然可以运行,不至于崩溃。

总结:Redis 主要通过惰性删除(访问时删除)和定期删除(随机抽样删除)来管理过期 Key。这两种策略权衡了 CPU 资源和内存回收效率。当内存真正吃紧时,才会启动内存淘汰策略,按照配置好的规则删除 Key 来腾出空间。

如何解决 Redis 中的热点 key 问题?

热点 Key 就像一个“明星商品”或者“爆款新闻”,在短时间内有大量并发请求去访问它。在 Redis 中,这会导致:

  1. CPU 飙升:Redis 处理这个 Key 的请求量太大,CPU 使用率过高。
  2. 网络拥塞:针对这个 Key 的请求和响应数据量太大,占用大量网络带宽。
  3. 单点压力:如果你的 Redis 是集群,所有请求都集中到存储这个热点 Key 的那个节点上,导致该节点压力过大。

这些都可能导致 Redis 服务响应变慢,甚至短暂宕机。

解决热点 Key 的方案:

解决热点 Key 的核心思想就是:分散压力减少直接访问

  1. 数据预热与本地缓存 (Local Cache)

    • 大白话:如果知道哪些 Key 会变成热点(比如秒杀商品、实时榜单),提前把它们放到应用服务器的内存里(本地缓存),或者专门用一个更靠近用户的缓存层。
    • 实现:
      • 服务启动时或定时任务将热点数据加载到本地内存(Guava Cache、Caffeine 等)。
      • 请求优先从本地缓存获取,本地缓存未命中或过期才去 Redis。
    • 优点:减少对 Redis 的直接访问,减轻 Redis 压力,响应速度快。
    • 缺点:存在数据一致性问题(本地缓存过期策略、数据更新的通知机制)。
  2. 热点 Key 分散存储 (Hash Tag)

    • 大白话:如果你的 Redis 是集群,一个 Key 只会存储在一个节点上。热点 Key 来了,这个节点就累死了。我们可以把一个热点 Key“复制”成多个,分散到不同的 Redis 节点上。
    • 实现:
      • 对热点 Key 进行二次散列,或在 Key 名后面加上后缀或随机数。
      • 例如:原来 Key 是 product:100,现在可以变成 product:100:#1product:100:#2 ... product:100:#N
      • 客户端在访问时,随机选择一个后缀去访问。
    • 优点:将热点请求分散到多个 Redis 节点,减轻单点压力。
    • 缺点:增加了数据的冗余,写入时需要更新多个 Key,并且可能需要额外的逻辑来保证多个副本之间的一致性(例如通过异步任务或消息队列更新所有副本)。
  3. 加锁与队列限流 (针对写操作)

    • 大白话:如果热点 Key 的问题主要发生在写操作上(比如热门商品的库存扣减),可以控制同一时间只有一个请求能操作,其他请求排队等待。
    • 实现:
      • 使用分布式锁(如 Redis 实现的分布式锁)来保证对热点 Key 的写操作的串行化。
      • 使用消息队列对请求进行削峰填谷,将大量并发写请求异步化处理。
    • 优点:保证数据一致性,防止超卖。
    • 缺点:降低了并发度,增加了延迟。
  4. 数据分级缓存 (多级缓存)

    • 大白话:把数据存在多个层级,越靠近用户的地方越快。比如 CDN → 本地缓存 → Redis → 数据库。
    • 实现:结合 CDN 缓存静态资源、应用服务本地缓存热点数据、Redis 作为分布式缓存、数据库作为最终存储。
    • 优点:充分利用各层优势,层层拦截请求,减轻后端压力。
    • 缺点:增加了系统复杂性,数据一致性管理更复杂。
Redis 集群的实现原理是什么?

单机 Redis 性能再强,也有内存和并发的上限。Redis 集群就是把多个 Redis 实例组织起来,让它们像一个整体一样工作,共同存储海量数据,并处理巨大的并发请求。

核心目标:

  1. 数据分片 (Sharding):将数据分散存储到不同的 Redis 节点上,突破单机内存限制。
  2. 高可用 (High Availability):即使部分节点宕机,整个集群仍然能对外提供服务。
  3. 可扩展性 (Scalability):可以方便地增加或减少节点,来扩展或缩减集群容量。

实现原理:

Redis Cluster 采用去中心化的架构,每个节点都保存整个集群的状态信息。

  1. 数据分片:槽 (Slot)

    • 大白话:Redis 集群把整个数据空间分成了 16384 个“小格子”(哈希槽)。每个 Key 在存储时,会根据它的哈希值计算出它属于哪个槽。每个 Redis 节点负责管理其中的一部分槽。
    • 原理:
      • Redis Cluster 有 16384 个哈希槽,编号从 0 到 16383。
      • 每个 Key 都会通过 CRC16 算法计算哈希值,然后 hash_value % 16384 得到该 Key 所属的槽位。
      • 每个 Master 节点负责管理一部分槽(例如,节点 A 负责 0-5000 槽,节点 B 负责 5001-10000 槽)。
    • 优点
      • 数据均匀分布:理论上 Key 会比较均匀地分布在不同节点。
      • 扩容/缩容方便:节点增减时,只需要在节点之间移动槽位即可,而不是移动大量实际数据。
    • Hash Tag (哈希标签):如果你想让某些 Key 强制落在同一个槽中(例如,一个用户的所有数据),可以在 Key 的一部分用 {} 包裹起来,Redis 只对 {} 中的内容计算哈希值。例如 user:{123}:nameuser:{123}:age 都会落在同一个槽中。
  2. 客户端路由:

    • 大白话:客户端第一次连接集群时,会从任意一个节点获取整个集群的槽位分布信息(哪个槽在哪个节点上)。之后,当客户端要操作某个 Key 时,它会自己计算 Key 属于哪个槽,然后直接连接到负责那个槽的节点上进行操作。
    • 重定向:如果客户端请求的 Key 不在该节点上,该节点会返回一个 MOVEDASK 错误,告诉客户端正确的节点地址,客户端会重定向到正确的节点。
  3. 高可用:主从复制 + 故障转移

    • 大白话:每个 Master 节点(负责一部分槽)都可以有自己的 Slave 节点。当 Master 宕机时,它的一个 Slave 会被选举出来接替 Master 的位置,继续提供服务。
    • 原理:
      • 每个 Master 节点至少有一个或多个 Slave 节点作为其备份。
      • 节点之间通过Gossip 协议(流言协议)互相通信,交换集群的状态信息(比如哪些节点存活、哪些节点宕机)。
      • 当某个 Master 节点宕机时,集群中的其他节点会感知到。
      • 通过投票机制,从该宕机 Master 的 Slave 节点中选举出一个新的 Master 来接管其槽位。
  4. 节点通信:

    • 大白话:集群中的所有节点都会互相“聊天”,交流集群健康状况、槽位信息等。
    • 原理:每个节点都会定期向其他节点发送消息,包括 PING/PONG 消息,以检测节点是否存活。

集群的优点:

  • 高性能:所有操作都是直接连接到目标节点,没有代理层开销。
  • 高可用:部分节点故障不影响整个集群服务。
  • 可扩展:方便地增加或减少节点来扩容或缩容。

集群的缺点:

  • 部分复杂性:集群部署和管理相对复杂。
  • 跨槽位操作限制:事务和多 Key 操作通常需要 Key 落在同一个槽位,否则无法直接执行。
Redis 中的 Big Key 问题是什么?如何解决?

Big Key (大 Key) 指的是 Redis 中存储的 Key 对应的值非常大。这个“大”可以是:

  1. 占用内存大:一个 String 类型的 Key 存储了 10MB 的文本。一个 Hash/List/Set/ZSet 存储了百万个元素。
  2. 元素数量多:一个 List 存储了千万条记录,虽然每条记录不大,但总数非常多。

Big Key 会带来什么问题?

  1. 网络阻塞:当你获取或删除一个 Big Key 时,Redis 需要在短时间内传输大量数据到客户端或从客户端接收大量数据,这会占用大量的网络带宽,影响其他请求。
  2. Redis 性能下降:
    • 读写阻塞:对 Big Key 进行操作(如 GETDELHGETALLLRANGE 大范围查询)时,需要消耗大量 CPU 和内存,导致 Redis 主线程阻塞,无法响应其他请求,造成“卡顿”。
    • 内存碎片:Big Key 的删除和更新可能导致内存碎片,影响 Redis 性能和内存利用率。
  3. 集群扩容困难:在 Redis Cluster 中,如果 Big Key 所在的槽需要迁移到其他节点,迁移过程会非常耗时,阻塞集群其他操作。
  4. AOF 重写/RDB 持久化压力:Big Key 会导致 AOF 文件或 RDB 文件变得非常大,持久化操作耗时增加。

如何发现 Big Key?

  1. 使用 redis-cli --bigkeys 命令:可以扫描整个 Redis 实例,找出各种数据类型中占用内存最大的 Key。
    • redis-cli -h 127.0.0.1 -p 6379 --bigkeys
  2. 使用 MEMORY USAGE key 命令:针对特定 Key 查看其内存占用。
  3. 慢查询日志:观察 Redis 的慢查询日志,看是否有针对特定 Key 的操作耗时过长。
  4. 业务层面:结合业务场景,预判可能出现 Big Key 的地方。

如何解决 Big Key 问题?

解决 Big Key 的核心思想就是:“化整为零”,把大 Key 拆分成小 Key,或者分批处理大 Key。

  1. 拆分 Key:

    • 大 Key 拆分成小 Key:将一个大 String 拆分成多个小 String;将一个大 Hash/List/Set/ZSet 拆分成多个小 Hash/List/Set/ZSet。
    • 示例:
      • user_feed:userid (List 存储用户所有动态) 拆分为 user_feed:userid:page1, user_feed:userid:page2 等多个小 List。
      • 将一个大 Hash big_object:id 拆分为多个小 Hash:big_object:id:field1, big_object:id:field2
    • 优点:读写操作更高效,减轻单点压力。
    • 缺点:增加了 Key 的数量,管理稍微复杂。
  2. 使用合理的数据结构:

    • 对于一些计数、统计场景,考虑使用 HyperLogLog 或 Bitmap,它们可以以非常小的内存占用处理大量数据。
  3. Big Key 的删除 (非阻塞删除):

    • 大白话:如果一个 Big Key 必须删除,不要直接用 DEL 命令,那样会阻塞 Redis。让 Redis 在后台慢慢删。
    • 实现:使用 Redis 4.0+ 提供的异步删除命令
      • UNLINK key [key ...]:这个命令是非阻塞的,它会把 Key 的实际删除操作放到后台线程中异步执行,主线程可以立即返回并处理其他请求。
      • FLUSHALL ASYNC / FLUSHDB ASYNC:同样是异步清理所有 Key。
    • 优点:避免删除 Big Key 时阻塞主线程。
  4. 限制集合类型的大小:

    • 在业务层面或代码层面,对 Hash、List、Set、ZSet 等集合类型的大小进行强制限制,例如,一个 List 最多存储 10000 条记录,超过就分片或丢弃旧数据。
  5. 定期清理:

    • 对不再需要的 Key 设置合理的过期时间,让 Redis 自动清理。

总结:发现 Big Key 是第一步,核心是将其拆小,并采用异步非阻塞的方式进行删除,以及在设计阶段就避免 Big Key 的产生。

贡献者

The avatar of contributor named as LI SIR LI SIR

页面历史