使用MQ的时候,怎么确保消息100%不丢失?

Fox 2025-11-19 09:56:29
“我一个朋友,面美团的时候,前面聊得都很好,结果最后被一个MQ的问题给挂了:‘在使用 MQ 的时候,怎么确保消息 100% 不丢失?’ 他当时就回答了ACK机制,然后……就没然后了。”

 

这几乎是每一个中高级后端面试中,都无法绕开的“送命题”。

 

为什么这个问题这么重要?因为它考察的早已不是你对某个MQ中间件的API有多熟悉,而是你对分布式系统中“可靠性”和“一致性”的理解有多深。它是一道系统设计题,考验的是你作为工程师的全局视野和严谨性。

 

大多数人只答得出生产端的ACK或消费端的手动提交,但这远远不够。一个完整的保障体系,必须覆盖消息从诞生到消亡的全链路。今天,我们就用一套“三板斧”架构,把这个问题彻底锤烂。

 

消息丢失的根源:三大风险环节

 

要保证消息不丢失,首先得知道它在哪可能丢。消息的生命周期无非三个阶段:生产、存储(Broker) 和 消费。每个阶段都潜藏着丢失的风险。

 

 

 
生产环节

 

生产者应用把消息发往 Broker,但可能因为网络问题、Broker宕机等原因,Broker压根就没收到。

 

 
存储环节

 

消息成功到达 Broker,但如果 Broker 还没来得及将消息持久化到磁盘就宕机了,消息依然会丢失。或者,在主从架构中,Master 节点收到了消息,但还没来得及同步给 Follower 节点就挂了,也可能导致丢失。

 

 
消费环节

 

消费者成功从 Broker 拉取了消息,但还没来得及处理完业务逻辑,自己的服务就挂了。如果此时你还用了“自动提交位移”,那 Broker 会误以为你已经消费成功,这条消息对你而言就永远丢失了。

 

搞清楚了风险点,我们的“三板斧”就可以精准打击了。

 

第一板斧:生产端保障 —— 让消息安全启航

 

生产端的保障核心,是确保消息能被 Broker “确认接收”。这里有两层保障。

 

 
基础保障:ACK 确认机制

 

以 Kafka 为例,当你调用 producer.send(message) 时,这个调用在默认情况下是异步的。为了确保消息被成功接收,你可以依赖生产者的 ACK (Acknowledge) 机制。acks 参数有三个核心配置:

 

  • acks = 0

生产者发送消息后,不等待 Broker 的任何响应。性能最高,但丢消息的风险也最大。

 

  • acks = 1

(默认值) 生产者发送消息后,只需等待 Leader 副本成功写入本地日志即可,无需等待所有 Follower。

 

  • acks = -1或 all

   最高可靠性等级。生产者发送消息后,需要等待 ISR (In-Sync Replicas,同步副本列表) 中的所有副本都确认接收后,才算成功。

 

面试标准答案:将 acks 设置为 all,并配合合理的重试机制 (retries 参数),基本可以解决绝大多数因网络抖动等原因导致的消息发送失败。

 

 
终极保障:本地消息表

 

仅仅设置 acks=all 就万无一失了吗?思考一个经典的分布式事务场景:

 

你的业务是“下单后扣减库存”。你的代码逻辑是:

 

  •  
  •  
  •  
  •  
开启数据库事务。UPDATE stock SET count = count - 1 WHERE ...producer.send("stock_deducted_message")提交数据库事务。

 

如果第3步 send 方法执行时,MQ Broker 恰好全挂了,或者网络完全中断,即使你设置了重试也没用,最终会抛出异常。此时,因为异常发生,数据库事务会回滚,库存没扣,消息也没发出去,数据是一致的。

 

但如果代码是这样写的呢?

 

  •  
  •  
  •  
  •  
  •  
开启数据库事务。UPDATE stock SET count = count - 1 WHERE ...提交数据库事务。producer.send("stock_deducted_message") // 在事务外发送

 

如果第3步事务提交成功,但第4步发送消息时失败了,怎么办?库存已经扣了,但下游服务(比如物流、发积分)永远也收不到这个通知了。这可是严重的生产事故!

 

“本地消息表”方案,就是为了解决“业务操作”和“发送消息”这两步操作的原子性问题。

 

它的核心思想是:

 

 

  • 在业务数据库中,创建一个“本地消息表”(local_message),用于记录待发送的消息。

  • 将业务操作和“向本地消息表插入一条消息”这两个动作,放在同一个本地数据库事务中。

  • 这样,只要业务操作成功,那么“待发送”的消息记录就一定能成功存入数据库。

  • 启动一个独立的后台任务(可以是定时任务,也可以是单独的线程),去轮询这张“本地消息表”,将状态为“待发送”的消息,发送给 MQ。

  • 当MQ Broker 确认收到消息后(依赖前面讲的 ACK 机制),再将这条消息记录的状态更新为“已发送”或直接删除。

 

这套组合拳,通过数据库的事务能力,巧妙地把“不确定的跨网络发送”转换成了“确定的本地数据库写入”,从而保证了只要业务成功,消息就一定不会在生产端丢失。

 

第二板斧:存储端保障 —— 让消息高枕无忧

 

