抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

这里是Redis相关的八股

Redis默认端口6379

1.基础

1.Redis为什么快?

  • 内存操作:完全基于内存,绝大部分请求是纯粹的内存操作,非常快速

  • I/O多路复用模型:采用了 I/O 多路复用机制处理大量的客户端 Socket 请求,其中的 IO 多路复用技术可以在只有一个线程的情况下,同时监听成千上万个客户端连接,解决传统 IO 模型中每个连接都需要一个独立线程带来的性能开销。

2.Redis有哪些数据类型?常用的使用场景

  1. String:存储单个值,适用于缓存和键值存储,常用命令:SET用于设置值,GET用于获取值。
    • 分布式锁、分布式Session、值缓存、对象
  2. List:有序、可重复的字符串集合,支持从头部或尾部插入/删除元素, 适用于消息队列和发布/订阅系统,常用命令:LPUSH用于从列表左侧添加元素,LRANGE用于获取指定范围的元素。
    • 分布式Duque、消息队列、Push式信息流
  3. Set:无序、不可重复的字符串集合,适用于标签系统和好友关系等,常用命令:SADD用于向集合添加成员,SMEMBERS用于获取集合所有成员。
    • 聚合计算(并集、交集、差集)场景,比如点赞、共同关注、抽奖活动等。
  4. Hash:包含键值对的无序散列表,适用于存储对象、缓存和计数器,常用命令:HSET用于设置字段值,HGETALL用于获取散列的所有字段和值。
    • 对象存储
  5. Zset:也就是Sorted Set,有序的字符串集合,每个成员关联一个分数,适用于排行榜和按分数范围获取成员,常用命令:ZADD用于添加成员及其分数,ZRANGE用于获取指定范围的成员,
    • 排序场景,比如排行榜、电话和姓名排序等。

3.String还是Hash存储对象更好呢?

性能

  • String:适合存储和读取大对象,因为它是整体操作,性能较高。
  • Hash:适合操作大量小字段,可以只处理需要的字段,减少不必要的数据传输。

内存

  • String:存储大对象时内存开销可能较大,尤其是对象频繁序列化和反序列化。
  • Hash:在存储大量小对象时更节省内存,因为字段共享同一个键名。

操作需求

  • String:如果对象不可变,或者总是整体读写,String更简单。
  • Hash:如果需要频繁访问或修改对象的某个字段,Hash更合适。

访问粒度上: String 是用一整块内存来存储数据,要进行操作的话都只能全部读取,而 Hash 则可以用基本操作实现单个数据的读取。

更新频率上:频繁改动对象中的少量字段,Hash 类型更好;如果是整体读取而很少写入,String 类型更好。

内存利用上:小字段用 Hash 更方便,大字段整体存取上用 String 更方便。

3.Redis可以用来做什么?

  • 缓存
  • 排行榜
  • 分布式计数器
  • 分布式锁
  • 消息队列
  • 延时队列
  • 分布式 token
  • 限流

2.持久化

什么是持久化?

大部分原因是为了之后重用数据(比如重启机器、机器故障之后恢复数据),或者是为了做数据同步(比如 Redis 集群的主从节点通过 RDB 文件同步数据)。 Redis 提供两种持久化机制 RDB(默认) 和 AOF 机制:

1.RDB

RDB:是缩写快照

RDB(Redis DataBase)是Redis默认的持久化方式。将某一时刻的内存数据,以二进制的方式写入磁盘; 对应产生的数据文件为dump.rdb。通过配置文件中的save参数来定义快照的周期。

因为 AOF 日志记录的是操作命令,不是实际的数据,所以用 AOF 方法做故障恢复时 ,需要全量把日志都执行一遍,一旦 AOF 日志非常多,势必会造成 Redis 的恢复操作缓慢。 为了解决这个问题,Redis 增加了 RDB 快照。 RDB 快照就是记录某一个瞬间的内存数据,记录的是实际数据

Redis 提供了两个命令来生成 RDB 快照文件:

  • save:同步保存操作,会阻塞 Redis 主进程;
  • bgsave:fork 出一个子进程,子进程执行,不会阻塞 Redis 主线程,默认选项。

2.RDB写时复制(Copy-on-Write)

假如RDB持久化过程中数据发生了改变怎么办?

在 RDB 快照生成过程中,如果数据发生了改变,Redis 的处理机制可以保证快照的一致性,同时不会丢失后续的修改。具体来说,Redis 使用了一种叫做 Copy-on-Write(写时复制) 的技术来解决这个问题。

先回顾快照的生成过程:

  • 当 Redis 触发 RDB 持久化时(比如通过 SAVE 或 BGSAVE 命令),它会创建一个子进程。
  • 子进程负责将内存中的数据写入磁盘,生成 .rdb 文件。
  • 主进程继续处理客户端的请求(比如读写操作)。

Copy-on-Write 机制

  • 在子进程生成快照时,它会基于某个时间点(触发快照的瞬间)的内存数据进行操作。
  • 如果主进程在这期间修改了数据,操作系统会利用 写时复制
    • 被修改的数据会被复制一份,主进程在新副本上操作。
    • 子进程仍然使用原始数据(未修改时的内存快照)生成快照。
  • 这样,主进程的修改不会影响子进程正在生成的快照,快照仍然是触发时刻的一致性数据。

数据丢失风险:快照生成后到下一次快照之间的修改,如果没来得及保存(比如 Redis 崩溃),会丢失。

3.AOF

AOF持久化(即Append Only File),Redis 在执行完一条写操作命令后,就会把该命令以追加的方式写入到一个文件里, 然后 Redis 重启时,会读取该文件记录的命令,然后逐一执行命令的方式来进行数据恢复。

AOF为什么是在执行完写命令才将该命令记录到AOF日志?

  1. 避免额外的检查开销,AOF 记录日志不会对命令进行语法检查;
  2. 在命令执行完之后再记录,不会阻塞当前的命令执行。

