一问一答:如何保证消息不被重复消费?

:如何保证消息不被重复消费?:首先要明确一点,RabbitMQ、RocketMQ、Kafka 这些主流 MQ,都天然存在消息重复投递的可能,这个问题不是 MQ 本身能完全保证的,核心是我们开发者要通过幂等性设计来兜底 —— 说白了,就是哪怕同一条消息被消费了 100 次,最终的业务结果和消费 1 次完全一样,不会出现数据错乱。

我先给你说清楚为什么会出现重复消费,再讲我们线上实际用的几种幂等性解决方案。


一、为什么会出现消息重复消费?

我拿最常用的 Kafka 举个例子,你一下就能明白:Kafka 里的每条消息都有一个唯一的 offset,相当于消息的序号。消费者消费完消息后,会定时把消费过的 offset 提交给 Kafka,告诉它 “这条消息我已经处理完了,下次重启从下一条开始给我发”。但意外总会发生:比如消费者刚处理完 offset=153 的消息,还没来得及提交 offset,进程就被强制 kill、或者服务宕机重启了。重启之后,Kafka 不知道这条消息已经被消费过,就会再次把 offset=153 的消息发给消费者,这条消息就被重复消费了。不止 Kafka,RabbitMQ 也是一样的逻辑:消费者处理完消息,还没给 MQ 发送 ack 确认,服务就宕机了,MQ 会把这条消息重新投递给其他消费者,也会出现重复消费。


二、保证消息不重复消费的核心方案(按场景选,线上直接能用)

重复消费不可怕,可怕的是重复消费后业务数据出错。我们线上会根据不同的业务场景,选对应的幂等性方案,主要有这 4 种,从简单到复杂给你讲:

  1. 数据库唯一键约束(最简单、最常用)这个方案适合往数据库插入数据的场景,也是我们用的最多的。生产者发送消息的时候,必须带上全局唯一的业务 ID,比如订单号、流水号、支付单号。我们消费消息往数据库插数据的时候,就用这个业务 ID 做表的主键,或者给它建唯一索引。这样一来,同一条消息重复过来,插入的时候数据库会因为唯一键冲突直接报错,不会插入重复数据,天然就保证了幂等性,哪怕重复消费 100 次,数据库里也只会有一条数据。
  2. 先查后判,标记消费状态(通用万能方案)这个方案适合所有业务场景,通用性最强。核心逻辑:消费消息的时候,先拿消息里的唯一业务 ID,去 Redis 或者数据库里查一下,这条消息有没有被消费过。
    • 如果查不到,说明是第一次消费,就正常执行业务逻辑,执行完之后,把这个唯一 ID 存到 Redis / 数据库里,标记为 “已消费”;
    • 如果能查到,说明这条消息已经被处理过了,直接跳过不处理,就不会出现重复消费的问题。我们线上订单状态同步的场景,就是用这个方案,用订单号做唯一键,Redis 里存消费状态,性能和可靠性都能保证。
  1. 天然幂等的操作,不用额外处理有些操作本身就是幂等的,不管执行多少次,结果都完全一样,这种场景不用做额外的防重处理。最典型的就是 Redis 的 set 操作,还有数据库的 update 操作,比如update order set status=1 where order_id=xxx,哪怕执行 100 次,订单的状态最终还是 1,不会有任何问题,天然就防重复消费。
  2. 分布式锁(高并发场景专用)高并发、大流量的场景,我们会用分布式锁来保证幂等性。消费消息的时候,用消息的唯一业务 ID 去抢 Redis 分布式锁,只有抢到锁的消费者,才能执行业务逻辑,执行完再释放锁。重复的消息过来的时候,抢不到锁,就没法执行业务逻辑,自然就不会出现重复消费的问题。

三、线上实际落地的小细节

我们线上所有的 MQ 消息,都会强制要求生产者带上全局唯一的业务 ID,不会用 MQ 自带的 messageId,因为不同 MQ 的 messageId 可能重复,业务 ID 是我们自己控制的,绝对唯一,这是所有防重方案的基础。而且我们不会只靠一种方案兜底,比如核心的支付场景,我们会同时用「数据库唯一键」+「先查后判标记消费状态」双保险,哪怕其中一层出问题,另一层也能保证数据不会出错。

一问一答:如何处理消息队列的消息丢失问题?

:如何处理消息队列的消息丢失问题?:消息丢失只会发生在三个核心环节:生产者发送消息到 MQ 的过程、MQ 服务端存储消息的过程、消费者消费消息的过程,主流的 RabbitMQ 和 Kafka 都遵循这个逻辑,我分别给你讲清楚每个环节丢消息的原因、对应的解决方案,还有我们线上的实际落地配置。


一、RabbitMQ 的消息丢失解决方案

1. 生产者环节:消息还没发到 MQ 就丢了
  • 丢消息原因:生产者发送消息时,出现网络波动、MQ 节点宕机,消息在半路就丢了,但生产者完全感知不到。
  • 解决方案:优先开启confirm 异步确认模式,这是线上生产环境的标准方案。开启这个模式后,每条消息会被分配一个唯一 ID,消息成功写入 RabbitMQ 后,MQ 会给生产者回传 ack 确认;如果消息写入失败,会回调 nack 接口通知生产者,我们可以在回调里做消息重试。它是异步非阻塞的,发送完一条消息不用等确认就能发下一条,对吞吐量的影响极小。
  • 补充:RabbitMQ 也提供了同步事务机制,但它是阻塞的,开启后吞吐量会暴跌,只适合并发极低、对消息可靠性要求极致的场景,线上基本不用。
2. MQ 服务端环节:消息到了 MQ,宕机后就丢了
  • 丢消息原因:RabbitMQ 收到了消息,但消息还在内存里,没来得及刷到磁盘,MQ 就宕机重启了,内存里的消息直接丢失。
  • 解决方案:开启消息持久化,必须同时完成两个配置,缺一不可:
    1. 创建 queue 队列的时候,设置durable=true,开启队列持久化,保证 MQ 重启后,队列的元数据不会丢失;
    2. 发送消息的时候,设置deliveryMode=2,把消息标记为持久化,RabbitMQ 会把这条消息刷到磁盘上。
  • 进阶优化:把持久化和生产者的 confirm 模式配合使用,只有消息被成功持久化到磁盘之后,RabbitMQ 才给生产者回 ack 确认。哪怕持久化之前 MQ 宕机,生产者收不到 ack,也会自动重试,绝对不会丢消息。
3. 消费者环节:消息到了消费者,还没处理就丢了
  • 丢消息原因:RabbitMQ 默认开启自动 ack,消费者刚拉到消息,还没处理完业务逻辑,服务就宕机了,但 MQ 已经收到了自动 ack,会认为这条消息已经处理完成,不会再重新投递,消息就直接丢了。
  • 解决方案关闭自动 ack,改成手动 ack。只有业务逻辑完全处理完成后,才在代码里手动调用 ack 方法,通知 RabbitMQ 消息处理完成;如果消息没处理完服务就宕机了,MQ 没收到 ack,会把这条消息重新投递给其他消费者,保证消息不会丢失。
  • 补充:手动 ack 会带来消息重复投递的问题,所以必须配合幂等性设计兜底,避免重复消费导致数据错乱。

二、Kafka 的消息丢失解决方案

1. 消费者环节:消息拉到了,还没处理就丢了
  • 丢消息原因:Kafka 消费者默认开启 offset 自动提交,刚拉到消息,还没执行业务逻辑,offset 就自动提交给 Kafka 了。此时消费者宕机重启,Kafka 会认为这条消息已经处理完成,从新的 offset 开始投递,这条没处理的消息就直接丢了。
  • 解决方案关闭 offset 自动提交,改成业务处理完成后手动提交 offset。只有业务逻辑完全执行成功,才手动提交 offset;如果没处理完就宕机,offset 没提交,重启后 Kafka 会重新投递这条消息,保证不丢。同样要配合幂等性设计,解决重复消费的问题。
