RocketMQ事务消息核心原理:解决售货柜下单、扣库存、支付数据一致性

事务消息核心原理:解决售货柜下单、扣库存、支付数据一致性

作者:黒漂技术佬 适用读者:了解RocketMQ基本用法,想搞懂分布式事务消息的同学

一、分布式事务的难题

先抛一个售货柜场景,你感受一下问题有多棘手。

用户扫码开门,拿走一瓶可乐,关门扣款。后台流程:

markdown 复制代码
1. 订单服务:创建订单记录
2. 库存服务:扣减可乐库存
3. 出货服务:向设备下发出货指令

这三个操作分布在三个服务、三个数据库。你要保证:要么全成功,要么全回滚。但分布式环境下,网络会抖、服务会挂、数据库会超时------你怎么保证三件事一起成功或一起失败?

1.1 传统方案的困境

方案 做法 问题
2PC / XA 协调者问所有参与者"能不能提交",全部同意才提交 性能极差(长时间锁资源),不适合微服务
TCC Try-Confirm-Cancel三阶段,每个服务写三套逻辑 开发成本极高,每个操作要写3个方法
本地消息表 本地事务+消息表同库写入,定时任务扫表发MQ 实现复杂,有延迟,要维护消息表
最大努力通知 发完消息就不管了 不可靠,可能丢数据

RocketMQ的事务消息提供了一种更优雅的方案------不需要额外消息表,不写TCC三套逻辑,利用MQ自身的两阶段提交机制搞定。


二、RocketMQ事务消息原理

2.1 核心思路:两阶段提交

事务消息的精髓在于**Half消息(半消息)**机制。简单说:

  • 第一阶段 :生产者先发一条"半消息"给Broker,Broker存下来但对消费者不可见
  • 第二阶段:生产者执行本地事务,根据结果决定是"提交"还是"回滚"这条半消息

小白疑问:什么叫"对消费者不可见"?

半消息写入Broker后,不会被构建到ConsumeQueue中(或者标记为不可消费),消费者拉取不到。只有当生产者确认"提交"后,半消息才变成正常消息,消费者才能消费。

2.2 完整流程图解

sql 复制代码
Producer                    Broker                    Consumer
   │                          │                          │
   │ ① 发送Half消息            │                          │
   │─────────────────────────→│                          │
   │                          │ 存储Half消息              │
   │                          │ (消费者不可见)             │
   │ ② Half消息发送成功        │                          │
   │←─────────────────────────│                          │
   │                          │                          │
   │ ③ 执行本地事务             │                          │
   │ (写数据库等)               │                          │
   │                          │                          │
   │ ④ 本地事务成功→Commit     │                          │
   │   本地事务失败→Rollback   │                          │
   │─────────────────────────→│                          │
   │                          │                          │
   │  如果Commit:              │                          │
   │                          │ ⑤ 半消息变为正常消息       │
   │                          │─────────────────────────→│ 消费者消费
   │                          │                          │
   │  如果Rollback:            │                          │
   │                          │ ⑤ 删除半消息(标记删除)     │
   │                          │                          │ 消费者无感知
   │                          │                          │
   │  如果超时未确认:           │                          │
   │                          │ ⑥ Broker主动回查           │
   │←─────────────────────────│                          │
   │ ⑦ 回查本地事务状态         │                          │
   │─────────────────────────→│                          │
   │  返回COMMIT/ROLLBACK/UNKNOWN                          │

2.3 事务回查机制

如果第④步因为网络问题,生产者没有告诉Broker是提交还是回滚,Broker不会让这条消息永远悬着。Broker会定期回查生产者:"你那条半消息的本地事务到底成功了没?"

生产者收到回查请求后,检查本地事务状态,返回三种结果之一:

  • COMMIT_MESSAGE:本地事务成功,提交半消息
  • ROLLBACK_MESSAGE:本地事务失败,回滚半消息
  • UNKNOWN:不确定,Broker稍后再回查

默认回查15次,如果15次都返回UNKNOWN,Broker就回滚这条消息。

这个设计很巧妙:即使生产者宕机了,Broker通过回查也能最终确定消息状态,不会出现"半消息永远悬着"的问题。


三、代码实现

3.1 事务消息生产者

java 复制代码
@Slf4j
@Service
public class OrderTransactionProducer {

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    /**
     * 发送事务消息
     * @param order 订单信息
     */
    public void sendOrderTransactionMessage(Order order) {
        Message<Order> message = MessageBuilder
                .withPayload(order)
                .setHeader("KEYS", order.getOrderId())
                .build();

        // 事务消息专用方法:syncSendOrderTransaction
        // 参数1:topic
        // 参数2:消息体
        // 参数3:事务参数(传给本地事务执行器的arg参数)
        TransactionSendResult result = rocketMQTemplate.sendMessageInTransaction(
            "transaction_order_topic",
            message,
            order  // 这个参数会传给 executeLocalTransaction 方法的 arg
        );

        log.info("事务消息发送完成: orderId={}, localTransactionState={}",
                 order.getOrderId(), result.getLocalTransactionState());
    }
}