潜在风险

  1. 如果刚执行完命令 Redis 就宕机会导致对应的修改丢失;
  2. 可能会阻塞后续其他命令的执行(AOF 记录日志是在 Redis 主线程中进行的,也就是说这两个操作是同步的,如果在将日志内容写入到硬盘时,服务器的硬盘的 I/O 压力太大,就会导致写硬盘的速度很慢, 进而阻塞住了,也就会导致后续的命令无法执行。

AOF一条

  • 优点:首先,AOF提供了更好的数据安全性,因为它默认每接收到一个写命令就会追加到文件末尾。 即使Redis服务器宕机,也只会丢失最后一次写入前的数据。
  • 缺点:因为记录了每一个写操作,所以AOF文件通常比RDB文件更大,消耗更多的磁盘空间

总结:RDB是Redis的快照持久化方式,通过周期性的快照将数据保存到硬盘,占用更少的磁盘空间和 CPU资源,适用于数据备份和恢复,但可能存在数据丢失的风险。AOF 是追加日志持久化方式,将每个写操作以追加的方式记录到日志文件中,确保了更高的数据完整性和持久性,但相对于RDB 消耗更多的磁盘空间和写入性能,适用于数据持久化和灾难恢复,且可以通过配置实现不同的同步频率。

4.AOF三种写回策略

Redis 写入 AOF 日志的过程,如下图:

  1. Redis 执行完写操作命令后,会将命令追加到 server.aof_buf 缓冲区;
  2. 然后通过 write() 系统调用,将 aof_buf 缓冲区的数据写入到 AOF 文件, 此时数据并没有写入到硬盘,而是拷贝到了内核缓冲区 page cache, 等待内核将数据写入硬盘;
  3. 具体内核缓冲区的数据什么时候写入到硬盘,由内核决定。

Redis 提供了 3 种写回硬盘的策略,控制的就是上面说的第三步的过程。

  1. always理论上不会丢失任何数据。每个写命令都会同步刷盘后再返回给客户端。这是最安全的方式,但性能开销巨大,会严重降低 Redis 的吞吐量,通常不推荐在生产环境使用。
  2. everysec可能丢失最多1秒的数据。这是默认配置,也是性能和安全的良好折衷。后台线程每秒执行一次刷盘。如果刚好在刷盘间隔内(1秒钟)发生宕机,就会丢失这1秒内的写命令。
  3. no:可能丢失最多数据。写入操作由操作系统内核决定何时刷到磁盘,通常间隔长达30秒。如果 Redis 进程或服务器在这30秒内宕机,这期间的所有命令都会丢失。

从上图可以看出,数据丢失发生在命令写入内存缓冲区 (aof_buf) 后,到真正写入磁盘前的这个时间窗口内。如果发生宕机,这个缓冲区中的数据就会丢失。

真正的“零丢失”方案: 单机 Redis 无法保证绝对的数据不丢失。真正的生产环境高可用方案是:

  • **主从复制 (Replication)**:配置一个从节点 (replica),主节点的数据会异步复制到从节点。如果主节点宕机,可以手动或通过哨兵/集群模式切换到从节点。
  • 结合使用 AOF 和 RDB:你可以同时开启 RDB 和 AOF。RDB 用于做定时快照和快速恢复,AOF 用于保证数据完整性。在重启时,Redis 会优先使用 AOF 来恢复,因为它通常更完整。

5.AOF重写机制

问题: AOF 文件记录的是每个写命令,随着时间推移,文件会越来越大。例如,一个计数器被递增了100次,AOF 文件会记录100条 INCR 命令,但实际上只需要一条 SET key 100 命令就能恢复最终状态。

解决方案:AOF 重写 (Rewriting)
AOF 重写会创建一个新的、更紧凑的 AOF 文件来替换原有的庞大文件。新文件只包含恢复当前数据集所需的最小命令集合,它通过读取当前数据库中的键值对来实现,而不是分析旧的 AOF 文件。

“AOF 重写是 Redis 为了解决 AOF 文件体积膨胀问题而设计的机制。它的核心目的是创建一个新的、更小的 AOF 文件,这个文件包含了重建当前数据库状态所需的最少命令集合。”

它的工作原理和过程是这样的:

  1. 触发方式
    • 自动触发:通过在 redis.conf 中配置 auto-aof-rewrite-percentage(增长比例)和 auto-aof-rewrite-min-size(最小体积)来触发。
    • 手动触发:通过调用 BGREWRITEAOF 命令。
  2. 执行过程
    • 重写是通过 fork() 出一个子进程来完成的,这保证了主进程可以继续处理命令,不会阻塞。
    • 子进程遍历当前数据库中的所有数据,根据值的类型(字符串、列表、哈希等)将每个键值对转换并写入一条最精简的命令到新的临时 AOF 文件中。
    • 与此同时,主进程接收到的新写命令不仅会写入原有的 AOF 缓冲区,还会写入一个专门的 AOF 重写缓冲区。这样就能保证即使重写期间有新数据,也不会丢失。
  3. 收尾工作
    • 当子进程完成重写后,会向主进程发送一个信号。
    • 主进程收到信号后,会将 AOF 重写缓冲区 中的所有命令追加到新的临时 AOF 文件中,确保数据完整性。
    • 最后,主进程会原子性地用新的 AOF 文件替换掉旧的 AOF 文件。至此,重写过程完成。

总结一下,AOF 重写的优点在于:

  • 大幅减小磁盘占用量
  • 提升恢复速度(因为要回放的命令变少了)。
  • 整个过程是非阻塞的(除了 fork() 瞬间的延迟),主进程可以持续服务。

核心“子进程做快照,双缓冲区保数据”

面试可能遇到的其他问题:

  1. 重写过程中如果失败怎么办?
    • 如果重写失败(例如,子进程出错或磁盘已满),旧的 AOF 文件不会被替换,Redis 会继续使用旧的文件,并在日志中记录错误。整个过程对客户端是无感知的。
  2. fork() 操作会阻塞吗?
    • 虽然 fork() 操作本身是阻塞的,但由于 Redis 使用了操作系统的写时复制(Copy-on-Write, COW)技术,fork() 的执行时间通常非常快,除非数据集非常大。但如果机器内存不足或数据集巨大,fork() 延迟可能会成为一个问题。
  3. AOF 重写和 RDB 快照的 fork() 有什么区别?
    • 它们的 fork() 机制在原理上是一样的。都是通过创建子进程来利用 COW 机制,避免阻塞主进程。区别在于:
      • RDB:子进程将整个数据库的快照写入一个二进制文件(.rdb)。
      • AOF重写:子进程将数据库转换为重建所需的最小命令集,写入一个新的文本文件(.aof)。
  4. 和 RDB 相比,AOF 重写的优势?
    • AOF 重写 是一个增量式的整理过程,它不会像 RDB 快照那样产生一个全量备份。它生成的仍然是一个可追加的日志文件,保证了 AOF 本身的实时性和连续性。而 RDB 是某一时间点的全量快照。

6.如何选择

1.RDB

缺点:周期性保存快照,但如果下次快照前宕机,会丢失数据很多

优点:保存的是原始数据,恢复起来比AOF更快(因为AOF保存的是命令还要执行);文件小,适合做数据的备份,灾难恢复。

2.AOF

缺点:恢复慢

优点:数据更加完整(即使Redis服务器宕机,也只会丢失最后一次写入前的数据)

建议开启混合模式

当开启了混合持久化时,在 AOF 重写日志时, fork 出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件,然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF文件替换旧的的 AOF 文件。

也就是说,使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量数据, 后半部分是 AOF 格式的增量数据

这样的好处在于,重启 Redis 加载数据的时候, 由于前半部分是 RDB 内容,这样加载的时候速度会很快

加载完 RDB 的内容后,才会加载后半部分的 AOF 内容, 这里的内容是 Redis 后台子进程重写 AOF 期间,主线程处理的操作命令, 可以使得数据更少的丢失


3.Redis内存管理

1.过期键的删除策略有哪些

  • 定时过期(CPU不友好,内存友好):每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
  • 惰性过期(CPU友好,内存不友好):只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
  • 定期过期(前两种的折中):每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。

Redis key的过期时间和永久有效分别怎么设置

expire命令和persist命令

2.内存淘汰粗略有哪些

MySQL里有2000w数据,redis中只存20w的数据,如何保证redis中的数据都是热点数据?

redis内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。

全局的键空间选择性移除

  • noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
  • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。(这个是最常用的)
  • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。

设置过期时间的键空间选择性移除

  • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
  • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,挑选将要过期的数据淘汰。

4.缓存篇

1.缓存一致性方案

1、Cache Aside Pattern (旁路缓存模式,双写模式)

对于:从缓存中读取,如果读不到,就从数据库中读取,并把这个数据写入缓存

双写读

对于:先更新数据库,然后直接删除缓存

双写写

  1. 为什么不是采用更新缓存模式,而是采用删除缓存?

    答:可能会造成无效写操作。如果更新的缓存很多,但这期间并没有查询操作,就造成了无效的写操作

  2. 为什么先更新数据库,而不是先删除缓存,再更新数据库?

    答:因为数据不一致。写线程A删除了缓存,但还没来得及更新数据库,此时,读线程B未命中缓存,去数据库读取旧数据,并写入缓存返回了,然后线程A更新了数据库中的数据,此时就数据不一致了。在这个过程中,写线程更新数据库的时间都比读线程读数据库+写回Redis的时间长了,所以非常有可能发生数据不一致

    为什么先删缓存不行

  3. 采用先更新数据库,再删除缓存就没有问题了吗?

    答:也有可能会有问题,但概率会小的多!只有这种恰巧的情况:读线程读的时候缓存失效了,而且就在它访问完数据库之后,准备写回缓存之前,这是写线程一口气执行完了更新数据库和删缓存这两个操作,然后读线程把旧的数据写回了缓存。但这种可能性很低,因为一般来说更新数据库是非常耗时的。

  4. 问题3如何解决?

    答:延迟双删。在读线程把旧数据写回缓存后,然后写线程隔一小段时间再把这个缓存给删了,就是写线程要删除两次缓存。或者消息队列(TODO)

2、Read/Write Through Pattern(读写穿透,直写缓存模式 ,写穿)

在此模式下,所有写操作都会先更新缓存,然后再同步更新数据库。

对于读:从缓存中读取,如果读不到,从数据库中读取,然后写回cache后再返回(和双写模式一样

对于写:先查缓存,缓存中没有就直接更新数据库,缓存中有,先更新缓存,再接着更新数据库。

3、Write behind Pattern(异步缓存写入,异步写)

写操作只更新缓存,不立即同步到数据库,而是延迟批量更新数据库。

对于读:和双写模式一样

对于写:只更新缓存,然后异步的更新数据库,可以将更新数据库的任务交给消息队列或者线程池

Read/Write Through 是同步更新 cache 和 db,而 Write Behind 则是只更新缓存,不直接更新 db,而是改为异步批量的方式来更新 db。

很明显,这种方式对数据一致性带来了更大的挑战,比如 cache 数据可能还没异步更新 db 的话,cache 服务可能就就挂掉了。

这种策略在我们平时开发过程中也非常非常少见,但是不代表它的应用场景少,比如消息队列中消息的异步写入磁盘、MySQL 的 Innodb Buffer Pool 机制都用到了这种策略。

Write Behind Pattern 下 db 的写性能非常高,非常适合一些数据经常变化又对数据一致性要求没那么高的场景,比如浏览量、点赞量。

三种模式比较

三者读性能一样的,主要区别就是写性能

旁路缓存模式:高并发场景可能存在短暂数据不一致。适合读操作频繁、写操作较少且数据一致性要求高的场景,例如用户信息、商品详情查询等。

写穿模式:写入最慢,一致性最好。适合数据一致性要求较高的场景,例如金融交易系统。

异步写模式:写入最快,一致性最差。适合写操作频繁、对一致性要求不高且容忍一定延迟的场景,例如日志系统、计数统计等。

2.缓存穿透

缓存和数据库中都没有用户要访问的数据,当有大量这样的请求到来时,数据库的压力骤增 ,造成数据库短时间内承受大量请求而崩掉。

解决方案:

  • 非法请求的限制:当有大量恶意请求访问不存在的数据的时候,也会发生缓存穿透,因此在 API 入口处我们要判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在, 如果判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。
  • 缓存空值或者默认值:当我们线上业务发现缓存穿透的现象时,可以针对查询的数据, 在缓存中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值, 返回给应用,而不会继续查询数据库。 这种方式可以解决请求的 key 变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求 key,会导致 Redis 中缓存大量无效的 key 。很明显,这种方案并不能从根本上解决此问题。
  • 布隆过滤器:我们可以在写入数据库数据时,使用布隆过滤器做个标记, 然后在用户请求到来时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在, 如果不存在,就不用通过查询数据库来判断数据是否存在。即使发生了缓存穿透,大量请求只会查询 Redis 和布隆过滤器,而不会查询数据库,保证了数据库能正常运行

3.布隆过滤器

  1. 一、核心概念:它是什么?

    布隆过滤器本质上是一个很长的二进制向量(位数组)和一系列随机映射函数(哈希函数)。

    它的主要作用是:以极小的空间开销,快速判断一个元素“一定不存在”或“可能存在”于某个集合中。

    注意这两个关键词:

    • 一定不存在:这个判断是绝对准确的。
    • 可能存在:这个判断有一定的误判率

    二、工作原理(它是如何工作的?)

    布隆过滤器的操作分为两部分:添加元素查询元素

    1. 添加元素 (Add)

    当一个元素被加入过滤器时,会进行以下操作:

    1. 使用 k 个不同的哈希函数对这个元素进行计算,得到 k 个哈希值。
    2. 将这些哈希值映射到位数组的对应位置上,并将这些位置置为 1

    2. 查询元素 (Check)

    当要检查一个元素是否存在于过滤器中时:

    1. 同样使用那 k 个哈希函数对元素进行计算,得到 k 个哈希值。
    2. 检查位数组中这些对应的位置:
      • 如果其中有任何一个位置的值为 0,那么这个元素一定不存在于集合中。
      • 如果所有位置的值都是 1,那么这个元素很可能存在于集合中(但存在误判的可能)。

三、优缺点

优点:

  1. 空间效率极高:不需要存储元素本身,只需要一个位数组,远超其他数据结构。
  2. 查询时间极快:查询时间都是常数时间 O(k),与集合大小无关。
  3. 保密性强:因为它不存储原始数据,对数据有安全保护作用。

缺点:

  1. **存在误判率 (False Positive)**:这是最大的缺点。判断“存在”时可能是错的,但判断“不存在”一定是正确的。
  2. 无法删除元素:因为一个位可能被多个元素共享,直接将某位置 0 会导致其他元素被误删。(但可以通过计数布隆过滤器变种解决,用计数器代替位)
  3. 不支持扩容:一旦位数组长度初始化后,无法动态扩展。

你可以这样回答:

“布隆过滤器是一个使用位数组和多个哈希函数来实现的概率型数据结构。它的核心作用是高效地判断一个元素是否一定不存在或者可能存在一个集合里

它的优点是空间效率和查询时间都极高,但缺点是有误判率,并且无法删除元素

它的经典应用场景是解决缓存穿透问题。在请求到来时,先问布隆过滤器这个数据存不存在,如果过滤器说不存在,我们就直接返回,避免了无效的数据库查询,保护了后端资源。

另外,像爬虫URL去重、垃圾邮件过滤这些需要大规模数据判断但又可以容忍极小误差的场景,也非常适合用它。

需要注意的是,布隆过滤器的误判率可以通过调整位数组大小和哈希函数数量来控制,在实际使用中我们需要根据业务场景来权衡空间和误判率。”

4.缓存击穿

某个热点数据在缓存中过期时,大量并发请求直接穿透到数据库,导致数据库瞬时压力激增甚至崩溃。

解决方案:

  • 互斥锁方案(看情况):保证同一时间只有一个业务线程更新缓存,未能获取互斥锁的请求, 要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。

  • 永不过期(不推荐):缓存永不过期,但存储一个过期时间字段,异步更新。

  • 提前预热(推荐):在缓存过期前主动刷新热点数据。

    • 实现方式

      定时任务:通过 Quartz 或 Spring Scheduler 定期触发加载。

      事件驱动:监听数据变更消息(如 Binlog + Canal)自动更新缓存。

  • 二级缓存(Multi-Level Cache)

    核心思想:本地缓存(如 Caffeine) + 分布式缓存(Redis)分层拦截。

  • 限流降级(Rate Limiting)

    核心思想:当缓存失效时,限制数据库访问的并发量。

方案对比总结

方案 优点 缺点 适用场景
互斥锁 强一致性 锁竞争可能成瓶颈 数据变更少,重建成本高
逻辑过期 高吞吐,无阻塞 短暂数据不一致 允许最终一致的热点数据
缓存预热 避免突发流量 依赖预测准确性 可预知的热点(如大促商品)
二级缓存 减少Redis压力 本地缓存一致性难维护 高频访问的静态数据
限流降级 保护数据库 用户体验下降(降级) 突发流量且允许部分失败

5.缓存雪崩

大量缓存数据在同一时间过期(失效)或者 Redis 故障宕机时,如果此时有大量的用户请求, 都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增, 严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃,

解决方案:

针对 Redis 服务不可用的情况:

  1. Redis 集群:采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。Redis Cluster 和 Redis Sentinel 是两种最常用的 Redis 集群实现方案
  2. 多级缓存:设置多级缓存,例如本地缓存+Redis 缓存的二级缓存组合,当 Redis 缓存出现问题时,还可以从本地缓存中获取到部分数据。

针对大量缓存同时失效的情况:

  • 设置随机失效时间(可选):为缓存设置随机的失效时间, 这样可以避免大量缓存同时到期,从而减少缓存雪崩的风险。
  • 提前预热(推荐):针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期。
  • 设置缓存锁:在缓存失效时,设置一个短暂的锁定时间,只允许一个请求查询数据库并刷新缓存,其他请求等待锁释放后再读取缓存。

6.大Key问题

一、什么是大 Key?(是什么?)

大 Key 通常指的是一个 Key 对应的 Value 值非常大,或者是一个集合类型(Hash, List, Set, Sorted Set, Stream)中元素数量过多。

常见的衡量标准(面试时可以说):

  • String 类型value > 10 KB
  • 集合类型元素数量 > 10000
    • 一个 Hash 有 10000 个 field
    • 一个 List 有 10000 个元素
    • 一个 Set/ZSet 有 10000 个成员

(注意:这个标准不是绝对的,需要根据实际业务和服务器配置来定,但面试时给出一个具体数字能体现你的经验)

二、大 Key 会带来什么问题?(为什么?)

这是回答的重点,要清晰地说明大 Key 对性能、稳定性等多个维度的负面影响。

你可以从以下四个角度阐述:

  1. 性能瓶颈
    • 操作延迟高:读写一个几百KB的String,或者获取一个上万元素的List全部内容(LRANGE key 0 -1),会非常耗时,容易阻塞后续命令。
    • 网络阻塞:一次查询大Key会占用大量带宽,影响其他请求的传输。
  2. 数据倾斜
    • 在 Redis 集群模式下,数据会通过分片分布在不同的节点上。但一个大 Key 无法被拆分,会导致某个节点的数据量远大于其他节点。该节点内存不足、CPU压力大,成为整个集群的瓶颈。
  3. 阻塞风险
    • Redis 是单线程处理命令的。如果使用 DEL 命令删除一个非常大的 Key(例如一个包含百万元素的Hash),这个删除操作会长时间占用主线程,导致 Redis 服务完全阻塞,所有其他请求都会超时。
    • 同样,对大 Key 的持久化(BGSAVEBGREWRITEAOF)时,fork() 子进程的过程可能会因为复制巨大的内存页表而变慢,导致主进程短暂阻塞。
  4. 内存浪费与碎片
    • 大 Key 的扩容可能不高效,容易产生内存碎片。
    • 如果对大 Key 频繁修改,可能会触发内存页复制,消耗更多 CPU。
    • 大Key占用过多的内存空间,可能导致可用内存不足, 从而触发内存淘汰策略。在极端情况下, 可能导致内存耗尽,Redis实例崩溃,影响系统的稳定性。

三、如何发现大 Key?(怎么找?)

1. 使用官方工具 redis-cli –bigkeys

  • 这是一个扫描工具,会快速找到每种数据类型中最大的 Key。
  • 优点:简单、快速、无需额外工具。
  • 缺点:只能返回每种类型的一个最大Key,且统计的是value的大小而非key本身的大小,对于集合类型是统计元素个数。

2. 使用 memory usage 命令

  • 对于可疑的 Key,可以用这个命令来精确查询它实际占用的字节数。
  • MEMORY USAGE your_key_name

3. 使用第三方工具

  • 一些开源工具如 rdb-tools,可以离线分析 RDB 快照文件,生成所有 Key 的大小报告,非常全面准确。

4. 监控与告警

  • 通过 info memoryslowlog 等命令进行监控,如果发现内存激增或慢查询中有操作大Key的痕迹,及时告警。

四、如何解决和处理大 Key?(怎么办?)

这是体现你解决方案能力的关键。

1. 拆分(最根本的解决方案)

  • String 大 Key:如果value很大且逻辑上可拆分,比如一个大JSON,可以拆分成多个小Key,用 MSET/MGET 来批量操作。
  • 集合大 Key
    • Hash:可以按 field 的规则进行拆分。例如,用户数据 user:1000:info 有1万个field,可以拆分成 user:1000:info:1user:1000:info:2 … 每个Hash存1000个field。
    • List/Set/ZSet:可以按元素范围或规则拆分。例如,一个大的消息列表 user:1000:messages,可以按时间拆分成 user:1000:messages:202310user:1000:messages:202311

2. 删除(如何安全地删除?)
千万不要直接使用 DEL!

  • 对于集合类型的大Key,使用对应的渐进式删除命令:
    • Hash: HSCAN + HDEL(先遍历,再分批删除)
    • Set: SSCAN + SREM
    • List: 直接 LTRIM key 0 99(逐步修剪,只保留最新100个)
    • ZSet: ZSCAN + ZREM
  • 对于任何类型,在 Redis 4.0+ 版本中,可以使用异步删除命令 UNLINKUNLINK 会将 Key 从 keyspace 中立即移除,真正的内存释放会在后台异步进行,不会阻塞主线程

3. 优化数据结构

  • 思考是否可以用更高效的数据结构。例如,统计用户签到,用 String 类型做位图(SETBIT)远比用 Set 类型存储每天的日期节省空间。

4. 从设计上避免

  • 这是最重要的。在业务设计阶段,就要避免产生大Key。建立代码规范,在写入Redis时就要判断Value大小或元素数量是否可能膨胀。

五、面试回答总结(精简版)

“大Key主要是指Value过大或元素过多的Key。它主要会带来四个问题:一是操作耗时,导致慢查询;二是数据倾斜,影响集群均衡;三是阻塞风险DEL删除或持久化时可能卡住整个服务;四是内存浪费

我们可以用 redis-cli --bigkeysmemory usage 来定位它。

处理上,核心思路是拆分和异步删除

  • 拆分:把一个大Hash拆成多个小Hash,一个大List拆成多个时间段的List。
  • 删除:绝对不用 DEL,而是用 HSCAN+HDEL 这类渐进式命令,或者直接用Redis 4.0提供的异步删除命令 UNLINK,这是最安全高效的方式。

当然,最好的办法还是在设计之初就通过规范来避免产生大Key。”

这样的回答,既全面又层次分明,一定能给面试官留下好印象。

7.Redis大key如何影响持久化

对于AOF日志的影响

  • 当使用 Always 策略的时候,如果写入是一个大 Key, 主线程在执行 fsync() 函数的时候,阻塞的时间会比较久, 因为当写入的数据量很大的时候,数据同步到硬盘这个过程是很耗时的
  • 当使用 Everysec 策略的时候,由于是异步执行 fsync0 函数,所以大 Key 持久化的过程(数据同步磁盘)不会影响主线程。
  • 当使用 No 策略的时候,由于永不执行 fsync( 函数,所以大 Key 持久化的过程不会影响主线程。

对于AOF重写和RDB的影响

当 AOF 日志写入了很多的大 Key,AOF 日志文件的大小会很大, 那么很快就会触发 AOF 重写机制

AOF 重写机制和 RDB 快照(bgsave 命令)的过程, 都会分别通过 fork() 函数创建一个子进程来处理任务。

在创建子进程的过程中,操作系统会把父进程的「页表」复制一份给子进程,这个页表记录着虚拟地址和物理地址映射关系,而不会复制物理内存,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个

这样一来,子进程就共享了父进程的物理内存数据了,这样能够节约物理内存资源, 页表对应的页表项的属性会标记该物理内存的权限为只读

随着 Redis 存在越来越多的大 Key,那么 Redis 就会占用很多内存, 对应的页表就会越大。

在通过 fork() 函数创建子进程的时候,虽然不会复制父进程的物理内存, 但是内核会把父进程的页表复制一份给子进程,如果页表很大, 那么这个复制过程是会很耗时的,那么在执行 fork 函数的时候就会发生阻塞现象

而且,fork 函数是由 Redis 主线程调用的,如果 fork 函数发生阻塞, 那么意味着就会阻塞 Redis 主线程。

什么时候会发生物理内存复制呢?

当父进程或者子进程在向共享内存发起写操作时,CPU 就会触发写保护中断, 这个「写保护中断」是由于违反权限导致的, 然后操作系统会在「写保护中断处理函数」里进行物理内存的复制,并重新设置其内存映射关系, 将父子进程的内存读写权限设置为可读写,最后才会对内存进行写操作, 这个过程被称为「**写时复制(Copy On Write)**」。

如果创建完子进程后,父进程对共享内存中的大 Key 进行了修改, 那么内核就会发生写时复制,会把物理内存复制一份, 由于大 Key 占用的物理内存是比较大的,那么在复制物理内存这一过程中, 也是比较耗时的,于是父进程(主线程)就会发生阻塞

总结:

当 AOF 写回策略配置了 Always 策略,如果写入是一个大 Key,主线程在执行 fsync0 函数的时候,阻塞的时间会比较久,因为当写入的数据量很大的时候,数据同步到硬盘这个过程是很耗时的。

AOF 重写机制和 RDB 快照(bgsave 命令)的过程,都会分别通过 fork()函数创建一个子进程来处理任务。会有两个阶段会导致阻塞父进程(主线程):

  1. 创建子进程的途中,由于要复制父进程的页表等数据结构, 阻塞的时间跟页表的大小有关,页表越大,阻塞的时间也越长;
  2. 创建完子进程后,如果父进程修改了共享数据中的大 Key, 就会发生写时复制,这期间会拷贝物理内存, 由于大 Key 占用的物理内存会很大,那么在复制物理内存这一过程,就会比较耗时,所以有可能会阻塞父进程。

8.热Key

一、什么是热 Key?

热 Key(Hot Key)是指在 Redis 中,某个 Key 在短时间内被极其高频地访问,其访问量远远超过了其他 Key。

例如:

  • 某个明星突然宣布离婚,其微博主页的缓存 Key 可能瞬间被每秒访问上百万次。
  • 电商平台中,某个参与秒杀活动的热门商品详情页的 Key。
  • 新闻App中,一条突发新闻的缓存 Key。

当一个 Key 成为热 Key 时,它就不再是一个简单的数据,而变成了整个系统的瓶颈点

二、热 Key 会带来什么问题?

热 Key 的危害是巨大且连锁的,主要体现在以下四个方面:

1. 流量集中,打爆 Redis 单节点性能瓶颈

  • Redis 是单线程模型(处理命令的主线程),单个节点的处理能力有上限。
  • 所有针对同一个 Key 的请求都必须由同一个节点处理(在集群模式下,一个 Key 只存在于一个节点)。这会导致该节点的 CPU 负载飙升,达到 100%,无法处理更多请求。
  • 即使 Redis 是集群模式,也无法分摊同一个 Key 的访问压力。

2. 带宽占满,影响其他服务

  • 如果热 Key 对应的 Value 很大(大 Value + 热 Key = 灾难),超高的 QPS 会瞬间占满该服务器的出口带宽,导致连正常的 SSH 连接都变得困难,并可能影响同一机器或同一交换机下的其他服务。

3. 连接数耗尽,资源不足

  • 大量的客户端连接涌向同一个 Redis 节点,可能会耗尽该节点的最大连接数(maxclients),导致新的合法连接无法建立,出现 Cannot assign requested addressConnection timeout 等错误。

4. 缓存击穿,压垮数据库

  • 如果这个热 Key 恰好过期失效,或者缓存服务因压力过大而崩溃,海量的请求会直接穿透缓存,全部打到后端数据库(如 MySQL)上。
  • 数据库根本无法承受如此高的并发查询,很快就会被压垮,从而导致整个系统雪崩。

三、如何发现热 Key?

1. 预估判断

  • 根据业务经验,提前预测哪些可能会成为热 Key,如秒杀商品、热门话题等。

2. 客户端收集

  • 在业务代码中嵌入统计逻辑,记录每个 Key 的访问频次,然后上报给监控系统。

3. Redis 自带命令

  • redis-cli –hotkeys:Redis 4.0 以上版本提供的命令,可以快速找出热点 Key。(注意:在生产环境使用可能对性能有影响)
  • monitor:可以实时打印出 Redis 处理的所有命令,然后通过脚本分析。严禁在生产环境长时间使用,会严重拖慢 Redis 性能。

4. 网络抓包分析

  • 通过一些网络抓包工具(如 TCPdump)分析流量,推断出热点 Key。

5. 借助第三方监控工具

  • 使用 Redis-Faina(Instagram 开源)等工具来分析 monitor 命令的输出。
  • 使用云服务商(如阿里云、腾讯云)提供的商业版 Redis 监控功能,它们通常自带热 Key 分析功能。

四、解决方案

解决热 Key 问题的核心思路是:分散压力,化点为面

1. 二级本地缓存(最有效、最常用的方案)

  • 方法:在访问 Redis 之前,增加一层本地缓存(如 Guava Cache, Caffeine, Ehcache)。
  • 实施
    1. 当热 Key 请求到来时,首先检查本地缓存。
    2. 如果本地缓存有,直接返回。
    3. 如果本地缓存没有,才去查询 Redis,并将结果写入本地缓存(设置一个较短的过期时间,比如几秒钟)。
  • 优点:绝大部分请求根本不会走到 Redis,压力被分散到各个应用服务器上。
  • 缺点:需要解决本地缓存的一致性问题和内存管理问题。

2. 备份 Key(读写分离)

  • 方法:将一个热 Key 复制出多个副本(key1, key2, key3, …),分散到不同的 Redis 节点上(如果是集群模式)或存储在同一个实例中。
  • 实施
    1. 写操作:同时更新所有的备份 Key。
    2. 读操作:在客户端采用随机策略,随机访问某一个备份 Key,将流量打散。
  • 优点:实现起来相对简单。
  • 缺点:数据一致性难以保证,更新逻辑更复杂。

3. 使用 Redis 集群并增加副本(Read Replicas)

  • 方法:为 Redis 集群中的主节点增加多个从节点(副本),让读请求可以分流到从节点上。
  • 实施:通过读写分离,让主节点只处理写请求,而让多个从节点来分担大量的读请求。
  • 优点:Redis 原生支持,架构清晰。
  • 缺点无法解决单一热 Key 的问题,因为同一个 Key 的数据只存在于一个主从组中,读请求仍然会压垮该主从组中的节点。主要用于提升整体读能力。

4. 业务逻辑优化

  • 方法:避免在 Value 中存储过大的数据。或者将热点数据“化整为零”。
  • 示例:对于一个热门帖子的评论列表,不要只用一个 List 来存,可以按页拆分,如 comment:postid:1, comment:postid:2

你可以这样回答(总分总结构):

“热 Key 问题是指某个 Key 在瞬间被大量请求访问,比如每秒几十万次,导致这个 Key 所在的 Redis 节点成为瓶颈,CPU、带宽、连接数全部被打满,进而引发服务雪崩。

它的核心危害有四个:1. 打满单节点性能;2. 占满网络带宽;3. 耗尽连接数;4. 导致缓存击穿,压垮数据库。

解决热 Key 的核心思路是分散压力,主要有两个方案:

  1. 二级本地缓存:这是最有效的方法。在应用层用 Guava Cache 或 Caffeine 做一层本地缓存,让绝大部分请求根本到不了 Redis,从而把集中式的压力分散到所有应用服务器上。
  2. 备份 Key:将热 Key 复制多份,比如 key1, key2,客户端随机访问其中一个,把对单个 Key 的访问压力分摊到多个 Key 上。

当然,首先要能及时发现热 Key,可以通过业务预估、redis-cli --hotkeys 命令或第三方监控工具来实现。在实际项目中,我们通常首选二级本地缓存的方案来预防和解决热 Key 问题。”

这样的回答,既点明了问题本质和危害,又给出了具体、可落的解决方案,层次清晰,能让面试官非常满意。

9.缓存预热

缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!

常见的缓存预热方式有两种:

  1. 使用定时任务,比如 xxl-job,来定时触发缓存预热的逻辑,将数据库中的热点数据查询出来并存入缓存中。
  2. 使用消息队列,比如 Kafka,来异步地进行缓存预热,将数据库中的热点数据的主键或者 ID 发送到消息队列中,然后由缓存服务消费消息队列中的数据,根据主键或者 ID 查询数据库并更新缓存。

5.底层结构

String——动态字符串SDS

List——双向链表

Set——哈希表

主要是ZSet的底层数据结构实现——跳表

底层结构

1.SDS

Redis 是用 C 语言实现的,但其String类型是采用了一个叫做简单动态字符串(simple dynamic string,SDS) 的数据结构来表示字符串。

为什么不用C语言中的字符串呢?

  1. 获取字符串长度的时间复杂度为 O(N);

  2. 字符串的结尾是以 “\0” 字符标识,这就要求字符串里面不能包含有 “\0” 字符, 因此不能保存二进制数据

  3. 字符串操作函数不高效且不安全, 比如有缓冲区溢出的风险,有可能会造成程序运行终止;

    例:strcat 函数是可以将两个字符串拼接在一起。 C 语言的字符串是不会记录自身的缓冲区大小的, 所以 strcat 函数假定程序员在执行这个函数时,已经为拼接的字符串分配了足够多的内存,而如果没有,就会发生溢出

SDS结构

SDS结构

引入了len,alloc,flags解决了C语言字符串的问题

  1. len,记录了字符串长度。 获取字符串长度 时间复杂度只需要 O(1)
  2. alloc,分配给字符数组的空间长度。 这样在修改字符串的时候,可以通过 alloc - len 计算出剩余的空间大小,可以用来判断空间是否满足修改需求,如果不满足的话,就会自动将 SDS 的空间扩展至执行修改所需的大小,然后才执行实际的修改操作, 所以使用 SDS 既不需要手动修改 SDS 的空间大小,也不会出现前面所说的缓冲区溢出的问题
  3. flags,用来表示不同类型的 SDS能灵活保存不同大小的字符串,从而有效节省内存空间
  4. buf[],字节数组,用来保存实际数据。不仅可以保存字符串,也可以保存二进制数据

2.双向链表

双向链表

优点:

  1. listNode 链表节点的结构里带有 prev 和 next 指针, 获取某个节点的前置节点或后置节点的时间复杂度只需O(1),
  2. list 结构因为提供了表头指针 head 和表尾节点 tail, 所以**获取链表的表头节点和表尾节点的时间复杂度只需O(1)**;
  3. list 结构因为提供了链表节点数量 len,所以**获取链表中的节点数量的时间复杂度只需O(1)**;

缺点:

  1. 链表每个节点之间的内存都是不连续的,意味着无法很好利用 CPU 缓存。 能很好利用 CPU 缓存的数据结构就是数组, 因为数组的内存是连续的,这样就可以充分利用 CPU 缓存来加速访问。

3.压缩列表ZipList

压缩列表是 Redis 为了节约内存而开发的,它是由连续内存块组成的顺序型数据结构, 有点类似于数组。

压缩列表

压缩列表在表头有三个字段:

  • zlbytes,记录整个压缩列表占用的内存字节数;
  • zltail,记录压缩列表「尾部」节点距离起始地址有多少字节, 也就是列表尾的偏移量;
  • zllen,记录压缩列表包含的节点数量;
  • zlend,标记压缩列表的结束点,固定值 0xFF(十进制255)。

压缩列表解决了双向列表内存碎片的问题,因为双向链表每个节点存放在内存中不连续的位置。另外,ziplist 为了在细节上节省内存,对于值的存储采用了 变长编码方式, 大概意思是说,对于大的整数,就多用一些字节来存储,而对于小的整数,就少用一些字节来存储。

压缩列表的缺点是会发生连锁更新的问题,因此连锁更新一旦发生, 就会导致压缩列表占用的内存空间要多次重新分配,这就会直接影响到压缩列表的访问性能

因此,压缩列表只会用于保存的节点数量不多的场景, 只要节点数量足够小,即使发生连锁更新,也是能接受的。

4.哈希表

dictionary 字典 dict

哈希表结构,哈希冲突,链式哈希这些都不说了,比较熟悉了,下面是不太熟悉的

1.rehash(扩容)

也就是哈希表的扩容(不过注意区别:处理哈希冲突有个再哈希法,那个意思是再用另一个哈希函数计算要插入的位置)

在实际使用哈希表时,Redis 定义一个 dict 结构体,这个结构体里定义了两个哈希表(ht[2])。之所以定义了 2 个哈希表,是因为进行 rehash 的时候,需要用上 2 个哈希表了。

在正常服务请求阶段,插入的数据,都会写入到「哈希表 1」,此时的「哈希表2」 并没有被分配空间。随着数据逐步增多,触发了rehash 操作,这个过程分为三步:

  • 给「哈希表 2」 分配空间,一般会比「哈希表 1」 大一倍(两倍的意思);
  • 将「哈希表 1 」的数据迁移到「哈希表 2」 中;
  • 迁移完成后,「哈希表 1 」的空间会被释放,并把「哈希表 2」 设置为「哈希表 1」,然后在「哈希表 2」 新创建一个空白的哈希表,为下次 rehash 做准备。

一图说明

PS:这里个人觉得很像JVM中的复制算法,两个空间交替使用完之后,然后交换名字继续用

rehash

  1. 原哈希表的数据迁移到新的哈希表(长度是原来的2倍)
  2. 迁移完成后,释放原哈希表的空间,并让新哈希表指向原哈希表的地址

这样有个问题,就是如果哈希表数据过多,在迁移的时候,因为会涉及大量的数据拷贝,此时可能会对 Redis 造成阻塞,无法服务其他请求。 所以引出了渐进式hash

2.渐进式hash——解决rehash第二步数据迁移可能会阻塞Redis的痛点

渐进式hash把一次性大量数据迁移工作的开销,分摊到了多次处理请求的过程中, 避免了一次性 rehash 的耗时操作。

为了支持渐进式重哈希,Redis 的 dict 结构包含两个哈希表:

  • **ht[0]**:当前正在使用的哈希表。
  • **ht[1]**:用于重哈希的目标哈希表(初始为空)。

渐进式 rehash 步骤如下:

哈希表元素的删除、查找、更新等操作都会在这两个哈希表进行。比如,查找一个 key 的值的话,先会在「哈希表 1」 里面进行查找, 如果没找到,就会继续到哈希表 2 里面进行找到。 在渐进式 rehash 进行期间,新增一个 key-value 时, 会被保存到「哈希表 2 」里面,而「哈希表 1」 则不再进行任何添加操作, 这样保证了「哈希表 1 」的 key-value 数量只会减少,随着 rehash 操作的完成, 最终「哈希表 1 」就会变成空表。

3.rehash触发条件

和负载因子有关。负载因子 = 哈希表已经保存的节点数/哈希表大小

  • *当负载因子大于等于 1 ,并且 Redis 没有在执行 bgsave 命令或者 bgrewiteaof 命令,也就是没有执行 RDB 快照或没有进行 AOF 写的时候,就会进行 rehash 操作。
  • 当负载因子大于等于 5 时,此时说明哈希冲突非常严重了, 不管有没有有在执行 RDB 快照或 AOF 重写, 都会强制进行 rehash 操作。

5.整数集合

intset 是一个由整数组成的 有序集合,从而便于在上面进行二分查找,用于快速地判断一个元素是否属于这个集合。 它在内存分配上与 ziplist 有些类似,是连续的一整块内存空间,而且对于大整数和小整数(按绝对值)采取了不同的编码,尽量对内存的使用进行了优化。

对于小集合使用 intset 来存储,主要的原因是节省内存。特别是当存储的元素个数较少的时候, dict 所带来的内存开销要大得多(包含两个哈希表、链表指针以及大量的其它元数据)。所以,当存储大量的小集合而且集合元素都是数字的时候,用 intset 能节省下一笔可观的内存空间。

实际上,从时间复杂度上比较, intset 的平均情况是没有 dict 性能高的。以查找为例,intset 是 OO(lgn) 的,而 dict 可以认为是 O(1) 的。但是,由于使用 intset 的时候集合元素个数比较少,所以这个影响不大。

6.跳表

链表在查找元素的时候,因为需要逐一查找,所以查询效率非常低,时间复杂度是O(N) ,于是就出现了跳表。跳表是在链表基础上改进过来的,实现了一种「多层」的有序链表, 这样的好处是能快读定位数据。

跳表结构

7.四种rehash对比

Rehash(重哈希)是哈希表在扩容或收缩时,重新计算所有元素在新容量下的位置并迁移数据的过程。核心目的是:

  1. 维持合理的负载因子(load factor)
  2. 减少哈希冲突
  3. 优化查询效率
类型 触发条件 执行方式 代表实现 优缺点
一次性Rehash 容量达到阈值 全部元素立即迁移 Java HashMap 简单但可能造成明显卡顿
渐进式Rehash 容量达到阈值 分多次逐步迁移 Redis哈希表 平滑但实现复杂
一致性哈希Rehash 节点增减 只迁移受影响数据 Redis Cluster 迁移量小但需要虚拟节点
动态Rehash 自动监测性能 按需触发 Go map 自适应但预测算法复杂

6.Redis线程模型

阅读以下文字,对于Redis线程模型相关的常见问题基本就有了一个答案

  1. 核心前提:操作在内存中完成 + 高效的数据结构

  • 内存中完成: 这是最关键的一点。与磁盘(即使是SSD)相比,内存的读写速度要快几个数量级。这意味着数据的读写操作本身耗时极短,CPU 执行这些操作的速度非常快,大部分请求的处理时间都花在了网络传输和协议解析上,而不是 CPU 计算上。
  • 高效的数据结构: Redis 不是简单地将数据扔进内存,而是为不同的数据类型精心设计了最优的数据结构。例如:
    • String 使用简单动态字符串(SDS)。
    • Hash 和 Set 在元素较少时使用压缩列表(ziplist)或整数集合(intset),元素多时转为哈希表或字典。
    • Sorted Set 使用了跳跃表(skiplist)和哈希表的组合。
      这些数据结构的设计目标就是在保证功能的前提下,最大化操作速度并最小化内存占用。这使得单个操作的 CPU 计算成本非常低。

结论: 这两点共同决定了,对于单个客户端请求,CPU 需要做的工作量很小,处理速度极快。

  1. 瓶颈分析:内存 vs. 网络 vs. CPU

  • 内存是瓶颈: 因为所有数据都存放在内存中,所以你能存储的数据量上限受限于机器的物理内存大小。内存的价格和扩展成本比磁盘高,这是 Redis 作为内存数据库的天然限制。
  • 网络带宽是瓶颈: Redis 的处理速度极快,通常每秒能处理数十万甚至上百万次请求。在如此高的吞吐量下,千兆网卡(125 MB/s)的带宽很容易成为瓶颈。尤其是在处理大体积的 value(如几个MB的字符串)时,网络 I/O 的时间远远超过 CPU 处理命令的时间。
  • CPU 不是瓶颈: 正因为单个命令的 CPU 处理成本极低,在绝大多数应用场景下(例如简单的 GETSETLPUSH 等),Redis 的服务能力在达到 CPU 饱和之前,早就被内存容量或网络带宽限制住了。提升 CPU 性能(例如增加核心数)并不能让你存储更多数据或增加网络带宽,因此对提升性能没有直接帮助。
  1. “自然采用单线程”的深层逻辑

既然 CPU 不是瓶颈,那么引入多线程反而会带来复杂性和开销,单线程则拥有巨大的优势:

1. 避免上下文切换和竞争锁的开销
这是最核心的原因。多线程模型虽然能利用多核,但线程间的切换(Context Switching)会消耗大量的 CPU 时间。同时,为了线程安全,必须使用锁(Lock)或其它同步机制来保护共享数据(如内存中的键值对)。加锁、解锁、等待锁释放都会带来额外开销,甚至可能导致线程阻塞,反而降低了性能。单线程模型完美地规避了所有这些开销和复杂性,实现了最高的效率。

2. 避免同步问题,实现原子性
单线程意味着所有操作都是串行执行的。每一个操作在执行时都是独占整个 CPU 时间的,中间不会有其他操作介入。这天然地保证了所有操作的原子性(Atomicity),无需任何额外的并发控制。开发者在编写 Lua 脚本或使用 Redis 命令时,无需担心数据竞争(Race Condition)的问题,极大地简化了系统设计和应用开发。

需要强调的是,我们所说的 Redis “单线程”指的是 其核心的网络 I/O 和键值对读写操作是由一个线程完成的

  • Redis 6.0 之后的多线程: 现代版本的 Redis 确实引入了多线程,但这并不是为了并行执行命令(命令执行仍然是单线程的),而是为了处理网络 I/O。即,使用多个 I/O 线程来并行地读取请求和写回响应,而真正执行命令的仍然是那个单线程。这样做的目的是为了将耗时的网络 I/O 操作从主线程中卸载,从而进一步提升整体性能,尤其是在高并发场景下。这恰恰印证了原话中的判断——瓶颈在于网络。
  • 后台线程: 此外,Redis 还有一些后台线程用于执行一些慢速的、可能阻塞的操作,如异步删除(UNLINK)、持久化(AOF fsync)等,以避免这些操作阻塞主线程。

1.单线程模型

  • 如果仅仅聊Redis的核心业务部分(命令处理),答案是单线程

  • 如果是聊整个Redis,那么答案就是多线程

Redis 在处理网络请求是使用单线程模型,并通过 IO 多路复用来提高并发。 但是在其他模块,比如:持久化,会使用多个线程。

Redis 内部使用文件事件处理器 file event handler这个文件事件处理器是单线程的,所以 Redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket ,将产生事件的 socket 压入内存队列中,事件分派器根据 socket 上的事件类型来选择对应的事件处理器进行处理。

文件事件处理器如下:

文件事件处理器

文件事件处理器的结构包含 4 个部分:

  • 多个 socket
  • IO 多路复用程序
  • 文件事件分派器
  • 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)

2.为什么Redis单线程模型也能效率这么高?

抛开持久化不谈,Redis是纯内存操作,执行速度非常快,它的性能瓶颈是网络延迟以及内存限制而不是执行速度,因此多线程并不会带来巨大的性能提升。

  1. 纯内存操作
  2. 避免多线程频繁的上下文切换开销,以及并发加锁开销
  3. IO多路复用模型,允许同时监听多个socket

3.既然Redis单线程也那么快,为什么后面又采用了多线程?

核心答案:Redis 的核心处理逻辑仍然是单线程的,它引入多线程主要是为了卸载那些最耗时的【网络 I/O】和【持久化】任务,从而释放主线程(单线程)的压力,让其更专注、更高效地处理核心命令。

1.Redis 单线程为什么快?(复习)

在理解“为什么变”之前,先要理解“为什么以前行”。Redis 单线程模型之所以高效,源于以下几点:

  1. 纯内存操作:数据存储在内存中,读写速度极快。
  2. 单线程避免上下文切换:单线程避免了多线程频繁切换 CPU 上下文带来的巨大开销。
  3. 避免锁竞争:单线程自然避免了多线程环境下复杂的同步机制(如锁)带来的性能和复杂度问题。
  4. 非阻塞 I/O 多路复用:使用 epoll 等机制,用单个线程管理成千上万的网络连接,高效地处理连接、读写事件。

2.为什么单线程模型会遇到瓶颈?

随着网络硬件性能的提升和业务数据量的爆炸式增长,单线程模型的瓶颈不再在 CPU,而转移到了以下两个方面:

1. 网络 I/O(最大的瓶颈)

  • 在千兆/万兆网卡已成为标配的今天,网络数据的读写速度极高。
  • 所有网络数据的读写、解析都由主线程完成,这变成了一个沉重的负担。当客户端数量极多、流量极大时,主线程会花费大量时间在 I/O 上,而不是处理命令,导致延迟增加,QPS 无法进一步提升。

2. 持久化(次要瓶颈)

  • fork 操作:在执行 BGSAVEBGREWRITEAOF 进行持久化时,需要调用 fork 创建子进程。fork 操作本身在数据量大时是阻塞的,而且会消耗大量 CPU 资源,尤其是云服务环境下。
  • AOF 刷盘:如果使用 appendfsync always 策略,每个命令都要刷盘,主线程会等待这个磁盘 I/O 完成。

3.Redis 的“多线程”具体指什么?

Redis 引入的多线程是非常克制和有选择性的,并非像 MySQL 那样用多线程来处理所有客户端连接。

1. 多线程网络 I/O(Redis 6.0 引入)

  • 做了什么:将网络数据的读写(Socket 的读和写)这部分最耗时的操作,交给一组独立的 I/O 线程去并行处理。
  • 没做什么命令的解析(Parsing)、执行(Execute)、返回结果的组装,仍然由主线程串行完成。这完美地保留了单线程无锁竞争的优势。
  • 工作流程
    1. 主线程负责通过 I/O 多路复用接收连接,并将 Socket 连接分配给 I/O 线程。
    2. I/O 线程并行地从这些 Socket 中读取请求数据,并将其解析为命令,然后放入一个队列中等待主线程处理
    3. 主线程串行地从队列中取出命令并执行
    4. 执行完成后,主线程将结果放入另一个回复队列。
    5. I/O 线程并行地从回复队列中取出结果,写回给对应的客户端 Socket。
  • 默认关闭:该功能默认是关闭的,需要配置 io-threads(设置线程数)和 io-threads-do-reads yes 来开启。

2. 后台线程处理慢操作(Redis 4.0 引入)

  • 目的:将一些原本由主线程执行的慢操作异步化,交给后台线程处理,避免阻塞主线程。
  • 典型任务
    • 异步删除UNLINKFLUSHDB ASYNCFLUSHALL ASYNC 命令。删除一个大 Key 可能在毫秒级,用 DEL 会阻塞主线程,而用 UNLINK 会交给后台线程慢慢删。
    • 异步持久化:虽然 fork 本身无法异步,但一些相关的清理工作可以交由线程处理。

4.面试回答技巧

面试官:既然Redis单线程也那么快,为什么后面又采用了多线程?

你可以这样回答:

“Redis 单线程快是建立在纯内存操作和避免上下文切换的基础上的。但随着硬件发展,尤其是万兆网卡的普及,网络 I/O 的处理逐渐成为了最大的瓶颈。主线程花费大量时间在读写网络数据上,限制了性能的进一步提升。

所以 Redis 引入多线程并非为了改变其核心的单线程命令处理模型,而是一种非常精巧的优化,主要体现在两方面:

  1. 多线程网络 I/O(Redis 6.0):将最耗时的网络数据读写操作从主线程中卸载,交给一组 I/O 线程去并行处理。而命令的解析和执行这个核心逻辑,依然由主线程串行完成。这样既利用了多核来突破网络 I/O 瓶颈,又保留了单线程无锁设计的全部优势。
  2. 后台线程(Redis 4.0):将一些慢操作,如大 Key 的异步删除(UNLINK),交给后台线程处理,避免阻塞主线程。

总之,Redis 的‘多线程’是辅助性的,它的目标不是变成一个多线程数据库,而是为了解放那个核心的单线程,让它跑得更快。这体现了 Redis 作者一种非常务实和平衡的设计哲学。”

这样的回答,既说明了历史原因,又解释了新技术方案解决的具体问题,还体现了你对技术细节的把握,绝对是面试官想要的满分答案。

7.高可用

1.主从复制

为了应对并发能力问题,可以搭建主从集群,实现读写分离,Redis大多都是读多写少的场景

多台服务器要保存同一份数据,这些服务器之间的数据如何保持一致性呢?数据的读写操作是否每台服务器都可以处理?

Redis 提供了主从复制模式,来避免上述的问题。

主从复制的核心过程可以分为建立连接数据同步命令传播三个阶段。

如何确定主从关系?比如想让服务器B变成服务器A的从服务器

1
2
# 服务器 B 执行这条命令
replicaof <服务器 A 的 IP 地址> <服务器 A 的 Redis 端口号>

建立连接阶段

从服务器就会给主服务器发送 psync 命令,表示要进行数据同步。

psync 命令包含两个参数,分别是主服务器的 runID 和复制进度 offset

在介绍第一次同步的过程之前,需要先介绍两个参数

从服务器就会给主服务器发送 sync 命令,表示要进行数据同步。 sync 命令包含两个参数 ,可以判断从服务器是否第一次来同步数据

  1. Replicationld:简称replid,每个 Redis 服务器在启动时都会自动生产一个随机的 ID 来唯一标识自己。 从服务器会继承主服务器的replid

  2. offset:偏移量,表示复制的进度,随着记录在repl_baklog中的数据增多而逐渐增大。主节点用offset记录自己的位置,从节点用offset记录自己的位置。slave完成同步时也会记录当前同步的offset。

    如果slave的offset小于master的offset,说明slave数据落后于master,需要更新。

全量同步阶段

  1. slave节点发送sync请求,表示要进行数据同步
  2. master将执行 bgsave 命令(异步,不会阻塞主线程)生成RDB,发送RDB到slave。
  3. slave清空本地数据,加载master的RDB
  4. master将生成RDB期间、发送RBD期间以及「从服务器」加载 RDB 文件期间的命令记录在replication_buffer,当slave加载完RDB后,slave会回复一个确认消息给主服务器。 然后主服务器将 replication buffer 缓冲区里所记录的写操作命令发送给从服务器, 主服务器将 replication buffer 缓冲区里所记录的写操作命令发送给从服务器,

至此,主从服务器的第一次同步的工作就完成了。

命令传播阶段:

主从服务器在完成第一次同步后,双方之间就会维护一个 TCP 连接。 而且这个连接是长连接的,目的是避免频繁的 TCP 连接和断开带来的性能开销。

  • 主节点每执行一个写命令,都会异步地将这个命令发送给所有与其连接的从节点。
  • 从节点接收到命令后,会执行与主节点相同的命令,从而保证数据最终一致性。

上面的这个过程被称为基于长连接的命令传播

增量同步阶段

主从连接短暂中断后重连 ,采用增量同步

  • 从节点重连后,会带上自己的 runID(服务器随机生成的ID)和当前的 offset(复制偏移量)发送 PSYNC <runid> <offset> 命令。
  • 主节点判断 runID 是否与自己一致,并检查 offset 是否还在积压缓冲区中。
  • 如果条件满足,主节点会回复 +CONTINUE,表示进行部分同步。
  • 主节点将积压缓冲区中从 offset 之后的所有写命令发送给从节点。
  • 从节点执行这些命令,即可追上主节点的状态。

主服务器怎么知道要将哪些增量数据发送给从服务器呢? 依赖以下两个东西

  • repl_backlog_buffer,是一个「环形」缓冲区, 用于主从服务器断连后,从中找到差异的数据;
  • replication offset,标记上面那个缓冲区的同步进度, 主从服务器都有各自的偏移量

这个缓冲区是什么时候写入的呢?

是在命令传播阶段写入的,在主服务器进行命令传播时,不仅会将写命令发送给从服务器, 还会将写命令写入到 repl_backlog_buffer 缓冲区里, 因此 这个缓冲区里会保存着最近传播的写命令。

注意,这个缓冲区是环形的,也就是存在覆盖的风险!

写满后会覆盖最早的数据。如果slave断开时间过久,导致尚未备份的数据被覆盖,则无法基于log做增量同步,只能再次全量同步。

因此,为了避免在网络恢复时,主服务器频繁地使用全量同步的方式,我们应该设置repl_backlog_buffer 缓冲区尽可能的大一些, 减少出现从服务器要读取的数据被覆盖的概率,从而使得主服务器采用增量同步的方式。

主从复制的作用

①、数据冗余: 主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。

②、故障恢复: 如果主节点挂掉了,可以将一个从节点提升为主节点,从而实现故障的快速恢复。

通常会使用 Sentinel 哨兵来实现自动故障转移,当主节点挂掉时,Sentinel 会自动将一个从节点升级为主节点,保证系统的可用性。 假如是从节点挂掉了,主节点不受影响,但应该尽快修复并重启挂掉的从节点,使其重新加入集群并从主节点同步数据。

③、负载均衡: 在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务 ,分担服务器负载。尤其是在读多写少的场景下,通过多个从节点分担读负载,可以大大提高 Redis 服务器的并发量。

④、高可用基石: 除了上述作用以外,主从复制还是哨兵和集群能够实施的 基础

重要特性与细节

  1. 异步复制:主节点向从节点发送数据是异步的。这意味着:
    • 高性能:主节点不会等待从节点的确认,它处理客户端请求的速度非常快。
    • 弱一致性:主从数据之间存在短暂延迟。从节点的数据会稍落后于主节点(最终一致)。
    • 潜在数据丢失风险:如果主节点在写命令发送给从节点之前宕机,且未开启持久化,那么这个写命令可能会丢失。
  2. 复制偏移量与积压缓冲区
    • **复制偏移量 (Replication Offset)**:主从节点都会维护一个偏移量(offset)。主节点每次传播一个字节的数据,偏移量就会增加。从节点每次收到数据,也会增加自己的偏移量。通过对比偏移量,可以判断主从数据是否一致。
    • 复制积压缓冲区 (Replication Backlog):主节点内部维护的一个固定长度的、先进先出(FIFO)的队列。它默认大小为 1MB,用于存放最近传播的写命令。这是实现部分重同步的基础。
  3. 服务器运行 ID (Run ID)
    • 每个 Redis 服务器,无论主从,都会在启动时自动生成一个唯一的 40 位随机十六进制字符串作为 runID
    • 从节点会保存主节点的 runID。断线重连时,通过对比 runID 来判断连接的是不是原来的主节点。如果不是,就需要进行全量同步。

注意两个缓冲区的区别

replication buffer 和 repl backlog buffer

你可以这样回答:

“这两个 Buffer 在 Redis 主从复制中扮演着完全不同的角色:

  1. Replication Buffer
    • 它是每个从节点独享的一个客户端输出缓冲区
    • 它的核心作用是在全量同步BGSAVE 期间临时存放主节点收到的新写命令。等从节点加载完 RDB 文件后,主节点再把这个 Buffer 里的命令发过去,保证数据一致性。
    • 如果它溢出了(从节点太慢),主节点会断开对应从节点的连接。
  2. Repl Backlog Buffer
    • 它是主节点上全局共享的一个固定大小的环形缓冲区
    • 它的核心作用是在正常的命令传播阶段持续记录最近传播的所有写命令,为部分重同步(也就是增量复制)提供数据源。当从节点网络闪断重连后,可以直接从这里获取错过的命令,避免全量同步。
    • 如果它溢出了(写命令太多且从节点断开太久),旧数据会被覆盖,导致部分重同步失败,从而退化为全量同步。

简单总结Replication Buffer同步期临时补给,而 Repl Backlog Buffer传播期持久化日志,用于断线重连后的快速恢复。”

2.哨兵(Sentinel)机制

1.为什么需要哨兵机制?

它主要解决了主从复制模式下的核心痛点:故障自动转移(Failover)

在主从模式中,如果主节点(Master)宕机,需要手动进行以下操作:

  1. 选择一个从节点(Replica)晋升为新的主节点。
  2. 让其他从节点改为复制新的主节点。
  3. 通知客户端应用程序切换连接至新的主节点。

这个过程繁琐、缓慢且容易出错,无法实现高可用。Sentinel 的核心目标就是将这个过程自动化

2.哨兵的四大功能

  1. 监控(Monitoring):持续检查主节点和从节点是否处于正常工作状态。
  2. 通知(Notification):当被监控的 Redis 实例出现问题时,可以通过 API 或其他系统发送通知。
  3. 自动故障转移(Automatic Failover):当主节点故障时,Sentinel 会自动将一个从节点提升为新的主节点,并让其他从节点复制新的主节点。
  4. 配置提供者(Configuration Provider):客户端应用首先连接到 Sentinel,询问当前哪个是主节点。故障转移后,Sentinel 会提供新的主节点地址,充当了服务发现的中心。

3.监控——如何判断主节点真的鼓掌了?

Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令

  • 主观下线:如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线。
  • 客观下线:为了防止误判,单个 Sentinel 发现主节点主观下线后,它会询问集群中的其他 Sentinel,看它们是否也认为该主节点不可用。当达到一定数量(quorum,法定人数,在配置文件中设置)的 Sentinel 都报告主节点主观下线时,此时才判定主节点客观下线

quorum 的值一般设置为哨兵个数的二分之一加 1,例如 3 个哨兵就设置 2

4.自动故障转移——由哪个哨兵进行主从故障转移

领导者选举

一旦主节点被判定为客观下线触发Leader选举),多个 Sentinel 实例需要选举出一个领导者(Leader),由这个领导者来执行故障转移操作。

为什么需要选一个领导者? 这是为了确保故障转移操作只由一个哨兵来执行,避免多个哨兵同时去操作,导致脑裂或者重复提升从节点等问题。

  1. 任何哨兵都可以发起投票:任何一个确认主节点为客观下线的哨兵,都可以向其他哨兵发送命令,请求对方选举自己为领导者。所以,每个哨兵在理论上都是潜在的候选者。

  2. 投票规则:当一个哨兵收到投票请求时,它会遵循一套严格的规则来决定是否同意:

    • 先到先得:每个哨兵在一个纪元内,只会投出一张票(先投给谁,后就无法再投给别人)。纪元是一个不断递增的计数器,每次选举都会在一个新的纪元中进行,确保选举的唯一性。
    • 无法自投:哨兵不能投给自己。
    • 优先投票给优先级高的:如果没有投过票,它会优先投票给第一个向它发送投票请求的哨兵。
  3. 成为领导者的条件:一个哨兵要想成为领导者,必须满足两个条件:

    • 获得多数票:它必须获得超过半数的哨兵节点的同意(注意:这个“半数”是哨兵节点的半数,而不是 quorum 的值)。例如,如果有 5 个哨兵,那么至少需要 3 张票。
    • 获得的票数还必须大于或等于配置文件中配置的 quorum 值

    公式:票数 >= max(quorum, num-sentinels/2 + 1)
    这确保了当选的领导者具有足够的权威性。

  4. 选举结果

    • 如果某个哨兵获得了足够的票数,它就成为了领导者,并开始执行故障转移流程(选一个从节点提升为主节点、修改配置、通知客户端等)。
    • 如果在规定时间内没有哨兵获得足够的票数,选举会失败。等待一段时间(随机延迟)后,会开启新一轮纪元(epoch)的选举,直到选出一个领导者为止。这个随机延迟有助于避免多个哨兵同时发起投票导致票数分散。

5.开始故障转移

a) 筛选新的主节点 Sentinel Leader 会根据以下规则,从剩余的从节点中筛选出最合适的新主节点:

  1. 排除不健康的:断开连接的、最近通信超时的、主观下线的。
  2. 排除优先级低的:比较 slave-priority(或 replica-priority)配置值,优先级数字越小,优先级越高
  3. 选择复制偏移量最大的:优先级相同,则选择复制偏移量(replication offset)最大的从节点(即拥有最完整数据的节点)。
  4. 选择 Run ID 最小的:如果以上都相同,则选择 Run ID 字典序最小的从节点(一个随机策略)。