2. Kafka 服务端环节:broker 宕机导致消息丢失
  • 丢消息原因:某个 broker 节点宕机,上面 partition 的 leader 挂了,但此时 follower 副本还没同步完所有消息,选举新 leader 后,没同步的消息就直接丢失了。
  • 解决方案:线上必须配置这 4 个核心参数,从架构上保证不丢消息:
    1. replication.factor > 1:给每个 topic 设置至少 2 个副本,且副本必须分散在不同的 broker 节点上,避免单节点故障丢失所有副本;
    2. min.insync.replicas > 1:要求 leader 至少感知到 1 个 follower 和自己保持数据同步,保证 leader 挂了之后,有同步完成的副本能顶上,不会丢数据;
    3. acks=all:生产者发送的消息,必须所有同步的副本都写入成功,才认为消息发送成功,保证消息不会因为单节点宕机丢失;
    4. retries=MAX:设置消息发送失败后无限重试,不会因为临时网络波动、broker 选主导致消息丢失。
3. 生产者环节:消息没发到 broker 就丢了
  • 丢消息原因:网络波动、broker 临时不可用,消息没发到 Kafka 集群就丢了,生产者没感知。
  • 解决方案:配合上面的acks=all+ 无限重试配置,只要消息没成功写入所有同步副本,生产者就会一直重试,不会出现半路丢消息的情况。

我们线上的实际落地配置

  1. 金融支付核心场景用的 RabbitMQ,我们开启了生产者 confirm 模式 + 全量消息持久化 + 消费者手动 ack,核心链路还加了消息落库、定时对账的兜底方案,线上运行 3 年,没有出现过消息丢失的问题;
  2. 日志采集、实时数据计算场景用的 Kafka,我们严格按照上面 4 个高可靠参数配置,哪怕出现 broker 节点宕机,也不会出现消息丢失、服务不可用的情况。

一问一答:如何保证消息队列的消息顺序性?

:如何保证消息队列的消息顺序性?:首先要明确,消息顺序性不是所有场景都需要,只有强业务顺序的场景才需要严格保证 —— 比如订单的创建→支付→发货→完成的状态流转,必须按顺序处理,不然会出现 “还没支付就发货” 的严重问题;还有用户积分的加、减操作,顺序错了会出现负积分的资损问题。

接下来我先讲清楚消息顺序为什么会错乱,再分别给你讲 RabbitMQ 和 Kafka 的解决方案,还有我们线上实际用的、兼顾顺序和吞吐量的落地方案。


一、先搞清楚:消息顺序为什么会错乱?

不管是 RabbitMQ 还是 Kafka,顺序错乱的核心逻辑都是一样的:消息发送的时候是有序的,但消费的时候被并行处理了,导致先到的消息后处理完,顺序就乱了

  1. RabbitMQ 的顺序错乱场景生产者按顺序发了 data1、data2、data3 三条消息到同一个 queue 里,但如果这个 queue 绑定了多个消费者,或者一个消费者用了多线程消费,就会出现:data2 先被处理完入库,data1 和 data3 后处理,顺序直接乱了。
  2. Kafka 的顺序错乱场景Kafka 本身在 partition 层面是天然有序的:我们给消息指定一个业务 key(比如订单号),相同 key 的消息一定会被发到同一个 partition 里,partition 里的消息是严格按发送顺序存储的,消费者从 partition 拉消息也是按顺序拉的。但顺序会在消费环节乱掉:如果消费者拉到消息后,用多线程并发处理,就会出现先拉到的消息后处理完,顺序就乱了。

二、保证消息顺序性的解决方案

1. RabbitMQ 的解决方案

我分三种场景给你讲,从简单到兼顾性能,覆盖不同的业务需求:

  • 方案 1:单 queue 单消费者,内部单线程消费(最简单,低并发场景适用)一个 queue 只绑定一个消费者,消费者内部只用单线程处理消息,完全保证先到的消息先处理,绝对不会乱序。但缺点也很明显,单线程吞吐量太低,高并发场景扛不住,只适合并发量小的业务。
  • 方案 2:按业务维度拆分 queue,单 queue 单消费者(线上常用,兼顾顺序和吞吐量)这是我们线上核心业务用的方案:把需要保证顺序的消息,按业务维度拆分到不同的 queue 里 —— 比如同一个订单的所有状态变更消息,都发到这个订单专属的 queue 里,每个 queue 只对应一个消费者。这样既保证了同一个订单的消息严格按顺序处理,又能通过多个 queue 并行消费,提升整体吞吐量,是平衡了可靠性和性能的最优解。
  • 方案 3:单 queue 单消费者,内部内存队列做分发(高并发场景专用)一个 queue 对应一个消费者,消费者拉到消息后,先按订单号这类业务 key,把消息分发到不同的内存队列里,每个内存队列对应一个工作线程处理。这样相同业务 key 的消息一定会被同一个线程按顺序处理,既保证了顺序,又通过多线程提升了吞吐量,适合超高并发的业务场景。
2. Kafka 的解决方案

Kafka 的顺序性核心是保证同一个业务 key 的消息顺序,我们线上也是围绕这个做的,业内通用的标准方案有两个:

  • 方案 1:单 topic 单 partition,单消费者单线程消费(不推荐)这个方案能保证绝对顺序,但吞吐量极低,完全发挥不了 Kafka 分布式的优势,线上基本不用。
  • 方案 2:按业务 key 做 hash 分发,消费者内部按 key 做内存队列隔离(线上生产标准方案)这是我们线上大数据场景用的方案,分两步实现:
    1. 发送端:给消息指定业务 key(比如订单号、用户 ID),Kafka 会按 key 的 hash 值,把同一个业务的所有消息都发到同一个 partition 里,保证 partition 内的消息是严格有序的;
    2. 消费端:消费者拉到消息后,不直接多线程处理,而是按业务 key 把消息分发到不同的内存队列,每个内存队列对应一个工作线程,相同 key 的消息一定会被同一个线程按顺序处理。这个方案既利用了 Kafka 的分布式能力提升吞吐量,又完全保证了同一个业务的消息顺序,是业内通用的最佳实践。

三、线上落地的兜底保障

我们线上做了双保险兜底:哪怕用了上面的方案,还会在业务层做顺序校验 —— 比如处理订单状态变更消息的时候,会先判断当前消息的状态版本,是不是比数据库里的版本新,如果是旧版本的消息,直接跳过不处理。哪怕出现极端的顺序错乱,也不会导致业务数据出错,完全避免资损问题。

一问一答:为什么要使用缓存?用了缓存会带来什么问题?

:你们的项目里为什么要使用缓存?用了缓存之后会带来什么不良后果?:我们在项目里引入缓存,核心是为了解决两个核心业务痛点:提升系统性能、支撑更高的并发量,同时我也非常清楚引入缓存会带来一系列的问题,我们线上也针对这些问题做了完整的兜底解决方案。


一、使用缓存的核心价值
1. 极致提升系统性能

我们的电商商品详情页场景,就是最典型的例子:用户查询商品详情,需要关联查询商品基础信息、库存、规格、营销活动等多个表,还要做数据聚合,一次复杂查询 MySQL 要耗时 600ms 左右。但商品详情的核心信息,几个小时内都不会有变化,而且 99% 都是读请求。我们就把查询出来的结果,按商品 ID 作为 key 放到 Redis 缓存里,下次用户再查同一个商品,直接从缓存里拿结果,只需要 2ms 就能返回,性能直接提升了 300 倍,用户体验也有了质的飞跃。简单来说,对于那些复杂耗时查询出来的、变化频率低、读请求多的数据,用缓存能直接把接口响应速度拉满。