消息安全到达 Broker 就够了吗?不够。如果 Broker 自己不靠谱,也白搭。存储端的保障,依赖于 MQ 中间件自身的持久化和高可用架构。

 

同样以 Kafka 为例,保障的核心是副本机制(Replication)。

 

 
副本数量 (replication.factor)

 

通常设置为 3 或更高,一个 Leader,多个 Follower。副本会分布在不同的物理机架上,防止因单机或单机架故障导致数据完全丢失。

 

 
同步副本 (min.insync.replicas)

 

当 acks 设置为 all 时,这个参数就派上用场了。它定义了必须确认收到消息的最小副本数。如果设置为 2,replication.factor 为 3,那么只要 Leader 和任意一个 Follower 确认了,生产者就会收到 ACK。这可以在性能和可靠性之间做一个平衡。为了最高可靠性,可以将其设置为与副本数相同。

 

 
不干净的 Leader 选举 (unclean.leader.election.enable)

 

这是最后一道保险!默认是 false,也绝对不要改成 true!如果 Leader 宕机,它的某些数据可能还没来得及同步给 Follower。如果此时允许一个数据落后的 Follower 成为新 Leader,那么这些未同步的数据就永远丢失了。关闭它,就意味着我们宁愿牺牲短时间的可用性(选举不出新 Leader),也要保证数据的绝对一致和不丢失。

 

第三板斧:消费端保障 —— 别让消息被“假”消费

 

生产和存储都万无一失了,最后来看看消费端。这也是最容易出问题的地方。

 

消费端丢失消息的唯一原因,就是:消息明明还没被成功处理,你却提前告诉 Broker 你已经处理好了。

 

这通常发生在“自动提交位移(Offset)”的场景下。

 

  • 错误的做法

 

设置 enable.auto.commit = true。消费者拉取一批消息后,程序还在吭哧吭哧地处理业务逻辑(比如写入数据库),但后台线程已经默默地帮你把这批消息的位移提交了。如果此时你的服务挂了,Broker 认为你已经消费成功。等你重启,你将从新的位移开始消费,之前那批没处理完的消息,就永远与你失之交臂了。

 

  • 正确的做法

 

关闭自动提交,改为手动提交!enable.auto.commit = false

 

 

  • 从 Broker 拉取消息。

  • 完整地执行完你的所有业务逻辑。

  • 当且仅当所有逻辑都成功处理后,再调用 consumer.commitSync() 或 commitAsync() 方法,手动提交位移。

 

这样,即使你的业务逻辑处理到一半就挂了,因为没有提交位移,下次重启后,你依然能从上一次的位置重新拉取这批消息,进行处理。

 

 
消费端的最后一块拼图:幂等性

 

手动提交保证了消息的“至少一次消费(At-Least-Once)”。但这也带来了新的问题:如果消息处理成功了,但在提交位移时网络抖动导致失败,消费者重启后会重复消费这条消息。

 

因此,消费端的业务逻辑必须设计成幂等的(Idempotent)。也就是说,同一个操作,执行一次和执行 N 次,结果是完全相同的。

 

常见的幂等性实现方法包括:

 

  • 数据库唯一键约束;

  • 乐观锁(版本号机制);

  • 分布式锁(如 Redis、Zookeeper);

  • 在业务表中记录一个“已处理消息ID”的字段。

 

总结:面试高分回答模板

 

现在,让我们回到最初的问题。当面试官再次问你时,你可以从容不迫地这样回答:

 

“面试官您好。要保证消息100%不丢失,我认为需要从生产、存储、消费三个环节,建立一个全链路的可靠性保障体系。

 

首先,在生产端,我会通过配置 acks=all 并设置合理的重试次数,来保证消息能够被MQ集群完整接收。对于一致性要求极高的业务,我会引入‘本地消息表’方案,将业务操作和发送消息封装在同一个本地事务中,确保业务和消息的原子性,从根源上杜绝生产端丢失的可能。

 

其次,在存储(Broker)端,我会通过配置多副本(replication.factor >= 3)来保证高可用,同时,设置 min.insync.replicas > 1 来确保消息至少被写入多个副本才算成功。最关键的是,我会确保 unclean.leader.election 为 false,坚决杜绝因选举出数据落后的副本而导致的数据丢失。

 

最后,在消费端,我会关闭‘自动提交位移’,改为在业务逻辑完全处理成功后,再进行‘手动提交位移’。这保证了即使我的消费服务宕机,消息也不会丢失。同时,因为这种机制是‘至少一次消费’,我会在消费端的业务逻辑中,做好幂等性设计,防止消息重复处理。

 

通过这三层保障,就可以构建一个高可靠的、消息零丢失的MQ系统。”

 

这样一套有深度、有广度、有细节、有权衡的组合拳打下来,你觉得,这次还能挂吗?

 

 
作者丨Fox
来源丨公众号:Fox爱分享(ID:dcl_yc)
dbaplus社群欢迎广大技术人员投稿,投稿邮箱:editor@dbaplus.cn
最新评论
访客 2024年04月08日

如果字段的最大可能长度超过255字节,那么长度值可能…

访客 2024年03月04日

只能说作者太用心了,优秀

访客 2024年02月23日

感谢详解

访客 2024年02月20日

一般干个7-8年(即30岁左右),能做到年入40w-50w;有…

访客 2023年08月20日

230721

活动预告