b) 提升新的主节点

  1. 向选中的从节点发送 SLAVEOF NO ONE 命令,使其停止复制,晋升为独立的主节点。
  2. 每秒发送 INFO 命令检查其角色(role)是否已成功切换为 master

c) 切换并重新配置其他从节点

  1. 向其他所有从节点发送 SLAVEOF <new_master_ip> <new_master_port> 命令,让它们转而复制新的主节点。

  2. 同时,Sentinel 会记住这些旧的从节点配置,当旧主节点重新上线时,会让它也成为新主节点的从节点

d) 通知客户端(Service Discovery)

故障转移完成后,Sentinel 必须通知客户端主节点发生了变化。这是通过 发布/订阅(Pub/Sub) 机制实现的。

  • Sentinel 会向名为 __sentinel__:hello 的频道发布消息,内容包括主节点的配置变化。
  • 客户端需要与 Sentinel 集群保持一个长期命令连接,并订阅这个频道。当收到主节点切换的消息时,客户端就能动态地重新连接到新的主节点。

更常见的做法是:客户端使用 Sentinel 客户端连接池(如 Jedis、Lettuce 都支持)。客户端首先连接到一个或多个 Sentinel,通过发送 SENTINEL get-master-addr-by-name <master-name> 命令来查询当前主节点的地址。故障转移后,客户端再次查询就能拿到新地址。

