消息丢失的终极防线
❓ 面试官:引入 MQ 后,怎么保证消息绝对不丢失?(以 RocketMQ / RabbitMQ 为例)
频率:🔥🔥🔥🔥🔥
💡 一句话总结(先抛结论): 一条消息从产生到被消费,会经历三个阶段,这三个阶段都可能丢消息。我们必须做到:生产者采用带确认的同步发送机制 + MQ 服务端开启同步刷盘和主从同步复制 + 消费者关闭自动 ACK 采用手动确认机制,这样才能保证消息绝对不丢失。
📝 详细三个阶段的防丢失策略实战:
第一关:生产者发送阶段丢失
- 丢失场景:网络突然抖动,代码里调了
send()以为发成功了,其实 MQ 根本没收到。 - 解决方案(RocketMQ 为例):
- 放弃异步/单向发送:核心业务必须使用同步发送(Sync Send)。
- 判断发送状态:调用
send()后,必须同步拿到一个SendResult对象。判断它的状态是不是SEND_OK。 - 重试与兜底方案:如果不是
OK或者抛了超时异常,触发重试机制(默认重试 2 次)。如果重试 3 次依然失败,必须捕获异常,把这条本该发送的消息记录到数据库的“消息发送失败表”中,然后由后台定时任务轮询补偿发送,确保第一关绝对不丢。
第二关:MQ 服务端(Broker)存储阶段丢失
- 丢失场景:MQ 确实收到了消息,先放进了系统内存(PageCache),正准备写到磁盘上时,MQ 所在的服务器突然断电/宕机,内存里的数据瞬间蒸发。
- 解决方案:
- 修改刷盘策略(牺牲性能保安全):把 MQ 默认的
异步刷盘(ASYNC_FLUSH)改为同步刷盘(SYNC_FLUSH)。这意味着,只有当这条消息确确实实写死到了硬盘上,MQ 才会给生产者返回SEND_OK。 - 修改主从同步策略:如果是集群部署,只有主节点写盘成功不够,万一主节点的硬盘直接烧了呢?把主从同步策略从
异步复制改为同步双写(SYNC_MASTER)。只有当主节点和从节点都把数据写到硬盘了,才算真的发送成功。
- 修改刷盘策略(牺牲性能保安全):把 MQ 默认的
第三关:消费者消费阶段丢失(极其容易踩坑)
- 丢失场景:消费者刚从 MQ 拉到消息,还没来得及执行业务逻辑(比如写数据库),代码里突然抛了个空指针异常,或者消费者服务器直接被 kill 掉了。但 MQ 以为你消费成功了,就把消息从队列里删了。
- 解决方案:
- 核心原则:关闭自动 ACK,改为手动 ACK。
- Spring Boot 落地:在监听器里,必须是你的业务逻辑全部执行成功(比如数据库 commit 成功)后,才手动给 MQ 返回
Action.CommitMessage或ConsumeConcurrentlyStatus.CONSUME_SUCCESS。 - 如果抛了异常,应该被
catch住,返回RECONSUME_LATER(稍后重试),MQ 会把这条消息放回重试队列,等会儿再发给你,绝对不会因为异常而直接丢弃消息。
🌟 面试加分项(实战血泪教训): "面试官您好,理论上做到上面三步确实能保证不丢,但这会导致系统的吞吐量产生断崖式的下跌(全是同步和等磁盘 IO)。 在我们真实的电商支付链路中,为了兼顾高并发,我们 MQ 服务端依然保留了异步刷盘。那万一宕机丢了怎么办? 我们引入了对账机制(兜底王牌)。我们会用一个定时任务,每隔 5 分钟去比对“订单库”和“支付库”的数据。一旦发现订单状态是已支付,但支付库没有对应的流水(说明丢了通知消息),就会通过对账程序自动发起状态补偿和重试。这才是大厂高并发下容忍一定故障但保证最终一致性的真实解法。"