2. 支撑超高并发流量

MySQL 这类关系型数据库,天生就不是为高并发设计的,单机 MySQL 能稳定支撑 2000QPS 就已经接近极限了,再高就会出现慢查询、连接打满,甚至直接宕机。但我们大促高峰期,商品详情页的查询请求一秒就有上万,全打在 MySQL 上服务直接就崩了。这个时候缓存就成了核心的流量屏障:Redis 这类缓存是纯内存的 key-value 操作,单机轻松就能扛住每秒十几万的请求,并发承载能力是 MySQL 的几十倍。我们把绝大多数读请求都用缓存扛住,MySQL 只处理写请求和极少量缓存未命中的读请求,就能轻松支撑住大促的峰值流量,保证服务不宕机。


二、引入缓存带来的不良后果 & 我们的解决方案

缓存不是银弹,引入它也会带来一系列的问题,这些问题如果处理不好,反而会给系统带来灾难,我们线上踩过坑,也有成熟的解决方案:

1. 缓存与数据库双写数据不一致

这是最常见的问题:更新数据库的时候,缓存和数据库的操作顺序不对,就会出现数据库里是新数据,缓存里还是旧数据,用户读到脏数据的情况。我们线上的解决方案是采用更新数据库 + 延迟删除缓存的策略:先更新数据库,成功后延迟几百毫秒再删除缓存,同时给缓存设置合理的过期时间,哪怕出现极端的不一致情况,缓存过期后也能自动拉取最新数据,保证最终一致性。

2. 缓存雪崩

缓存雪崩指的是:大量缓存同时过期失效,或者缓存集群整体宕机,导致所有请求瞬间全部打到数据库上,直接把数据库打垮,引发服务雪崩。我们的解决方案:

  • 给不同 key 的过期时间加随机值,避免大量 key 同时过期;
  • 搭建 Redis 主从 + 哨兵集群,做异地容灾,避免缓存集群整体宕机;
  • 做服务熔断和降级,缓存异常时,降级返回兜底数据,避免所有请求都打到数据库。
3. 缓存穿透

缓存穿透指的是:用户大量查询数据库里根本不存在的数据,这些数据在缓存里也没有,每次请求都会穿透缓存直接打到数据库上,把数据库拖垮。比如黑客用不存在的商品 ID 疯狂请求接口,就会出现这个问题。我们的解决方案:

  • 对查询为空的结果,也在缓存里设置一个短时间的过期时间,避免重复查询空数据;
  • 用布隆过滤器,提前把所有合法的商品 ID 存到布隆过滤器里,请求过来先查布隆过滤器,不存在的 ID 直接拦截,根本不会走到缓存和数据库层。
4. 缓存击穿

缓存击穿指的是:某个热点 key 过期失效的瞬间,刚好有大量并发请求过来,这些请求都会直接打到数据库上,把数据库打垮。比如爆款商品的缓存过期,瞬间几万请求过来,就会出现这个问题。我们的解决方案:

  • 对热点 key 设置永不过期,后台异步更新缓存;
  • 用分布式互斥锁,缓存失效的时候,只有一个线程能去查数据库更新缓存,其他线程等待缓存更新完成,避免大量请求同时打到数据库。
5. 缓存并发竞争

高并发场景下,多个线程同时更新同一个 key,会出现更新顺序错乱,导致缓存里的数据是错的。我们的解决方案:

  • 用分布式锁保证更新操作的串行化;
  • 给缓存数据加版本号,更新的时候校验版本号,避免旧版本覆盖新版本的数据。

一问一答:Redis 的线程模型 & 单线程高性能原因

:Redis 的线程模型是什么?为什么 Redis 单线程却能支撑超高并发?:Redis 的核心线程模型是单线程的文件事件处理器模型,我们常说 Redis 是单线程,核心指的是它的命令执行、内存数据操作全是单线程完成的;但它能轻松支撑每秒十几万的并发请求,核心是靠「IO 多路复用 + 纯内存操作」的架构设计。我先给你讲清楚它的线程模型结构,再讲它单线程却能高性能运行的原因。


一、Redis 的单线程文件事件处理器模型

这个模型是 Redis 线程模型的核心,它由 4 个核心部分组成,全程单线程处理所有事件:

  1. 多个 socket:就是客户端和 Redis 服务端建立的网络连接,每个客户端连接对应一个 socket,连接的建立、读、写都会产生对应的事件。
  2. IO 多路复用程序:它会同时监听所有 socket,不管有多少客户端连接,都能一次性监听到所有产生事件的 socket,然后把这些有事件的 socket 压到一个内存队列里,排队交给事件分派器,保证事件串行处理,不会乱序。
  3. 文件事件分派器:从队列里按顺序取出 socket,根据 socket 上的事件类型(比如连接建立、读请求、写请求),分发给对应的事件处理器处理。
  4. 事件处理器:一共有三类,分别处理不同的事件:
    • 连接应答处理器:处理客户端的建连请求,和客户端完成 TCP 三次握手,建立连接;
    • 命令请求处理器:读取客户端发送的 Redis 命令(比如 set/get),在内存里完成数据操作;
    • 命令回复处理器:把命令执行的结果,写回给对应的客户端。
完整的客户端和 Redis 通信流程,你一下就能懂
  1. 客户端发起建连请求,服务端的监听 socket 产生读事件,IO 多路复用程序监听到后,把这个 socket 丢到队列里,事件分派器取出来交给连接应答处理器,完成连接建立,同时把这个新连接的读事件和命令请求处理器绑定。
  2. 客户端发送set key value命令,这个连接的 socket 产生读事件,IO 多路复用程序把它丢到队列里,事件分派器交给命令请求处理器,处理器读取命令,在内存里完成 key-value 的写入,同时把这个 socket 的写事件和命令回复处理器绑定。
  3. 客户端准备接收返回结果,socket 产生写事件,事件分派器交给命令回复处理器,把ok结果写回给客户端,完成一次完整的请求,全程都是单线程串行处理。

二、为什么 Redis 单线程模型,效率还能这么高?

很多人会疑惑,单线程怎么能比多线程还快?核心有 4 个原因,每一个都踩中了高性能的关键点:

  1. 纯内存操作Redis 所有的数据都存在内存里,所有命令操作都是纯内存读写,内存操作的速度是纳秒级的,比磁盘 IO 快了几十万倍,这是它高性能的基础。
  2. 基于非阻塞的 IO 多路复用机制用 IO 多路复用程序,单线程就能同时监听上万的客户端连接,只处理有事件发生的 socket,不会在空闲连接上浪费资源,能高效承接超高的并发连接,不会因为连接多了就阻塞。
  3. C 语言实现C 语言是更贴近操作系统的底层语言,执行效率更高,同样的逻辑,C 语言实现的程序运行速度远高于其他高级语言。
  4. 单线程反而规避了多线程的性能损耗多线程编程会带来频繁的线程上下文切换,还有锁竞争带来的性能开销,甚至会出现死锁问题。Redis 用单线程,完全避免了这些问题,不用加锁、不用切换线程,所有时间都用来执行命令,反而把 CPU 的利用率拉到了最高。

补充加分项:Redis 6.0 的多线程优化

这里要特别说明,我们说 Redis 是单线程,指的是核心命令执行是单线程,Redis 6.0 之后引入了多线程,只是用来处理网络 IO 的读写、数据的解析和序列化,真正的命令执行还是单线程串行的,完全没有改变它单线程模型的本质,也不会出现线程安全问题,只是进一步提升了高并发下的网络 IO 处理能力。

一问一答:Redis 的核心数据类型 & 适用场景