5.面试回答技巧

面试官:详细说一下Redis的哨兵机制。

你可以这样回答(总分总结构):

“Redis哨兵(Sentinel)是为解决主从模式无法自动故障转移而设计的高可用方案。它是一个分布式系统,主要提供监控、通知、自动故障转移和服务发现四大功能。

它的核心工作流程可以分为四步:

  1. 监控与判断下线:每个Sentinel通过PING命令监控所有节点。当认为主节点主观下线(SDOWN) 后,会咨询其他Sentinel,若达成法定人数(quorum) 则判定为客观下线(ODOWN),这是触发故障转移的前提。
  2. 领导者选举:多个Sentinel会基于Raft算法选举出一个Leader,由它来执行故障转移,避免混乱。
  3. 故障转移:Leader会根据优先级、复制偏移量等规则,从从节点中选出最合适的作为新主节点,并让其他从节点复制它。
  4. 通知客户端:故障转移后,Sentinel通过发布订阅机制或充当配置中心,通知客户端新的主节点地址,完成服务发现。

在生产环境中,我们通常需要部署至少3个Sentinel实例,并且将它们分散在不同的机器上,以保证其自身的可靠性。”

这样的回答,既逻辑清晰,又抓住了核心流程和关键细节(主观/客观下线、Raft选举),能充分展现你的理解深度。

