MQ(消息队列)在分布式系统里几乎是标配,但聊到“消息不丢失”这个话题,很多同学的第一反应是:我们配置了高可用,应该不会丢吧。我在过去几年排查过的丢消息事故里,至少有一半的根因都不是Broker直接崩了,而是生产者、消费者或配置细节上一环没锁住,消息就悄无声息地没了。这篇文章我就从一条消息从产生到被业务真正处理这条链路出发,把生产端、Broker存储端、消费端以及延迟消息这类特殊场景全部拆开讲清楚。适合正在用或打算用MQ的开发和运维同学,尤其是已经遇到“莫名的丢消息”却不知道从哪下手的人。
1. 先弄明白一条消息从哪丢到哪:端到端故障模型
1.1 三个环节一次都不能断
一条正经消息从产生到被消费,经过三段:
- 生产端:业务应用把消息发到MQ的Broker
- Broker存储:消息在Broker持久化并同步副本
- 消费端:消费者拉取/推送消息,执行业务逻辑,提交进度
用快递类比:你在电商平台下单(生产),包裹在中转中心(Broker)暂存,最后由快递员派送给你并签收(消费)。任何一环出了问题,包裹就收不到了。MQ的可靠性也一样,必须全链路闭环,哪怕只有生产端一个环节“默认成功”,整体都会丢。
这就是为什么我经常说,不要试图用一个“巨大的配置项”解决所有问题。Kafka设置acks=all不代表消费端一定不丢,因为消费者可能自动提交offset;RocketMQ开了同步刷盘也不代表生产者发了就万事大吉,因为你的发送代码可能根本不检查返回值。后面几节我会按链路逐一展开。
1.2 丢消息的七个常见现场
先把我这些年见过的高频丢消息场景列出来,后面再逐个拆:
- 生产者异步发送消息,回调里的异常被吞掉,消息没有重试。
- 生产者端收到Broker的ACK,但Broker把消息丢在PageCache里还没刷盘,节点断电重启。
- Broker的主从复制是异步的,主节点故障切换后,从节点上少了刚才“成功”的数据。
- 消费者使用自动提交offset,在业务逻辑执行前偏移量已经被提交,业务异常时消息再也拉不回来。
- 消费者在处理消息过程中发生Rebalance,分区被分配给别的实例,旧实例已拉取但未提交的消息被新消费者重新消费,如果业务处理了一半没有幂等保护,可能造成数据错乱。
- 延迟消息被存在进程内存的时间轮里,还没有到期,部署重启后消息直接消失。
- 事务消息本地事务执行成功了,但Broker的回查失败或超时,最终消息没有被commit,业务方没有补偿。
这些场景单独看都挺简单,但放在真实环境里往往叠加在一起出现。先意识到“所有环节都可能丢”,后面才能设计出一套端到端不丢的方案。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 生产端:把消息“送出去”前必须锁死的三板斧
2.1 发送确认机制:别把“发出去了”当成“收到了”
生产端的第一道防线,是确认你的消息真的被Broker接收了。
很多开发刚接触MQ时,习惯用类似producer.send(msg)的单向/异步发送,不关心返回值,也不注册回调。在低峰期看起来一切正常,一旦网络抖动或Broker出现瞬时故障,发送请求可能直接抛出异常,或者虽然发送成功但Broker返回响应超时,消息就丢了。
正确做法是“发送后必须确认”。拿Kafka生产端的Java客户端举例:
java复制producer.send(new ProducerRecord<>("order_topic", key, value), (metadata, exception) -> {
if (exception != null) {
// 记录失败消息,进入补偿流程
handleSendFailure(message);
} else {
// metadata.topic() 和 metadata.offset() 能拿到消息真正落位的位置
log.info("mq message sent ok, topic={}, offset={}", metadata.topic(), metadata.offset());
}
});
RocketMQ的同步发送写法更直白:
java复制SendResult result = producer.send(msg);
if (result.getSendStatus() != SendStatus.SEND_OK) {
// 重试或落库补偿
}
无论用哪