:Redis 都有哪些核心数据类型?分别适合什么业务场景?:Redis 有 5 种核心的基础数据类型,分别是string 字符串、hash 哈希、list 列表、set 集合、sorted set 有序集合(也叫 zset),每种类型都对应不同的业务场景,我分别给你讲清楚它们的特点、用法和我们线上实际的使用场景。


1. string(字符串)

这是 Redis 最基础、最常用的类型,就是简单的 key-value 键值对,value 不仅可以存字符串,也能存数字、二进制数据,单个 value 最大支持 512MB。

  • 常用命令:set/get(存 / 取)、incr/decr(原子递增 / 递减)、incrby/decrby(按步长增减)
  • 核心业务场景:
    1. 基础 KV 缓存:比如缓存商品详情、用户基础信息、用户登录的 token 令牌,是我们用的最多的场景;
    2. 计数器场景:比如文章的阅读量、点赞数、接口限流的计数,用incr命令天然单线程原子性,完全不用担心并发计数错乱的问题。

2. hash(哈希)

这个类型类似 Java 里的 HashMap,一个大 key 下面挂着多个 field-value 小键值对,特别适合存结构化的对象数据。

  • 常用命令:hset/hget(给对象的单个字段存 / 取值)、hmset/hmget(批量操作多个字段)
  • 核心业务场景:存储结构化的对象,比如用户信息、商品信息。比如用user:1001作为 key,下面的name/age/id作为 field,分别存对应用户的属性。最大的好处是可以单独修改对象的某个字段,不用把整个对象全量更新,既节省了网络开销,也不用频繁做序列化和反序列化,性能比用 string 存整个序列化对象好很多。

3. list(列表)

它是有序的字符串列表,底层是双向链表实现的,在列表的头尾插入、删除元素的时间复杂度是 O (1),速度极快,还支持按索引范围查询元素。

  • 常用命令:lpush/rpush(从列表头 / 尾加元素)、lpop/rpop(从列表头 / 尾取元素)、lrange(按范围查询列表元素)
  • 核心业务场景:
    1. 有序列表类场景:比如文章的评论列表、用户的粉丝列表、APP 的消息流,天然保证插入的顺序;
    2. 高性能分页:用lrange命令实现简单的分页,比如微博的下拉加载更多,纯内存操作性能极高;
    3. 简单消息队列:用lpush从队头放消息,rpop从队尾取消息,实现先进先出的简易队列,适合轻量的异步任务场景。

4. set(集合)

它是无序的字符串集合,天然支持自动去重,还支持多个集合之间的交集、并集、差集操作,单个元素的查询、添加、删除都是 O (1) 的时间复杂度。

  • 常用命令:sadd(添加元素)、sismember(判断元素是否存在)、scard(获取集合元素数量)、sinter(交集)、sunion(并集)、sdiff(差集)
  • 核心业务场景:
    1. 全局去重场景:比如用户签到去重、文章点赞去重,尤其是分布式多机器部署的系统,用 Redis 的 set 做全局去重,比本地的 HashSet 方便太多;
    2. 社交关系场景:比如计算两个用户的共同关注、共同好友(用交集)、给用户推荐可能认识的人(用差集),是社交类业务的神器。

5. sorted set(有序集合,zset)

它在 set 的基础上,给每个元素加了一个score分数值,Redis 会自动根据 score 给元素排序,既保证了元素的唯一性,又实现了排序,还支持按分数范围、排名查询。

  • 常用命令:zadd(添加带分数的元素)、zrevrange(按分数从高到低取排名范围内的元素)、zrank(获取元素的排名)、zincrby(原子增减元素的分数)
  • 核心业务场景:
    1. 各类排行榜场景:比如商品销量榜、文章热度榜、游戏积分排行榜,用 score 存对应的分数,直接就能取排名前 10 的元素,不用我们自己在业务代码里排序,性能极高;
    2. 带权重的优先级队列:用 score 存任务的优先级,高优先级的任务分数更高,会被优先取出执行,适合调度类的业务场景。

一问一答:Redis 的过期策略 & 内存淘汰机制

:Redis 的 key 过期策略有哪些?过期的 key 是怎么被清理的?如果大量过期 key 堆积在内存里,Redis 会怎么处理?:Redis 针对过期 key 的清理,核心采用的是定期删除 + 惰性删除的组合策略;如果这两个策略没清理干净的过期 key 导致内存占满,就会触发内存淘汰机制来兜底,我分别给你讲清楚每个部分的原理和我们线上的实践。


一、Redis 的过期 key 清理策略

Redis 不会全量扫描所有过期 key 做清理,因为它是单线程模型,全量扫描会导致正常命令执行被阻塞,引发性能灾难,所以用了「主动 + 被动」结合的平衡方案。

1. 定期删除(主动清理)

这是 Redis 主动清理过期 key 的核心策略,在清理效率和服务性能之间做了平衡。

  • 执行逻辑:Redis 默认每隔 100ms,会从所有设置了过期时间的 key 里,随机抽取 20 个 key,检查是否过期,过期就直接删除;
  • 循环触发规则:如果本轮抽取的 key 里,超过 25% 都是过期的,就会重复抽取、检查、删除的流程,直到过期 key 的占比低于 25%,才停止本轮清理,等待下一轮周期;
  • 设计原因:哪怕 Redis 里有上百万个带过期时间的 key,随机抽样的方式也不会占用太多 CPU 时间,不会阻塞正常的业务命令执行。
2. 惰性删除(被动兜底清理)

作为定期删除的补充,解决定期删除没扫描到的过期 key 问题。

  • 执行逻辑:当用户访问某个 key 时,Redis 会先检查这个 key 是否设置了过期时间、是否已经过期;如果已经过期,就直接删除这个 key,不会给用户返回任何内容;
  • 设计原因:定期删除会漏掉很多没被抽到的过期 key,惰性删除能保证用户永远访问不到过期的 key,同时顺便完成清理。
组合策略的遗留问题

这两个策略结合后,还是会有漏洞:如果一个过期 key 既没被定期删除抽到,也一直没被用户访问,就会一直堆积在内存里,慢慢占满 Redis 的内存。这个时候,就需要内存淘汰机制来兜底处理。


二、Redis 的内存淘汰机制

内存淘汰机制,就是当 Redis 的内存使用达到我们设置的maxmemory上限时,Redis 会按照指定规则淘汰一部分 key 释放内存,保证新的写入命令能正常执行。Redis 一共有 8 种内存淘汰策略,我给你讲清楚每一种的逻辑、适用场景,还有我们线上的选择:

表格

淘汰策略

核心逻辑

适用场景

noeviction

内存满了之后,新的写入命令直接报错,不淘汰任何 key

仅适合绝对不允许数据丢失的特殊场景,线上基本不用

allkeys-lru

从所有 key 里,淘汰最近最少使用的 key

线上最常用的策略,适合绝大多数缓存场景,优先保留热点数据

allkeys-random

从所有 key 里,随机淘汰一部分 key

线上基本不用,随机淘汰无法保证热点数据留存

allkeys-lfu

从所有 key 里,淘汰使用频率最低的 key

Redis 4.0 新增,适合需要长期保留高频访问数据的场景

volatile-lru

只从设置了过期时间的 key 里,淘汰最近最少使用的 key

适合需要保证永久数据不被淘汰、仅清理过期缓存的场景

volatile-random

只从设置了过期时间的 key 里,随机淘汰一部分 key

极少使用

volatile-ttl

只从设置了过期时间的 key 里,优先淘汰最早要过期的 key

适合需要优先清理快过期数据的场景

volatile-lfu

只从设置了过期时间的 key 里,淘汰使用频率最低的 key

Redis 4.0 新增,适合带过期时间的高频数据场景

我们线上的实践配置

