事务消息是什么

你可以先把 事务消息 理解成一句话:

事务消息是用来保证"本地数据库操作"和"发送 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 后面会回查生产者。

最关键的是这句话:

事务消息保证的是"生产者本地事务成功后,消息才对消费者可见"。

不是保证消费者一定消费成功。

相关推荐
闪电悠米1 小时前
黑马点评-分布式锁-03_lua_atomic_unlock
java·数据库·分布式·缓存·oracle·wpf·lua
码不停蹄的玄黓1 小时前
MySQL唯一索引能否做主键索引
数据库·sql·mysql
段一凡-华北理工大学1 小时前
工业领域的Hadoop架构学习~系列文章09:HBase列式数据库
数据库·人工智能·hadoop·架构·hbase·高炉炼铁·高炉炼铁智能化
江畔柳前堤1 小时前
XZ09_Word和MD格式转换
开发语言·数据库·人工智能·python·深度学习·word
移动云开发者联盟1 小时前
信创版图加速扩展!移动云云数据库Redis通过安全可信认证
数据库·安全
小马爱打代码1 小时前
SpringBoot + 本地缓存 + 布隆过滤器:防止恶意 ID 查询打穿数据库
数据库·spring boot·缓存
憧憬成为java架构高手的小白1 小时前
数据库期末复习笔记
数据库·笔记·oracle
10WTW011 小时前
个人思考记录(一)What u need in AI era
数据库·mongodb