TODO

  1. 脑裂Sentinel 可以防止脑裂吗?(在小林”主从复制是怎么实现的“那里最后有提到脑裂,可结合gpt看看)

3.分片集群Cluster

1.为什么需要分片集群?

主从,和哨兵,都是为了提升Redis的读性能,但所有节点仍然是存储全量数据,加再多从节点,也无法突破单机节点内存瓶颈。且写操作仍然集中在主节点,概括的说,如下:

  1. 海量数据存储:单机 Redis 的内存容量有限(如 16GB, 32GB),无法存储数百 GB 或 TB 级别的数据。
  2. 高并发写操作:Redis 是单线程模型,单个主节点的写能力有上限。虽然主从复制和哨兵提供了高可用读扩展,但写操作仍然集中在单个主节点上,无法扩展。

分片集群通过“分而治之”的思想,将数据拆分成多个分片(Shard),每个分片由一个主节点和多个从节点组成(负责该分片的高可用),从而同时实现了:

  • 数据的分布式存储(解决容量问题)
  • 写操作的负载均衡(解决并发问题)
  • 高可用性(每个分片内部是主从结构)

2.哈希槽

现在的架构是这样的

客户端发送命令——经过CRC16计算哈希槽——由负责该哈希槽的Redis节点处理请求 (每个Redis节点是一个主从集群)

  • 哈希槽的数量:一个 Redis 集群固定有 16384 (2^14) 个哈希槽。你可以把它想象成一个有 16384 个插槽的巨型数组。
  • 数据分片规则
    1. 对每个写入的 Key,使用 CRC16 算法计算出一个哈希值。
    2. 将这个哈希值对 16384 取模,得到一个介于 0 到 16383 之间的数字,这个数字就是该 Key 所属的哈希槽编号。
    3. 集群中的每个主节点(Master)负责处理其中一部分哈希槽