我们线上核心业务的缓存集群,用的是allkeys-lru策略,同时把maxmemory设置成服务器内存的 60%,预留足够内存给系统和 Redis 的其他操作,避免 OOM。选择这个策略的原因是,我们的业务绝大多数是热点数据缓存,用 LRU 能保证最近被访问的商品、用户数据被保留,淘汰长期没人访问的冷数据,完全匹配我们的业务需求。

一问一答:如何保证 Redis 的高并发和高可用?

:如何保证 Redis 的高并发和高可用?:我们线上的 Redis 集群,是通过主从架构 + 读写分离来支撑超高并发,通过哨兵(Sentinel)集群实现自动故障转移来保证高可用,同时配合主从复制的核心机制保证数据一致性,最终实现了 99.99% 的服务可用性,轻松支撑大促峰值的读请求。我分别给你讲清楚高并发和高可用的实现原理,还有我们线上的实践方案。


一、如何保证 Redis 的高并发?

Redis 单机的 QPS 极限大概在几万,对于电商大促这种每秒十几万的读请求,单机完全扛不住,我们是通过主从架构 + 读写分离 + 水平扩容来支撑高并发的。

1. 核心架构:一主多从 + 读写分离

我们采用的是一主多从的架构,核心规则是:

  • 主节点(master):只负责处理写请求,以及少量核心的读请求;
  • 从节点(slave):只负责处理读请求,主节点会把写操作的数据异步复制到所有从节点,保证主从数据一致。

这样做的好处是:绝大多数的读请求都被分摊到了多个从节点上,我们可以通过增加从节点的数量,无限水平扩容读的并发能力,轻松支撑大促的峰值读流量。比如我们线上核心商品缓存集群,一主三从的架构,就能轻松支撑每秒 10 万 + 的读请求。

2. 数据同步的核心:主从复制机制

主从之间的数据同步,是靠 Redis 的 replication 复制机制实现的,核心分为全量复制和增量复制两种场景:

  1. 全量复制(首次建连时触发)从节点第一次启动连接主节点时,会发送PSYNC命令给主节点,触发全量同步:
    • 主节点执行bgsave,在后台生成 RDB 快照文件,同时把新收到的写命令缓存到内存里;
    • 主节点把 RDB 文件发给从节点,从节点清空本地数据,加载 RDB 文件到内存;
    • 主节点再把缓存的写命令发给从节点,从节点执行这些命令,完成全量同步,后续就只同步增量的写命令了。
  1. 增量复制(网络断连重连时触发)Redis 2.8 之后支持断点续传的增量复制,解决了断连后重新全量复制的性能问题:
    • 主节点会在内存里维护一个复制积压缓冲区 backlog,主从都会记录当前的复制偏移量 offset 和主节点的运行 id;
    • 如果主从网络断连重连,从节点会把自己的 offset 发给主节点,主节点会从 backlog 里,把 offset 之后的增量命令发给从节点,只同步缺失的部分,不用重新全量复制,大幅提升了同步效率。
3. 主从复制的核心优化 & 注意事项
  • 我们线上开启了无磁盘化复制,主节点生成 RDB 时直接在内存里创建,通过网络发给从节点,不用落地到本地磁盘,避免了磁盘 IO 的性能瓶颈;
  • 强制要求主节点必须开启 RDB+AOF 持久化,避免主节点宕机重启后,本地数据为空,导致所有从节点同步空数据,把整个集群的数据清空;
  • 主节点过期、淘汰 key 时,会模拟一条del命令发给从节点,保证主从的过期 key 处理完全一致,不会出现数据不一致的问题。

二、如何保证 Redis 的高可用?

主从架构只能解决高并发的问题,但如果主节点宕机了,整个集群就没法处理写请求了,服务就不可用了。我们是通过哨兵(Sentinel)集群,实现自动故障转移(主备自动切换),来保证 Redis 的高可用。

1. 哨兵集群的核心作用

哨兵是 Redis 官方提供的高可用组件,我们线上部署了 3 个哨兵节点组成集群,它的核心作用有 4 个:

  1. 监控:哨兵会持续监控主节点和所有从节点的运行状态,判断节点是否存活;
  2. 选主:当主节点故障宕机时,哨兵会按照规则,从健康的从节点里选举出一个新的主节点;
  3. 故障转移:选举完成后,哨兵会让其他从节点同步新的主节点,同时通知客户端新的主节点地址,整个过程完全自动,不用人工干预;
  4. 配置通知:主备切换完成后,哨兵会更新集群的配置,保证所有节点的配置一致。
2. 故障自动转移的完整流程
  1. 当主节点宕机,超过哨兵配置的超时时间没有响应,哨兵会标记主节点为「主观下线」;
  2. 多个哨兵节点投票确认,超过半数的哨兵都认为主节点故障,就会把主节点标记为「客观下线」,触发故障转移;
  3. 哨兵集群会选举出一个领头哨兵,负责从健康的从节点里,选出一个最优的节点升级为新的主节点;
  4. 领头哨兵让其他所有从节点,同步新主节点的数据,同时把新主节点的地址通知给客户端,客户端自动切换连接到新主节点;
  5. 原来的故障主节点恢复后,会自动变成新主节点的从节点,整个故障转移流程完成。
3. 我们线上的高可用实践

我们线上的核心业务集群,采用的是一主三从 + 3 节点哨兵集群的架构:

  • 哨兵集群部署在不同的物理机器上,避免单台机器宕机导致哨兵集群不可用;
  • 配置了合理的故障检测超时时间,避免网络波动导致的误切换,同时保证主节点故障后,30 秒内就能完成自动主备切换;
  • 配合主节点的持久化、数据备份,哪怕出现极端的集群故障,也能通过 RDB 备份快速恢复数据,完全不会丢失业务数据。

总结

我们通过主从架构 + 读写分离实现了读请求的水平扩容,支撑了超高并发;通过哨兵集群实现了主节点故障的自动转移,保证了服务的持续可用;同时通过主从复制的优化、持久化配置,保证了数据的一致性和安全性,完全满足线上业务的高并发、高可用要求。

一问一答:Redis 哨兵集群的高可用实现原理

:Redis 的哨兵(Sentinel)集群是什么?它是如何实现 Redis 高可用的?:Redis 的哨兵(Sentinel)是官方提供的高可用核心组件,专门解决主从架构下 master 节点的单点故障问题,通过自动故障转移(主备自动切换),实现 Redis 集群的持续可用 —— 哪怕 master 节点宕机,也能在几十秒内完成自动化切换,业务几乎无感知。我会从哨兵的核心功能、集群部署原则、故障转移完整流程,还有我们线上的最佳实践,给你讲清楚它的实现原理。


一、哨兵的四大核心功能

这是哨兵实现高可用的基础,四个功能环环相扣,覆盖了从故障检测到故障恢复的全流程:

  1. 集群监控:哨兵会周期性地持续监控集群里所有 master 主节点、slave 从节点的运行状态,判断节点是否存活、服务是否正常,这是故障检测的基础。
  2. 消息通知(告警):如果监控到某个 Redis 实例出现故障,哨兵会立刻发送告警通知给管理员,同时把故障事件同步给集群里的其他哨兵节点。
  3. 自动故障转移:这是哨兵最核心的能力。当 master 主节点故障宕机时,哨兵会自动从健康的 slave 从节点里,选举出一个最优节点升级为新的 master,再让其他所有从节点同步新 master 的数据,整个过程完全自动化,无需人工干预。
  4. 配置中心:故障转移完成后,哨兵会把新的 master 节点地址同步给所有业务客户端,客户端会自动切换连接到新 master,无需手动修改业务代码的配置。

同时要注意,哨兵本身是分布式运行的,必须以集群的方式部署,多个哨兵节点协同工作,避免哨兵自身成为单点故障。


二、哨兵集群的核心部署原则

这是面试的高频考点,也是线上踩坑的重灾区:哨兵集群至少要部署 3 个节点,且必须分散在不同的物理机器上,绝对不能只部署 2 个哨兵节点。

