连载中 5/20

消息不丢(上):生产端确认与重试

2026-07-31 · 2961 阅读 · 0 评论 · 0 赞

不丢的三段论

「消息不丢」是 MQ 可靠性的第一命题。一条消息从生到死要过三关:生产端送达 Broker、Broker 自己不弄丢、消费端确认后再干完活。三关都要设防,本篇先守第一关:生产端。

生产端丢消息的三种死法

死法一:oneway 一把梭。发送模式里最浪的一种——消息扔出去就不管了,不等应答也不重试。网络抖一下,消息就消失在世界上。oneway 只该用在允许丢的埋点日志上,业务消息碰都别碰。

死法二:异步回调吞异常。异步发送吞吐高,但很多人写出这样的代码:

producer.send(msg, new SendCallback() {
    public void onSuccess(SendResult result) { log.info("发送成功"); }
    public void onException(Exception e) {
        // 什么都没写——异常被吞了,消息无声无息地没了
    }
});

回调里不打日志、不落补偿、不告警,等于失败了对全世界保密。异步发送的正确姿势是 onException 里必须做事:记表、重试或告警,三选一起码做一个。

死法三:进程崩溃消息还在内存。业务先提交事务,再发消息,如果业务成功后、消息发出前进程被杀,这条消息就永远停在「该发未发」的状态。这是状态机缺口问题,普通重试救不了,要用下面的本地消息表。

第一层防线:同步发送与发送重试

同步发送是最基本的要求:send 返回 SendResult,检查发送状态再往下走。RocketMQ 的 send 默认同步模式,发送失败自动重试 2 次(共 3 次机会),并且会自动换一个 Broker 重试——第一次碰上的 Broker 恰好挂了,第二次换个路口再试。

SendResult result = producer.send(msg);
if (result.getSendStatus() != SendStatus.SEND_OK) {
    // FLUSH_DISK_TIMEOUT / SLAVE_NOT_AVAILABLE 等,按业务决定重投或告警
    localMessageMapper.markForRetry(msg);
}

注意重试的副作用:重试可能导致重复发送。第一次其实成功了,只是 ACK 超时,重试后 Broker 里就有了两条一模一样的消息。所以「不丢」和「不重」天生是一对,生产端负责不丢,消费端负责不重(第 7 篇)。

第二层防线:本地消息表

重试解决「发送时失败」,解决不了「该发未发」。终极兜底是把消息先落到自己的数据库里,和业务数据同一个事务提交:

@Transactional
public void createOrder(Order order) {
    orderMapper.insert(order);
    // 消息记录与订单同库同事务,要么都在,要么都不在
    localMessageMapper.insert(new LocalMessage(order.getId(), ORDER_PAID_EVENT, PENDING));
}
// 事务提交后投递,失败由定时任务扫描补偿
afterCommit(() -> mq.sendAndMark(localMessage));

本地消息表的关键字段:业务单号(幂等键)、消息内容、状态(待发送/已发送/发送失败)、重试次数、下次重试时间。定时任务扫描 PENDING 和失败超限的记录,保证只要业务数据在,消息终将被投出。代价是每条消息多一次落库,用性能换确定性,核心链路值得。RocketMQ 的事务消息(第 10 篇)本质上是把这张表内置到了 Broker 里。

三层防线怎么排兵布阵

防线解决的问题代价适用
同步发送+检查发送失败无感知吞吐下降所有业务消息的底线
发送重试瞬时抖动、单点故障可能重复发送客户端默认开启
本地消息表业务与消息的原子性多一次落库+补偿任务核心链路,不可丢的场景

小结

生产端不丢的三板斧:不用 oneway 发业务消息、异步回调必须处理异常、核心链路上本地消息表。再记住一条:重试和重复是一对孪生兄弟,生产端把消息送到了,重复的烂摊子交给消费端的幂等去收拾。

消息送到了 Broker,就安全了吗?下一篇讲消息不丢(下):Broker 的刷盘策略、主从复制,以及消费端那个最容易踩坑的 ACK 时机——先干活再提交,还是先提交再干活,选错一边就是丢或者重。

503

10 年全栈工程师 · 503咖啡馆主理人

#消息队列#消息不丢#生产端#本地消息表#发送重试

评论 (0)

相关推荐

连载中 12/20

排查四件套:jstack、jmap、jstat、jcmd 的实战分工

jstack 看线程在干什么,jmap 看堆里装了什么,jstat 看运行时在变什么,jcmd 是统一入口。四把刀各管一段,配合着用没有查不动的现场。

#jstack#jmap#jstat#jcmd#排查工具
2026-09-16 · 3 阅读 · 0 评论 · 0 赞
连载中 11/20

GC 日志:把回收过程翻译成人话

一行 GC 日志里塞着七种信息:谁触发的、收了哪、停了多久、活了哪些。加上 -Xlog 配置,再加上日志分析工具,GC 不再是只能盯监控曲线的黑盒。

#GC日志#Xlog#日志分析#GC监控#Full GC排查
2026-09-16 · 13 阅读 · 0 评论 · 0 赞
连载中 10/20

ZGC:亚毫秒停顿是怎么炼成的

百 G 大堆停顿不到一毫秒,靠的是把搬家全部挪到并发阶段——着色指针让引用自带状态,读屏障让搬运中的对象依然可访问。代价是吞吐与内存,收益是停顿与堆大小解耦。

#ZGC#着色指针#读屏障#亚毫秒停顿#分代ZGC
2026-09-15 · 8 阅读 · 0 评论 · 0 赞