示例
一个三主节点的集群,哈希槽分配可能如下:

  • 节点 A:包含 0 到 5500 号哈希槽。
  • 节点 B:包含 5501 到 11000 号哈希槽。
  • 节点 C:包含 11001 到 16383 号哈希槽。

当你执行 SET user:1000 "John" 时:

  1. 计算 CRC16("user:1000") % 16384,假设得到 7000。
  2. 哈希槽 7000 属于节点 B。
  3. 那么这个 Key 的数据就会被存储到节点 B 上。

用这个哈希槽的好处就是,可以动态扩充Redis节点,然后重新分配哈希槽即可。否则如果计算哈希槽时,是对Redis节点个数n取余,那如果增加节点,n会变化,所有的节点都要进行数据迁移

3.Redis实例上没有数据?—Moved重定向/ASK重定向

Moved重定向:

客户端给一个Redis实例发送数据读写操作时,如果计算出来的槽不是在该节点上, 这时候它会返回MOVED重定向错误,MOVED重定向错误中, 会将哈希槽所在的新实例的IP和port端口带回去。

ASK重定向:

Ask重定向一般发生于集群伸缩的时候。当客户端向源节点请求一个键,而该键已经被迁移到了目标节点时,源节点无法处理这个请求,但它会返回一个 ASK 错误给客户端。