这里涉及到两个核心概念,也是哨兵工作的核心规则:

  • quorum(法定票数):要判定 master 节点正式故障(客观下线),需要至少 quorum 个哨兵节点统一意见;
  • majority(大多数):要成功执行故障转移、选举出负责操作的领头哨兵,必须有超过半数的哨兵节点存活且正常运行。
为什么 2 个哨兵节点是线上禁忌?

如果只部署 2 个哨兵节点,quorum 一般设为 1,majority 固定为 2。

  • 如果 master 宕机,只要 1 个哨兵认为它故障,就能触发客观下线;
  • 但如果 master 和其中一个哨兵所在的机器一起宕机,此时只剩 1 个哨兵存活,达不到 majority=2 的要求,就无法执行故障转移,整个集群会彻底失去写能力,完全不可用。
为什么 3 节点是线上经典最佳实践?

我们线上核心业务用的就是 3 节点哨兵集群,quorum 设为 2,majority 固定为 2。

  • 哪怕 master 和其中一个哨兵所在的机器宕机,还剩 2 个哨兵存活,既满足 quorum=2 的故障判定要求,也满足 majority=2 的选举要求,能正常完成故障转移,集群依然可用;
  • 3 个节点既能保证哨兵集群自身的高可用,也不会带来过多的运维成本,是业内最通用、最稳妥的部署方案。

三、哨兵实现自动故障转移的完整流程

整个流程全程自动化执行,分为 4 个核心步骤:

  1. 主观下线检测:每个哨兵节点会定期给所有 Redis 实例发送心跳包,如果 master 节点超过配置的超时时间没有响应,这个哨兵会先把 master 标记为「主观下线」,意思是 “我当前认为这个 master 节点挂了”。
  2. 客观下线确认:标记了主观下线的哨兵,会询问集群里其他的哨兵节点,是否也认为这个 master 故障了。如果同意的哨兵数量达到了我们配置的 quorum 值,就会把 master 正式标记为「客观下线」,确认 master 故障,触发故障转移流程。
  3. 领头哨兵选举:哨兵集群会通过 Raft 分布式选举算法,从存活的哨兵里选举出一个领头哨兵,由它全权负责后续的故障转移操作,避免多个哨兵同时操作导致集群状态混乱。
  4. 执行故障转移:领头哨兵会完成 4 个核心操作,完成主备切换:
    • 从健康的 slave 从节点里,按照节点优先级、数据完整性、节点 ID,选出一个最优的节点,升级为新的 master 主节点;
    • 让集群里其他所有的 slave 从节点,切换到新的 master,同步新 master 的全量 + 增量数据;
    • 把原来故障的 master 节点,标记为新 master 的从节点,等它恢复后,自动变成从节点同步数据;
    • 把新的 master 地址同步给所有业务客户端,客户端自动切换连接到新 master。

四、线上使用哨兵的核心注意事项 & 避坑指南
  1. 哨兵只保证服务高可用,不保证数据零丢失因为 Redis 主从复制是异步的,如果 master 宕机时,还有部分写命令没同步到 slave,这部分数据就会丢失。我们线上的解决方案是,配置主节点的写命令必须至少同步到 1 个从节点,才给客户端返回成功,最大限度减少数据丢失。
  2. 哨兵节点必须和 Redis 节点分散部署绝对不能把哨兵和 Redis 实例部署在同一台机器上,否则机器宕机,Redis 和哨兵会一起挂掉,触发前面说的无法故障转移的问题,彻底失去高可用能力。
  3. 必须做充足的故障演练我们集群上线前,会做多次 master 宕机、哨兵节点宕机、网络分区的故障演练,验证故障转移的成功率和耗时,确保线上真的出问题时,哨兵能正常工作。
  4. 合理配置故障检测参数故障检测的超时时间不能设置太短,避免网络波动导致的误切换;也不能设置太长,导致故障发现不及时。我们线上一般设置为 30 秒,兼顾准确性和故障响应速度。

一问一答:Redis 哨兵主备切换的数据丢失问题 & 核心机制

:Redis 哨兵主备切换的时候,会出现数据丢失吗?是什么原因导致的?该怎么解决?:Redis 哨兵主备切换的过程中,确实存在两种会导致数据丢失的场景,分别是异步复制延迟导致的数据丢失集群脑裂导致的数据丢失,我们线上也针对性做了配置优化和兜底方案,能最大限度避免数据丢失。同时我也会给你讲清楚哨兵故障转移相关的核心机制,这些也是面试的高频考点。


一、主备切换数据丢失的两个核心场景
1. 异步复制导致的数据丢失

Redis 的主从复制是异步执行的,这是数据丢失最核心的原因。

  • 正常流程:master 收到客户端的写请求,写完本地内存后就直接给客户端返回成功,之后才会异步把写命令同步给 slave 节点。
  • 丢失场景:如果 master 刚给客户端返回写成功,还没来得及把这条命令同步给 slave,就突然宕机了。此时哨兵会把 slave 选举成新的 master,这条没同步的写命令就彻底丢失了 —— 等旧 master 恢复后,会变成新 master 的从节点,清空自己的本地数据,重新同步新 master 的全量数据。
2. 集群脑裂导致的数据丢失

脑裂指的是 Redis 集群出现了两个同时工作的 master 主节点,这是极端网络场景下的问题。

  • 问题场景:旧 master 所在的机器突然出现网络故障,和 slave 节点、哨兵集群都断连了,但旧 master 本身还在正常运行。此时哨兵集群会认为旧 master 已经宕机,把一个健康的 slave 选举成新的 master,集群里就出现了新旧两个 master。
  • 丢失场景:如果业务客户端还没切换到新 master,继续给旧 master 写入数据,这部分数据在旧 master 恢复网络后,会被直接清空 —— 旧 master 会变成新 master 的从节点,全量同步新 master 的数据,这部分写入的数据就彻底丢失了。

二、数据丢失问题的解决方案

我们线上通过两个核心 Redis 配置,就能从根源上解决绝大多数的数据丢失问题,配置如下:

plaintext

min-slaves-to-write 1
min-slaves-max-lag 10

我给你讲清楚这两个配置的含义和作用:

  1. 配置含义
    • min-slaves-to-write 1:要求 master 节点至少要有 1 个正常连接的从节点;
    • min-slaves-max-lag 10:要求 slave 节点给 master 发送数据同步 ack 的延迟,最大不能超过 10 秒。两个配置配合起来,只要 master 所有的 slave 节点,数据同步延迟都超过了 10 秒,master 就会直接拒绝接收客户端的写请求。
  1. 如何解决数据丢失
    • 针对异步复制丢失:如果 slave 同步延迟太大,master 就会拒绝写请求,从根源上避免了 “master 写成功但没同步到 slave” 的情况。哪怕 master 宕机,所有写成功的数据都已经同步到 slave 了,不会出现数据丢失。
    • 针对脑裂丢失:如果旧 master 发生脑裂,和 slave 节点断连,超过 10 秒收不到 slave 的 ack,就会直接拒绝写请求。客户端不会再给旧 master 写入新数据,哪怕发生脑裂,也不会有新增数据,自然就不会出现脑裂导致的数据丢失。
  1. 我们线上的实践优化我们线上核心业务的 Redis 集群,会把配置升级为min-slaves-to-write 2min-slaves-max-lag 5,要求至少 2 个从节点的同步延迟不超过 5 秒,进一步降低数据丢失的风险。同时配合客户端的自动切换机制,master 拒绝写的时候,客户端会快速感知到主备切换,自动切换到新 master 写入,不会影响业务的可用性。

三、哨兵故障转移的核心补充机制(面试高频考点)