3.2 事务监听器:核心逻辑

这是事务消息的心脏------两个方法分别处理"执行本地事务"和"事务回查"。

java 复制代码
@Slf4j
@Component
@RocketMQTransactionListener
public class OrderTransactionListener
        implements RocketMQLocalTransactionListener {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private InventoryService inventoryService;

    /**
     * 执行本地事务(半消息发送成功后调用)
     * 这里做实际的业务操作:创建订单 + 扣库存
     */
    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(
            Message msg, Object arg) {

        Order order = (Order) arg;
        String orderId = order.getOrderId();

        try {
            log.info("执行本地事务: orderId={}", orderId);

            // ① 创建订单
            orderMapper.insert(order);

            // ② 扣减库存
            boolean stockOk = inventoryService.deduct(
                order.getProductId(), order.getQuantity());
            if (!stockOk) {
                // 库存不足,回滚
                log.warn("库存不足,回滚事务: orderId={}", orderId);
                return RocketMQLocalTransactionState.ROLLBACK;
            }

            // 本地事务成功,提交半消息
            log.info("本地事务成功,提交半消息: orderId={}", orderId);
            return RocketMQLocalTransactionState.COMMIT;

        } catch (Exception e) {
            log.error("本地事务执行异常,回滚: orderId={}", orderId, e);
            return RocketMQLocalTransactionState.ROLLBACK;
        }
    }

    /**
     * 事务回查(Broker长时间未收到确认时调用)
     * 根据本地事务实际状态返回结果
     */
    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
        // 从消息中提取orderId
        String orderId = (String) msg.getHeaders().get("KEYS");

        log.info("收到事务回查请求: orderId={}", orderId);

        // 查数据库:订单是否已经创建
        Order order = orderMapper.selectByOrderId(orderId);

        if (order != null && order.getStatus() != null) {
            // 订单已存在且状态有效,说明本地事务成功了
            log.info("回查: 订单已存在, 返回COMMIT: orderId={}", orderId);
            return RocketMQLocalTransactionState.COMMIT;
        }

        // 订单不存在,说明本地事务没成功
        log.info("回查: 订单不存在, 返回ROLLBACK: orderId={}", orderId);
        return RocketMQLocalTransactionState.ROLLBACK;
    }
}

3.3 消费者(出货服务)

消费者端和普通消费者一模一样,无感知事务消息的存在:

java 复制代码
@Slf4j
@Component
@RocketMQMessageListener(
    topic = "transaction_order_topic",
    consumerGroup = "shipment-transaction-consumer-group",
    messageModel = MessageModel.CLUSTERING
)
public class ShipmentTransactionConsumer
        implements RocketMQListener<Order> {

    @Autowired
    private DeviceService deviceService;

    @Override
    public void onMessage(Order order) {
        log.info("出货服务收到事务消息: orderId={}, deviceId={}",
                 order.getOrderId(), order.getDeviceId());

        // 消费到这条消息,说明本地事务一定成功了
        // 安全地执行出货
        deviceService.sendShipmentCommand(
            order.getDeviceId(),
            order.getProductId(),
            order.getQuantity()
        );

        log.info("出货指令已发送: orderId={}", order.getOrderId());
    }
}

四、无人售货柜场景实战

把上面的代码串起来,看售货柜完整的事务消息流程。

4.1 业务场景

复制代码
用户开门拿货 → 创建订单 + 扣库存(本地事务)→ 确认后 → 下发出货指令

核心要求:订单和库存要么同时成功,要么同时失败。出货指令只有在订单和库存都成功后才发送。

4.2 为什么用事务消息而不是普通消息?

用普通消息的方案:

java 复制代码
// 方案A:先写数据库再发消息
orderMapper.insert(order);
inventoryService.deduct(...);
rocketMQTemplate.syncSend("shipment_topic", order);
// 问题:发消息失败怎么办?订单和库存已经改了,没法回滚
java 复制代码
// 方案B:先发消息再写数据库
rocketMQTemplate.syncSend("shipment_topic", order);
orderMapper.insert(order);
// 问题:消息发出去了,数据库写入失败,出货服务已经在出货了,但订单不存在

用事务消息的方案:

java 复制代码
// 事务消息:先发半消息 → 执行本地事务 → 根据结果提交/回滚半消息
rocketMQTemplate.sendMessageInTransaction(
    "transaction_order_topic", message, order);

这样保证了:本地事务和消息发送的原子性。半消息提交了,本地事务一定成功了;本地事务失败了,半消息被回滚,消费者收不到消息。

