你可以先把 事务消息 理解成一句话:
事务消息是用来保证"本地数据库操作"和"发送 MQ 消息"尽量保持一致的。
它解决的不是"消费者一定成功消费"的问题,而是解决:
text
我数据库里的业务操作成功了,MQ 消息也应该成功发出去。
我数据库里的业务操作失败了,MQ 消息就不应该被消费者看到。
1. 先看普通消息有什么问题
假设你有一个下单接口:
text
1. 创建订单,写入数据库
2. 发送 MQ 消息,通知库存系统扣库存
代码可能像这样:
java
public void createOrder() {
// 1. 创建订单,写数据库
orderService.saveOrder();
// 2. 发送 MQ 消息
rocketMQTemplate.convertAndSend("order_topic:create", "订单创建成功");
}
看起来没问题,但它有两个风险。
情况一:订单创建成功了,但消息发送失败了
text
订单写入数据库成功 ✅
发送 MQ 消息失败 ❌
结果就是:
text
数据库里有订单
但是库存系统不知道
所以库存没有扣
这就出现了数据不一致。
情况二:消息发送成功了,但订单创建失败了
如果你先发消息,再写数据库:
java
public void createOrder() {
// 1. 先发消息
rocketMQTemplate.convertAndSend("order_topic:create", "订单创建成功");
// 2. 再创建订单
orderService.saveOrder();
}
可能出现:
text
MQ 消息发送成功 ✅
订单写入数据库失败 ❌
结果就是:
text
库存系统收到消息,开始扣库存
但是数据库里根本没有这个订单
这也不对。
2. 所以事务消息要解决什么?
事务消息就是要解决这个问题:
text
本地事务,比如创建订单
MQ 消息发送
这两个动作要配合起来。
它想达到的效果是:
text
订单创建成功 -> 消息才能被消费者消费
订单创建失败 -> 消息不能被消费者消费
3. RocketMQ 事务消息的核心思想
RocketMQ 不是一上来就把消息给消费者。
它会先发一条特殊消息,叫:
text
半消息
Half Message
你可以理解为:
text
消息先放到 RocketMQ 里,但是暂时不让消费者看到。
就像你寄快递时,先把快递放到快递站,但告诉快递站:
text
先别派送,等我确认。
4. 事务消息完整流程
假设订单系统创建订单,然后通知库存系统扣库存。
流程是这样:
text
1. 订单系统先发送半消息到 RocketMQ
2. RocketMQ 保存半消息,但消费者暂时看不到
3. 订单系统执行本地事务,也就是创建订单
4. 如果订单创建成功,告诉 RocketMQ:提交消息
5. RocketMQ 把消息变成可消费状态
6. 库存系统才能消费消息,开始扣库存
如果订单创建失败:
text
1. 订单系统先发送半消息到 RocketMQ
2. RocketMQ 保存半消息,但消费者暂时看不到
3. 订单系统执行本地事务,创建订单失败
4. 告诉 RocketMQ:回滚消息
5. RocketMQ 删除/丢弃这条消息
6. 库存系统永远看不到这条消息
5. 用生活例子理解
你把 RocketMQ 想成快递站。
普通消息是:
text
你把快递交给快递站
快递站马上派送
事务消息是:
text
你把快递交给快递站
但是贴了一个"暂不派送"的标签
然后你去确认一件事:
text
钱有没有付成功?
订单有没有创建成功?
数据库有没有写成功?
如果确认成功:
text
你告诉快递站:可以派送了
如果确认失败:
text
你告诉快递站:这个快递取消,不要派送
所以事务消息的关键不是"消费者消费成功",而是:
text
消费者看到消息之前,先确认生产者自己的本地事务成功了。
6. 为什么叫"事务消息"?
因为它和数据库事务有关。
数据库事务是:
text
要么都成功
要么都失败
比如转账:
text
A 扣 100
B 加 100
不能只扣 A,不加 B。
而 RocketMQ 事务消息想解决的是:
text
数据库操作
MQ 消息投递
这两个动作的一致性问题。
比如:
text
订单入库成功
订单消息也应该发出去
不能出现:
text
订单入库成功,但是消息没发出去
也不能出现:
text
订单没入库,但是消息发出去了
7. 事务消息和普通消息的区别
普通消息:
text
消息一发出去,消费者就可能收到。
事务消息:
text
消息先进入 RocketMQ,但消费者暂时看不到。
等本地事务成功后,消息才会被消费者看到。
对比一下:
text
普通消息:
生产者 -> Broker -> 消费者
事务消息:
生产者 -> 半消息 -> 执行本地事务 -> 提交消息 -> 消费者
8. 最核心的三个状态
RocketMQ 事务消息有三个结果:
text
COMMIT:提交消息,消费者可以消费
ROLLBACK:回滚消息,消费者看不到
UNKNOWN:暂时不知道结果,RocketMQ 后面会回查
这三个很重要。
1)COMMIT
本地事务成功。
比如:
text
订单创建成功
于是告诉 RocketMQ:
text
这条消息可以投递给消费者了。
2)ROLLBACK
本地事务失败。
比如:
text
订单创建失败
于是告诉 RocketMQ:
text
这条消息不要了,消费者不应该看到。
3)UNKNOWN
生产者执行本地事务后,可能因为网络问题、服务宕机等原因,没有告诉 RocketMQ 到底成功还是失败。
这时 RocketMQ 不知道该提交还是回滚。
于是它会问生产者:
text
你刚才那条消息对应的本地事务到底成功了吗?
这个过程叫:
text
事务回查
9. 什么是事务回查?
这是事务消息里最容易懵的地方。
假设流程走到这里:
text
1. 半消息发送成功
2. 订单系统创建订单成功
3. 订单系统准备告诉 RocketMQ:提交消息
4. 结果订单系统突然宕机了
RocketMQ 此时很尴尬:
text
我这里有一条半消息
但是我不知道订单到底创建成功没有
怎么办?
RocketMQ 会过一段时间主动问订单系统:
text
这条消息对应的订单,到底有没有创建成功?
订单系统就去查数据库:
text
查一下 orderId = 10001 的订单是否存在
如果存在:
text
返回 COMMIT
如果不存在:
text
返回 ROLLBACK
这就是事务回查。
10. 用订单例子完整串起来
你发起下单:
text
用户点击下单
RocketMQ 事务消息流程:
text
第一步:订单系统发送半消息
消息内容:订单 10001 创建成功
但是消费者暂时看不到
第二步:订单系统执行本地事务
往订单表插入订单 10001
第三步:判断本地事务结果
如果订单插入成功:
提交消息 COMMIT
库存系统可以消费消息
如果订单插入失败:
回滚消息 ROLLBACK
库存系统看不到消息
如果订单系统宕机,不知道结果:
RocketMQ 后面回查订单系统
订单系统查数据库
有订单 -> COMMIT
没订单 -> ROLLBACK
11. 小白版流程图
text
订单系统
|
| 1. 发送半消息
v
RocketMQ
|
| 2. 先保存,但不投递
|
订单系统
|
| 3. 创建订单到数据库
v
MySQL
|
| 4. 判断订单是否创建成功
|
|--- 成功 ---> 告诉 RocketMQ 提交消息 ---> 消费者可以消费
|
|--- 失败 ---> 告诉 RocketMQ 回滚消息 ---> 消费者看不到
|
|--- 未知 ---> RocketMQ 后面回查订单系统
12. 事务消息解决了什么?没解决什么?
它解决的是:
text
生产者本地事务 和 MQ 消息发送 的一致性
比如:
text
订单创建成功,消息一定尽量发出去
订单创建失败,消息不要被消费
但它不解决:
text
消费者一定消费成功
消费者消费失败怎么办?
还是靠:
text
消费失败重试
幂等
死信队列
人工补偿
你之前问的"生产者消息发送出去了,消费者已经完全消费消息了",这件事不是事务消息直接保证的。RocketMQ 里 Producer、Consumer、Topic、Group、Broker 的关系是:Producer 把消息发到 Broker,Consumer 再根据 Topic 和 Group 去消费消息。
13. 为什么不是直接用数据库事务包住 MQ 发送?
你可能会想:
java
@Transactional
public void createOrder() {
orderService.saveOrder();
rocketMQTemplate.convertAndSend("order_topic:create", "订单创建成功");
}
这样不就行了吗?
不完全行。
因为数据库事务只能管数据库,管不了 RocketMQ。
比如:
text
数据库事务提交成功了
但是 MQ 发送失败了
数据库没法自动回滚,因为事务已经提交了。
或者:
text
MQ 发送成功了
但是数据库事务回滚了
消息已经发出去了,消费者可能已经开始处理了。
所以:
text
数据库事务不能天然保证 MQ 消息一致性
RocketMQ 才引入事务消息。
14. 代码大概长什么样?
你不用现在完全看懂代码,先看注释。
发送事务消息:
java
rocketMQTemplate.sendMessageInTransaction(
"order_topic:create",
MessageBuilder.withPayload(orderMessage).build(),
orderMessage
);
然后写一个事务监听器:
java
@Component
@RocketMQTransactionListener
public class OrderTransactionListener implements RocketMQLocalTransactionListener {
/**
* 执行本地事务
* 这里通常写:创建订单、写数据库
*/
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object arg) {
try {
// 1. 执行本地事务:创建订单
orderService.createOrder(arg);
// 2. 本地事务成功,提交消息
return RocketMQLocalTransactionState.COMMIT;
} catch (Exception e) {
// 3. 本地事务失败,回滚消息
return RocketMQLocalTransactionState.ROLLBACK;
}
}
/**
* 事务回查
* 当 RocketMQ 不知道本地事务结果时,会调用这里
*/
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
// 1. 根据订单ID查询数据库
boolean orderExists = orderService.exists(message);
// 2. 如果订单存在,说明本地事务成功
if (orderExists) {
return RocketMQLocalTransactionState.COMMIT;
}
// 3. 如果订单不存在,说明本地事务失败
return RocketMQLocalTransactionState.ROLLBACK;
}
}
你目前只要看懂这两个方法:
text
executeLocalTransaction:执行本地事务
checkLocalTransaction:事务回查
15. 再用一句话总结
事务消息就是:
text
先把消息发到 RocketMQ,但先不让消费者看到;
然后执行本地数据库事务;
如果数据库事务成功,就提交消息,让消费者消费;
如果数据库事务失败,就回滚消息,不让消费者消费;
如果中间状态不确定,RocketMQ 后面会回查生产者。
最关键的是这句话:
事务消息保证的是"生产者本地事务成功后,消息才对消费者可见"。
不是保证消费者一定消费成功。