除了数据丢失问题,面试官还会顺着这个问题,追问哨兵故障转移的核心机制,我也给你梳理清楚了:

1. sdown 和 odown 的转换机制

这是哨兵判断 master 故障的核心规则:

  • sdown(主观下线):单个哨兵节点给 master 发送心跳包,超过is-master-down-after-milliseconds配置的时间没收到响应,就会主观认为 master 宕机,标记为 sdown。
  • odown(客观下线):标记了 sdown 的哨兵,会询问集群里其他哨兵的判断,如果有超过我们配置的quorum数量的哨兵,都认为 master 宕机了,就会把 master 正式标记为 odown,确认故障,触发后续的故障转移流程。
2. slave 节点选举为新 master 的算法

当 master 被确认客观下线后,领头哨兵会按照固定优先级,从健康的 slave 里选出新的 master,排序规则是:

  1. 先过滤掉不健康、和 master 断连时间过长的 slave,排除不适合的候选者;
  2. slave-priority优先级排序,配置的优先级数值越低,选举优先级越高;
  3. 优先级相同的话,看复制偏移量replica offset,复制的数据越完整、越接近 master 的 slave,优先级越高,最大限度减少数据丢失;
  4. 如果前面两个条件都相同,就选 run id 更小的 slave 节点。
3. quorum 和 majority 的核心区别

这是哨兵集群部署的核心原则,也是面试的高频坑点:

  • quorum:判定 master 客观下线需要的最少哨兵票数,是我们手动配置的;
  • majority:执行故障转移、选举领头哨兵需要的最少存活哨兵数,固定为集群总哨兵数的半数以上(比如 3 个哨兵的 majority 是 2,5 个哨兵的 majority 是 3)。
  • 只有同时满足两个条件:拿到 quorum 票确认 master 客观下线,同时有超过 majority 的哨兵存活,才能正常执行故障转移。这也是为什么哨兵集群必须部署奇数个、至少 3 个节点的核心原因。
4. 哨兵集群的自动发现与配置同步
  • 哨兵之间的互相发现,是通过 Redis 的发布订阅机制实现的:每个哨兵都会往_sentinel_:hello频道发送自己的节点信息,其他监听这个频道的哨兵,就能自动感知到新的哨兵节点,加入集群。
  • 主备切换完成后,新 master 会生成一个唯一的configuration epoch(版本号),哨兵会通过发布订阅机制,把新的 master 配置同步给集群里所有的哨兵,其他哨兵通过版本号判断配置的新旧,自动更新自己的配置,保证整个集群的配置一致。

一问一答:Redis 的持久化方式 & 选型最佳实践

:Redis 的持久化有哪几种方式?分别有什么优缺点?线上该如何选型?:Redis 的持久化核心有两种基础方式,分别是RDB 快照持久化AOF 日志持久化,Redis 4.0 之后还推出了两者结合的混合持久化模式。持久化的核心目的,是解决 Redis 纯内存运行时,宕机重启后数据全部丢失的问题,重启时能从磁盘恢复数据,也是 Redis 高可用、数据灾备的核心环节。我分别给你讲清楚每种方式的原理、优缺点,还有我们线上的选型和最佳实践。


一、RDB 快照持久化

RDB 是全量快照的持久化方式,简单说就是给 Redis 内存里的所有数据,在某个时间点拍一张完整的 “数据照片”,生成一个二进制的 RDB 快照文件保存到磁盘。Redis 重启的时候,直接加载这个 RDB 文件,就能快速把所有数据恢复到内存里。

RDB 的触发分为两种:一种是手动触发,执行save/bgsave命令;另一种是自动触发,在配置文件里设置规则,比如 “900 秒内至少 1 个 key 被修改” 就自动生成快照,满足规则就会自动执行。

RDB 的核心优点
  1. 非常适合做冷备与灾备:每个 RDB 文件都是对应时间点的完整数据快照,我们可以把不同时间点的 RDB 备份到云存储,哪怕出现磁盘故障、数据误删,也能通过历史快照恢复到指定时间点的数据。
  2. 对 Redis 读写性能影响极小:生成快照时,Redis 主进程只会 fork 一个子进程,所有磁盘 IO 操作都由子进程完成,主进程完全不用参与磁盘操作,能继续正常处理客户端请求,最大程度保证了 Redis 的高性能。
  3. 数据恢复速度极快:RDB 是二进制的全量数据文件,重启恢复时直接加载到内存即可,比 AOF 逐条回放命令的恢复速度快几个数量级,特别适合 Redis 集群故障后快速恢复服务。
RDB 的核心缺点
  1. 数据丢失风险大:RDB 是周期性生成的,哪怕配置 5 分钟生成一次快照,如果 Redis 在两次快照之间宕机,就会丢失最近 5 分钟内的所有写数据,对数据安全性要求高的业务,这个风险是无法接受的。
  2. 大数据量下会有短暂阻塞:如果 Redis 内存数据量很大,fork 子进程时需要复制主进程的内存页表,这个过程会阻塞主进程,数据量越大阻塞时间越长,极端情况会阻塞几百毫秒甚至几秒,影响线上请求的响应。

二、AOF 日志持久化

AOF 是写命令追加的持久化方式,和 RDB 存全量数据不同,AOF 会把 Redis 执行的每一条写命令(比如 set、hset、del),以 append-only 追加的方式写入到 AOF 日志文件里。Redis 重启时,会从头到尾重新执行 AOF 文件里的所有写命令,就能完整重建整个数据集。

AOF 的核心是刷盘策略,也就是写命令同步到磁盘的时机,有三个可选配置:

  • always:每执行一条写命令就立刻刷盘,数据零丢失,但性能损耗极大,线上基本不用;
  • everysec:每秒后台执行一次刷盘,是线上默认配置,最多丢失 1 秒的数据,平衡了性能和数据安全性;
  • no:完全交给操作系统控制刷盘时机,性能最好,但数据丢失风险极大,线上绝对不推荐使用。

同时 AOF 支持自动重写(rewrite):AOF 文件会随着写命令持续变大,Redis 会定期触发重写,基于当前内存的数据集,生成一份能重建所有数据的最小命令集,替换掉原来的大文件,大幅压缩 AOF 的体积。

AOF 的核心优点
  1. 数据安全性极高,丢失风险极小:线上用默认的everysec每秒刷盘策略,哪怕 Redis 宕机,最多只会丢失 1 秒的数据,完全能满足绝大多数业务的数据安全要求。
  2. 写入性能高,文件健壮性强:AOF 是追加写入模式,没有磁盘随机寻址的开销,写入性能非常高;哪怕文件尾部出现损坏,也能通过工具轻松修复,不会出现整个文件失效的问题。
  3. 支持灾难性误操作恢复:AOF 文件的命令是明文可读的,如果有人不小心执行flushall清空了数据,只要 AOF 还没触发重写,我们就能直接修改 AOF 文件删掉这条清空命令,重启 Redis 就能恢复所有数据,这是 RDB 做不到的。
AOF 的核心缺点
  1. 文件体积远大于 RDB:AOF 会记录每一条写命令,哪怕是对同一个 key 的多次修改,都会全部记录下来,哪怕经过重写,文件体积还是会比同数据量的 RDB 大很多。
  2. 重启恢复速度远慢于 RDB:AOF 恢复数据需要逐条回放执行所有写命令,如果数据量很大,恢复过程会非常慢,可能需要几十分钟,而 RDB 只需要几分钟就能完成恢复。
  3. 对写入性能有一定影响:哪怕是每秒刷盘的策略,也会有一定的性能开销,极端高并发的写场景下,AOF 的写入 QPS 会比仅开启 RDB 的场景低一些。

三、线上持久化的选型最佳实践