特性 MOVED 重定向 ASK 重定向
含义 永久性重定向。槽的负责权已经完全转移 临时性重定向。槽的负责权正在转移过程中
发生场景 集群稳定状态,客户端请求了错误的节点。 集群数据迁移(Resharding)过程中。
客户端行为 客户端更新本地槽位映射缓存,之后所有对该槽的请求都直接发往新节点。 客户端不更新本地槽位映射缓存,只是临时地对本次请求发往新节点。

4.TODO各个节点之间是如何通信的——Gossip协议

5.TODO故障转移

6.TODO为什么Redis分片集群的哈希槽长度是16384 2^14

7.场景

1.分布式锁

一个最基本的分布式锁需要满足:

  • 互斥:任意一个时刻,锁只能被一个线程持有。
  • 高可用:锁服务是高可用的,当一个锁服务出现问题,能够自动切换到另外一个锁服务。并且,即使客户端的释放锁的代码逻辑出现问题,锁最终一定还是会被释放,不会影响其他线程对共享资源的访问。这一般是通过超时机制实现的。
  • 可重入:一个节点获取了锁之后,还可以再次获取锁。

除了上面这三个基本条件之外,一个好的分布式锁还需要满足下面这些条件:

  • 高性能:获取和释放锁的操作应该快速完成,并且不应该对整个系统的性能造成过大影响。
  • 非阻塞:如果获取不到锁,不能无限期等待,避免对系统正常运行造成影响。

