连载中 5/20

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

2026-07-31 · 2965 阅读 · 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)

热门推荐

连载中 11/22

主从搭建实操:从零配出一主两从

光讲原理不过瘾?手把手搭一主两从:my.cnf 六个参数、复制账号、GTID、CHANGE REPLICATION SOURCE TO、SHOW REPLICA STATUS 验收,附翻车排查清单。

#MySQL#主从复制#GTID#主从搭建#高可用
2026-05-07 · 10100 阅读 · 0 评论 · 0 赞
连载中 16/22

连接池:HikariCP 参数与连接风暴

连接池不是越大越好:8 核机器配 1000 连接反而更慢的数学原理,HikariCP 四个必调参数,maxLifetime 与 wait_timeout 的隐形陷阱。

#MySQL#连接池#HikariCP#maxLifetime#连接风暴
2026-05-10 · 9872 阅读 · 0 评论 · 0 赞
连载中 4/16

缓存穿透:恶意 ID 打穿 MySQL 的四道防线

请求的数据在缓存和数据库里都不存在时,缓存形同虚设。聊聊参数校验、空值缓存、布隆过滤器、限流熔断四道防线的原理与组合打法。

#Redis#缓存穿透#布隆过滤器#高可用
2026-05-16 · 9293 阅读 · 21 评论 · 287 赞