我们线上不会单独只用 RDB 或者只用 AOF,而是采用RDB+AOF 混合持久化模式,这也是 Redis 官方推荐的生产环境最佳实践:

  1. 绝对不要单独只用 RDB:虽然恢复速度快,但会丢失大量数据,仅适合纯缓存、允许数据丢失的非核心场景。
  2. 不建议单独只用 AOF:虽然数据安全性高,但恢复速度太慢;同时 RDB 作为冷备,能避免 AOF 复杂的恢复机制出现 bug 导致数据无法恢复的问题,是更健壮的兜底方案。
  3. 线上首选:混合持久化(Redis 4.0+)混合持久化完美结合了两者的优点:AOF 重写的时候,会把当前内存的全量数据以 RDB 格式写入 AOF 文件的开头,之后的增量写命令以 AOF 格式追加到文件末尾。重启恢复时,先加载开头的 RDB 全量数据,再回放后面的增量 AOF 命令,既保证了恢复速度快,又把数据丢失的风险降到了最低。

我们线上核心业务的 Redis 集群,就是用的混合持久化:RDB 配置 15 分钟生成一次快照,AOF 用每秒刷盘的策略,同时开启自动重写。既保证了宕机最多丢失 1 秒的数据,又能在故障后快速恢复服务,同时还有 RDB 快照作为冷备兜底,哪怕出现极端的 AOF 文件损坏,也能通过 RDB 快照恢复数据。

一问一答:Redis 的缓存雪崩、穿透、击穿 & 解决方案

:什么是 Redis 的缓存雪崩、缓存穿透、缓存击穿?分别该怎么解决?:这三个是缓存最核心的三大致命问题,平时不会出现,一旦出现就会直接打垮数据库,引发整个系统的服务雪崩,是面试必问的核心考点。我分别给你讲清楚每个问题的触发场景、核心原理,还有我们线上经过验证的完整解决方案。


一、缓存雪崩
1. 到底什么是缓存雪崩?

简单说,缓存雪崩就是缓存层整体失效,所有流量直接打到数据库,最终把数据库打垮,引发系统雪崩。主要有两种触发场景:

  • 场景 1:Redis 集群整体宕机,原本缓存能扛住的每秒几千上万的请求,瞬间全部打到数据库上,数据库扛不住高并发直接被打挂,哪怕重启数据库,也会被新的流量再次打垮,整个系统彻底不可用。
  • 场景 2:大量的缓存 key 在同一时间集体过期失效,所有请求这些 key 的流量,同时绕过缓存查数据库,同样会把数据库压垮。

举个实际的例子:我们系统高峰期每秒有 5000 个请求,其中 4000 个都被缓存扛住了,只有 1000 个落到数据库。如果缓存挂了,5000 个请求会全部打到数据库,而我们的 MySQL 单机最多扛 2000QPS,瞬间就会被打挂。

2. 完整解决方案(事前 + 事中 + 事后全链路兜底)

我们线上是从事前、事中、事后三个维度做了完整的防护,完全避免缓存雪崩的问题:

  • 事前(根源预防)
    1. 保证 Redis 集群的高可用,线上用主从 + 哨兵 / Redis Cluster 集群部署,避免 Redis 单点故障导致全盘崩溃;
    2. 给不同 key 的过期时间加随机值,比如原本统一设置 30 分钟过期,我们会给每个 key 加 0-5 分钟的随机过期时间,避免大量 key 同时过期,引发集中失效;
    3. 热点数据提前预热,大促前把商品、活动这类热点数据,提前加载到缓存里,同时延长过期时间,避免高峰期出现缓存失效。
  • 事中(故障防护,保证数据库不被打死)
    1. 多级缓存兜底:用本地 Caffeine/ehcache 缓存 + Redis 分布式缓存,请求过来先查本地缓存,没查到再查 Redis,都没有才会查数据库,哪怕 Redis 挂了,本地缓存也能扛住一部分热点流量;
    2. 限流 + 熔断降级:用 Sentinel 限流组件,限制每秒打到数据库的请求数,超过阈值的请求直接降级,返回兜底的默认数据或者友好提示,绝对保证数据库不会被打死。只要数据库不挂,就还有一部分请求能正常处理,系统不会彻底瘫痪。
  • 事后(快速恢复服务)开启 Redis 的 RDB+AOF 混合持久化,Redis 宕机重启后,能快速从磁盘恢复缓存数据,重建缓存屏障,快速恢复服务。

二、缓存穿透
1. 到底什么是缓存穿透?

缓存穿透指的是用户请求的数据,在缓存里不存在,在数据库里也不存在,导致每次请求都会完全绕过缓存,直接打到数据库上。如果是大量的这种无效请求,比如黑客的恶意攻击,会直接把数据库打垮。

举个最典型的例子:我们数据库里的商品 ID 都是从 1 开始的正整数,黑客用负数 ID、或者超大的不存在的 ID,每秒发起几千次请求,缓存里没有这些 key,数据库里也查不到对应的数据,每次请求都会直接查库,很快就会把数据库打挂。

2. 完整解决方案

我们线上用了多层防护,从根源上解决缓存穿透的问题:

  1. 空值缓存(最常用,成本最低)如果从数据库里没查到对应的数据,我们就给这个 key 缓存一个空值或者默认值,同时设置一个较短的过期时间(比如 3-5 分钟)。下次相同的 key 过来,直接从缓存里返回结果,不会再查数据库,哪怕是恶意攻击,也只会有一次请求落到数据库。
  2. 布隆过滤器(应对大规模恶意攻击,效果最好)我们会提前把所有合法的业务 key(比如全量的商品 ID、用户 ID),写入布隆过滤器。请求过来先查布隆过滤器,如果过滤器里没有这个 key,直接拦截返回,根本不会走到缓存和数据库层,完美应对黑客的恶意穿透攻击。
  3. 接口层前置校验在接口入口就做参数合法性校验,比如 ID 为负数、格式非法、长度超出范围的请求,直接在入口层拦截掉,不会让无效请求打到下游的缓存和数据库。

三、缓存击穿
1. 到底什么是缓存击穿?

缓存击穿也叫热点 key 失效,指的是某个被超高并发集中访问的热点 key,在缓存过期失效的瞬间,大量的并发请求同时绕过缓存,直接打到数据库上,就像在缓存的屏障上凿开了一个洞,瞬间就会把数据库压垮。

最典型的场景:大促期间的爆款商品详情,每秒有几万的访问请求,这个商品的缓存过期的瞬间,所有请求都会同时去查数据库,直接把数据库打挂。

2. 完整解决方案

我们线上针对热点 key,用了两种核心方案,完全避免缓存击穿的问题:

  1. 热点 key 永不过期(最稳妥,从根源解决问题)对于超高并发的热点 key,我们直接设置为永不过期,不会出现过期失效的问题。同时后台会启动异步任务,定时更新这个 key 的缓存数据,既保证了缓存不会过期,又能保证数据的一致性。
  2. 分布式互斥锁(通用方案,适配绝大多数场景)当缓存失效时,我们用 Redis 分布式锁控制:只有第一个请求能抢到锁,去查询数据库并更新缓存,其他请求会等待缓存更新完成后,直接从缓存里拿数据。这样保证了同一时间,最多只有一个请求打到数据库,不会出现并发击穿的问题。
  3. 热点数据提前预热大促前,我们会通过大数据统计,提前把热点商品、活动数据加载到缓存里,同时给这些 key 设置超长的过期时间,避免高峰期出现缓存失效的情况。

我们线上的实践总结

我们核心的商品缓存集群,就是用了这套完整的防护方案:布隆过滤器防穿透、热点 key 永不过期防击穿、Redis 集群 + 多级缓存 + 限流熔断防雪崩,哪怕是双十一大促的峰值流量,也没出现过这三类问题,数据库一直运行稳定。

一问一答:如何保证缓存与数据库的双写一致性?

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