使用Redis命令设计的基础分布式锁

setnx ex,这样可以设置一个简单的分布式锁,setnnx是如果key不存在,则创建key,否则失败;而ex则给这个key设置了过期时间,避免锁无法释放。

而且Redis的命令都是原子性的,这样就保持了获得锁和设置过期时间是一起操作的

对于自己设计的分布式锁,如果要保证判断该锁是否为自己的释放锁这两个操作为原子操作,就需要用Lua脚本。释放锁的时候判断是否与获得锁的线程id一样

Redisson

Redisson 是一个开源的 Java 语言 Redis 客户端,提供了很多开箱即用的功能,不仅仅包括多种分布式锁的实现。

而redisson不仅实现了上述功能,还更为强大,有以下功能:

  1. 自动续期
  2. 可重入
  3. 读写锁
  4. 公平锁

下面介绍redisson原理

加锁

key是锁的名称,value是个map,map的key是线程id,value是锁的重入次数,然后设置锁过期时间

加锁成功

  • 如果加锁成功,锁的重入次数加一,这就实现了重入锁的功能;
    • 加锁成功后,就会执行一个看门狗的机制。看门狗机制是为了防止业务还没执行完,但锁到期了的问题。看门狗就是一个定时任务,只要当前线程任务没有挂掉,且没有主动释放锁,就会隔一段时间给锁续期。默认情况下,每过 10 秒,看门狗就会执行续期操作,将锁的超时时间设置为 30 秒。
  • 如果失败,返回锁的过期时间
    • 加锁失败后,会进入一个循环中,此线程会被semaphore阻塞,当之前的线程释放锁后,会通过semaphore来唤醒此线程,然后获得锁后跳出此循环
  • 释放锁时,如果该锁的线程id不是自己的,就无权释放;如果是,就将重入次数减一,如果减后的重入次数还是>0,就不能释放,更新锁到期时间,否则就释放锁,然后发送锁释放消息,唤醒被阻塞的线程

无论加锁成功或失败,都会有一个future结果器来接收加锁结果

2.如何设计一个秒杀场景

8.面试题

  • redis是什么? Redis架构是怎么样的?怎么设计redis? RDB是什么,AOF是什么?RDB和AOF的区别是什么? Redis有什么作用? Redis为什么要单线程? Redis redis的持久化机制是怎么样的? Redis支持String,List,Set,Zset Redis支持哪些数据类型? Redis是单线程吗? Redis缓存过期策略 Redis缓存淘汰策略 缓存过期策略和缓存淘汰策略的区别是什么? LRU是什么? Redis-cli是什么? Redis通信协议是怎么样的? Redis的协议格式是怎么样的? 为什么Redis不用HTTP? RedisJson是什么? RediSearch是什么? RediTImeSeries是什么? RedisGraph是什么? Redis的高可用,高性能怎么做? Redis是什么? 主从模式是什么?主从模式的具体实现细节? 主从同步中有数据写入怎么办? 经典主从模式有什么问题? 哨兵是什么? 哨兵模式是什么? 哨兵集群是什么? Redis怎么提升高可用? 什么是主观下线和客观下线 Redis内存不足的解决方案 Redis集群模式架构解析 Redis数据切分方法详解 Redis哈希槽工作原理 Redis集群高可扩展性实现 Redis单机崩溃服务不可用问题 Redis集群节点扩容步骤 Redis哈希槽迁移过程 Redis读写重定向机制 Redis CLUSTER SLOTS命令详解 Redis数据迁移期间访问策略 Redis集群主从故障切换流程 Redis集群客户端连接实践 Redis集群与主从复制区别 Redis避免全量数据迁移方法 Redis扩容后数据分布逻辑 Redis哈希槽16384个原因 Redis高可用高扩展实现 Redis突破单机内存限制方案 Redis集群模式数据分片原理 Redis键值分片计算公式 Redis节点增减数据迁移风险 Redis哈希槽固定长度优势 Redis集群节点间通信机制 Redis重定向错误处理方法 Redis集群客户端库推荐 Redis迁移状态判断技巧 Redis集群主节点选举过程 Redis集群读写请求流程示例 Redis集群三主节点配置案例 Redis哈希槽范围分配规则 Redis集群扩容数据迁移量 Redis客户端slot缓存机制 Redis集群节点宕机处理 Redis集群数据持久化策略 Redis集群性能优化技巧 Redis集群监控关键指标 Redis集群常见问题排查 Redis集群槽位重新分配 Redis集群数据一致性保障 Redis集群慢查询优化 Redis集群大Key处理方案 Redis集群热Key解决方案 Redis集群管道技术使用 Redis集群事务支持情况 Redis集群Lua脚本限制 Redis集群如何分片数据 Redis集群节点职责划分 Redis集群数据重定向原理 Redis集群为什么需要16384个槽 Redis集群扩容时数据迁移量 Redis集群客户端如何感知节点变化 Redis集群主节点故障处理流程 Redis集群从节点如何提升为主节点 Redis集群如何保证高可用性 Redis集群数据分布策略详解 Redis键值对如何映射到哈希槽 Redis集群跨槽操作支持情况 Redis数据切分是什么 Redis重定向机制是什么 Redis CLUSTER SLOTS命令是什么 Redis集群节点扩容是什么 Redis哈希槽迁移是什么 Redis主从故障切换是什么 Redis集群高可扩展性是什么 Redis集群高可用性是什么 Redis键值分片计算是什么 Redis集群节点通信是什么 Redis集群跨槽操作是什么 Redis槽位重新分配是什么 Redis客户端Slot缓存是什么

评论