4.3 异常场景分析

异常 处理方式 数据一致性
半消息发送失败 本地事务不执行,无影响 一致
半消息发送成功,本地事务失败 返回ROLLBACK,半消息删除 一致
半消息发送成功,本地事务成功,提交确认丢失 Broker回查→返回COMMIT→半消息提交 一致
消费者消费失败 RocketMQ重试机制,多次失败进死信队列 最终一致
生产者宕机 Broker回查超时→15次后回滚 一致(出货指令不发)

事务消息不能保证100%的强一致性,但能保证最终一致性。对于售货柜场景足够了------最坏情况是出货指令晚一点发送,不会出现"扣了库存没出货"或"出了货没扣库存"的错位。


五、事务消息 vs 普通消息+本地消息表

两种方案都能解决分布式事务问题,对比一下:

对比项 事务消息 本地消息表
实现复杂度 中等(实现两个方法) 高(维护消息表+定时任务)
消息延迟 极低(本地事务成功即提交) 有延迟(定时任务扫描间隔)
额外存储 不需要额外表 需要一张本地消息表
数据库压力 无额外压力 消息表读写增加压力
适用场景 RocketMQ原生支持,推荐 不支持事务消息的MQ
回查机制 Broker自动回查 无,靠定时任务补偿

结论:用RocketMQ就选事务消息,没必要再造本地消息表的轮子。


六、事务消息使用注意事项

6.1 本地事务要幂等

回查机制可能导致 executeLocalTransaction 被调用时业务已经执行过(比如第一次Commit确认丢失,Broker回查时本地事务其实已经成功了)。所以本地事务要做好幂等:

java 复制代码
public RocketMQLocalTransactionState executeLocalTransaction(
        Message msg, Object arg) {
    Order order = (Order) arg;

    // 幂等检查:订单是否已存在
    if (orderMapper.selectByOrderId(order.getOrderId()) != null) {
        return RocketMQLocalTransactionState.COMMIT;
    }

    // 正常执行
    orderMapper.insert(order);
    inventoryService.deduct(order.getProductId(), order.getQuantity());
    return RocketMQLocalTransactionState.COMMIT;
}

6.2 回查方法要查数据库,不要依赖内存

checkLocalTransaction 方法在生产者重启后会被调用,内存状态丢失。必须查数据库判断本地事务状态。

6.3 事务消息不适合高吞吐场景

事务消息比普通消息多了一轮交互(半消息→确认),性能有损耗。不要在心跳上报、日志类高频场景用事务消息,那是对性能的浪费。事务消息只用在需要保证数据一致性的关键业务上。

6.4 Topic要单独隔离

事务消息的Topic不要和普通消息混用,避免管理混乱。建议命名带 transaction_ 前缀。


七、总结

事务消息的核心就是两阶段提交 + 事务回查

sql 复制代码
Half消息(不可见)→ 执行本地事务 → Commit/Rollback → 消费者可见/不可见
                                            │
                                     超时未确认?
                                            │
                                    Broker回查本地事务状态

三个角色各司其职:

  • 生产者:执行本地事务 + 返回事务状态
  • Broker:存半消息 + 定期回查 + 根据确认决定消息可见性
  • 消费者:正常消费,无感知事务过程

售货柜场景里,下单→扣库存→出货这条链路用事务消息,保证"扣了库存一定会出货,没扣库存一定不出货"。这是分布式系统数据一致性的经典解法,也是RocketMQ区别于其他MQ的杀手锏。

相关推荐
阿拉斯攀登1 小时前
无人售货柜设备消息体系设计:心跳上报、故障告警、状态同步
架构
阿拉斯攀登1 小时前
无人售货机订单业务MQ架构:创建订单、支付回调、出货完成、订单归档
架构
微三云 - 廖会灵 (私域系统开发)1 小时前
抖店 OPC(Operational Process Control)自动化运营系统:架构、痛点与服务商商业体系分析
运维·架构·自动化
jianqiang.xue1 小时前
收官:从v0.1到“研发新常态“,一套嵌入式智能体的落地与边界
stm32·单片机·物联网·架构·esp32
阿拉斯攀登2 小时前
SpringBoot整合RocketMQ:生产者、消费者快速搭建
架构
楚兴3 小时前
ACP 到底解决了什么?让 IDE 和 Coding Agent 解耦
人工智能·后端·架构
Rescenix3 小时前
流式瀑布渐变闪烁根治-硬核技术版
架构·github
楚兴3 小时前
DeepSeek Harness 到底在做什么?拆开 Agent 的运行时
人工智能·后端·架构
贺公子之数据科学与艺术3 小时前
向量数据库实操:Milvus 与 Chroma 的 Collection、索引、相似度度量与标量过滤
架构