经典两难:先写库还是先发消息
第 2 篇立过一块碑:订单落库和消息发送是两个系统的事,数据库事务管不到 MQ。把这个两难摆开:
- 先写库,再发消息:两步之间进程崩溃,库里有订单,消息永远没发出去——下游全不知情;
- 先发消息,再写库:消息发出去了,落库失败——下游拿着一条幽灵订单开始发积分。
第 5 篇的本地消息表是工程界的第一答案:把消息记录塞进业务库,用同一个事务捆住。RocketMQ 则把这个思路内置到了 Broker 里,叫事务消息(半消息机制)。
半消息机制的四步舞
完整流程是这样:
1 生产者 -> 发送半消息(half message) -> Broker 存储
<- 返回成功,但此时消息对消费者不可见
2 生产者 -> 执行本地事务(订单落库)
3 生产者 -> 根据事务结果,提交(消息可见)或回滚(消息删除)
4 Broker -> 若迟迟等不到二次确认,主动回查生产者:那笔事务到底成没成?关键设计在第一步和第四步。半消息先存进系统内部 Topic(RMQ_SYS_TRANS_HALF_TOPIC),消费者看不见——这就同时规避了「发早了」和「发丢了」:事务成功前它不存在于业务视野,事务成功后它必然已安全落盘。
代码长什么样
TransactionMQProducer producer = new TransactionMQProducer("order_group");
producer.setTransactionListener(new TransactionListener() {
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
orderService.create((Order) arg); // 本地事务
return LocalTransactionState.COMMIT_MESSAGE; // 成功,放行
} catch (Exception e) {
return LocalTransactionState.ROLLBACK_MESSAGE;
}
}
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
// 回查:查数据库的真实状态,绝不查内存变量
boolean exists = orderMapper.exists(msg.getKeys());
return exists ? LocalTransactionState.COMMIT_MESSAGE
: LocalTransactionState.UNKNOW; // 拿不准就再等等,别乱回滚
}
});
producer.sendMessageInTransaction(msg, order);回查的两个要点
第一,回查逻辑必须查库。回查发生时,发消息的那个进程可能已经重启了八回,内存里的标志位早没了。唯一可信的依据是数据库里事务的最终状态——这也要求业务表设计上能表达「这笔业务成没成」。
第二,拿不准就返回 UNKNOW。查库超时、主从延迟、状态还是中间态,都返回 UNKNOW 让 Broker 稍后再问。回查有次数上限(默认 15 次),超限后 Broker 丢弃半消息并记日志——配好告警,别让消息悄无声息地死掉。
事务消息和本地消息表怎么选
| 对比项 | 事务消息 | 本地消息表 |
|---|---|---|
| 存储位置 | Broker 内部 Topic | 业务库自己的表 |
| 补偿机制 | Broker 主动回查 | 自己写定时任务扫描 |
| 代码侵入 | 改用事务生产者+两个回调 | 业务代码里多写一张表+任务 |
| 运维成本 | 零额外组件,但绑定 RocketMQ | 任何 MQ 都能用,通用性强 |
| 可见性 | 消息状态在 MQ 控制台可查 | SQL 直接查,排查直观 |
选型不复杂:RocketMQ 用户直接享受事务消息的现成回查;多 MQ 混用或想保留排查直观性的,本地消息表依然是利器。两者的可靠性是同一档的,区别只是「谁替你做补偿」。
它保证什么,不保证什么
事务消息保证的是:本地事务成功,消息一定送达 Broker 且最终对消费者可见——本地事务与「发消息」这个动作的原子性。它不保证下游执行成功:积分服务收到消息还是可能失败、可能重复。所以下游的老三样一个不能少:消费重试、幂等、死信告警。事务消息只是把最难的「第一跳」焊死了,整条链路的可靠性仍然是各跳各自负责。
小结
半消息机制的精髓:先把消息「藏」起来保证不存在假消息,事务成功后再「放」出来保证不会丢消息,Broker 回查兜住一切意外。回查必须查库,拿不准就 UNKNOW,两条写进代码评审清单。
生产端的课题到这里全部完结。下一站轮到消费端:消费失败与重试——重试队列怎么工作,死信队列里的消息谁来管,以及一条毒消息的自救流程。
评论 (0)