💳 RocketMQ事务消息:分布式事务的优雅解法!

34 阅读10分钟

副标题:半消息、本地事务、消息回查,三步走保证最终一致性!🎯


🎬 开场:分布式事务的噩梦

经典场景:电商下单 🛒:

用户下单购买商品,需要:
1. 创建订单(订单服务)
2. 扣减库存(库存服务)
3. 扣减余额(账户服务)
4. 发送优惠券(营销服务)

问题:如何保证这些操作要么全部成功,要么全部失败?

传统方案的问题

方案问题
分布式事务(2PC/3PC)性能差,阻塞,不适合高并发 ❌
TCC代码侵入性强,复杂度高 ❌
Saga需要补偿逻辑,实现复杂 ❌

RocketMQ事务消息

  • 性能高 ✅
  • 实现简单 ✅
  • 最终一致性 ✅

📚 什么是事务消息?

核心概念

事务消息(Transactional Message)

保证本地事务和消息发送的原子性

要么:本地事务成功 + 消息发送成功
要么:本地事务失败 + 消息不发送

避免了:
❌ 本地事务成功,但消息发送失败
❌ 消息发送成功,但本地事务失败

生活例子 💰

转账场景

普通转账(可能出问题):
1. 你的账户扣款 -100元  ✅
2. 发送"转账成功"通知
3. 对方账户到账 +100元  ❌ 失败了!

结果:你的钱扣了,对方没收到!💔

事务消息转账(安全):
1. 你的账户扣款 -100元  ✅
2. 如果扣款成功,才发送"转账成功"消息
3. 对方收到消息后,账户到账 +100元  ✅

结果:要么都成功,要么都失败!😊

🔑 RocketMQ事务消息原理

三个核心概念

1. 半消息(Half Message)

半消息:
- 已经发送到Broker
- 但消费者看不到
- 处于"待确认"状态

就像快递已经到了快递柜,但还没通知你取件

2. 消息状态

COMMIT_MESSAGE:    提交消息(消费者可以消费)
ROLLBACK_MESSAGE:  回滚消息(删除消息)
UNKNOW:           状态未知(需要回查)

3. 消息回查(Message Check)

如果Broker长时间收不到确认
就会主动询问生产者:
"这个事务到底成功没有?"

生产者查询本地事务状态并回复

完整流程图

生产者                  RocketMQ Broker              消费者
  │                          │                        │
  │  ① 发送半消息            │                        │
  ├──────────────────────→   │                        │
  │                          │ 存储半消息              │
  │                          │ (消费者看不到)          │
  │  ② 返回发送成功          │                        │
  │  ←──────────────────────┤                        │
  │                          │                        │
  │  ③ 执行本地事务          │                        │
  │     (扣款/创建订单等)     │                        │
  │                          │                        │
  │  ④ 提交/回滚消息         │                        │
  ├──────────────────────→   │                        │
  │                          │                        │
  │                          │  如果是COMMIT:         │
  │                          │  消息变为可见          │
  │                          ├────────────────────→   │
  │                          │                        │ ⑤ 消费消息
  │                          │                        │   (加库存等)
  │                          │                        │
  │  ⑥ 如果超时未收到确认     │                        │
  │  ←──────────────────────┤ 回查本地事务状态        │
  │     查询事务状态          │                        │
  ├──────────────────────→   │                        │
  │  返回COMMIT/ROLLBACK     │                        │
  │                          │                        │

💻 代码实现

1. 生产者发送事务消息

@Service
public class OrderTransactionProducer {
    
    @Autowired
    private RocketMQTemplate rocketMQTemplate;
    
    @Autowired
    private OrderService orderService;
    
    /**
     * 发送事务消息
     */
    public void sendTransactionMessage(OrderDTO orderDTO) {
        // 构建消息
        Message<OrderDTO> message = MessageBuilder
            .withPayload(orderDTO)
            .build();
        
        // 发送事务消息
        TransactionSendResult result = rocketMQTemplate.sendMessageInTransaction(
            "order-topic",           // topic
            message,                 // 消息
            orderDTO                 // 透传参数(传给本地事务)
        );
        
        log.info("事务消息发送结果: {}", result.getSendStatus());
    }
}

2. 实现事务监听器

/**
 * 事务消息监听器
 */
@RocketMQTransactionListener
public class OrderTransactionListener implements RocketMQLocalTransactionListener {
    
    @Autowired
    private OrderService orderService;
    
    @Autowired
    private TransactionLogService transactionLogService;
    
    /**
     * 执行本地事务
     * 
     * 这个方法在发送半消息成功后执行
     */
    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        try {
            // 1. 获取消息内容
            OrderDTO orderDTO = (OrderDTO) arg;
            String transactionId = msg.getHeaders().get("transactionId", String.class);
            
            log.info("开始执行本地事务: transactionId={}", transactionId);
            
            // 2. 记录事务日志(状态:处理中)
            TransactionLog log = new TransactionLog();
            log.setTransactionId(transactionId);
            log.setStatus(TransactionStatus.PROCESSING);
            log.setOrderData(JSON.toJSONString(orderDTO));
            transactionLogService.save(log);
            
            // 3. 执行本地事务
            Order order = orderService.createOrder(orderDTO);
            
            // 4. 更新事务日志(状态:成功)
            log.setStatus(TransactionStatus.SUCCESS);
            log.setOrderId(order.getId());
            transactionLogService.update(log);
            
            log.info("本地事务执行成功: orderId={}", order.getId());
            
            // 5. 返回COMMIT,消息将被投递给消费者
            return RocketMQLocalTransactionState.COMMIT;
            
        } catch (Exception e) {
            log.error("本地事务执行失败", e);
            
            // 更新事务日志(状态:失败)
            // ...
            
            // 6. 返回ROLLBACK,消息将被删除
            return RocketMQLocalTransactionState.ROLLBACK;
        }
    }
    
    /**
     * 回查本地事务状态
     * 
     * 如果Broker长时间没收到确认,会调用这个方法
     */
    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
        String transactionId = msg.getHeaders().get("transactionId", String.class);
        
        log.info("回查本地事务状态: transactionId={}", transactionId);
        
        // 1. 查询事务日志
        TransactionLog log = transactionLogService.getByTransactionId(transactionId);
        
        if (log == null) {
            // 事务日志不存在,可能还没来得及执行
            return RocketMQLocalTransactionState.UNKNOW;
        }
        
        // 2. 根据事务状态返回结果
        switch (log.getStatus()) {
            case SUCCESS:
                // 本地事务成功,提交消息
                return RocketMQLocalTransactionState.COMMIT;
                
            case FAILED:
                // 本地事务失败,回滚消息
                return RocketMQLocalTransactionState.ROLLBACK;
                
            case PROCESSING:
                // 本地事务还在处理中,返回UNKNOW
                // Broker会稍后再次回查
                return RocketMQLocalTransactionState.UNKNOW;
                
            default:
                return RocketMQLocalTransactionState.UNKNOW;
        }
    }
}

3. 消费者处理消息

/**
 * 订单消息消费者
 */
@Service
@RocketMQMessageListener(
    topic = "order-topic",
    consumerGroup = "order-consumer-group"
)
public class OrderConsumer implements RocketMQListener<OrderDTO> {
    
    @Autowired
    private InventoryService inventoryService;
    
    @Autowired
    private AccountService accountService;
    
    @Override
    public void onMessage(OrderDTO orderDTO) {
        log.info("收到订单消息: {}", orderDTO);
        
        try {
            // 1. 扣减库存
            inventoryService.deduct(
                orderDTO.getProductId(),
                orderDTO.getQuantity()
            );
            
            // 2. 扣减余额
            accountService.deduct(
                orderDTO.getUserId(),
                orderDTO.getAmount()
            );
            
            // 3. 发送优惠券
            couponService.send(orderDTO.getUserId());
            
            log.info("订单处理成功: orderId={}", orderDTO.getOrderId());
            
        } catch (Exception e) {
            log.error("订单处理失败", e);
            // 抛出异常,消息会重新消费
            throw new RuntimeException("订单处理失败", e);
        }
    }
}

4. 事务日志表设计

CREATE TABLE transaction_log (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    transaction_id VARCHAR(64) UNIQUE NOT NULL,  -- 事务ID
    status VARCHAR(20) NOT NULL,                 -- 状态:PROCESSING/SUCCESS/FAILED
    order_data TEXT,                             -- 订单数据
    order_id BIGINT,                             -- 订单ID
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_transaction_id (transaction_id),
    INDEX idx_status (status)
);

🔍 详细流程解析

正常流程(本地事务成功)

Step 1: 发送半消息
Producer  Broker: 我要发一条消息
Broker: 好的,我先存起来,但不让消费者看到

Step 2: 执行本地事务
Producer: 开始扣款...
Database: 扣款成功!

Step 3: 提交消息
Producer  Broker: 我的事务成功了,COMMIT这条消息
Broker: 好的,我把消息设为可见

Step 4: 消费消息
Consumer: 收到消息,开始加库存...
Database: 加库存成功!

结果:✅ 扣款成功 + 加库存成功

异常流程1(本地事务失败)

Step 1: 发送半消息
Producer  Broker: 我要发一条消息
Broker: 好的,我先存起来

Step 2: 执行本地事务
Producer: 开始扣款...
Database:  余额不足,扣款失败!

Step 3: 回滚消息
Producer  Broker: 我的事务失败了,ROLLBACK这条消息
Broker: 好的,我删除这条消息

结果:✅ 扣款失败 + 消息不发送(正确)

异常流程2(网络故障,需要回查)

Step 1: 发送半消息
Producer  Broker: 我要发一条消息
Broker: 好的,我先存起来

Step 2: 执行本地事务
Producer: 开始扣款...
Database: 扣款成功!

Step 3: 提交消息(网络故障)
Producer  Broker: 我的事务成功了,COMMIT... 
网络:  断了!
Broker: 没收到确认消息...

Step 4: 等待超时(默认6秒)
Broker: 等了6秒还没收到,我主动问一下吧

Step 5: 回查事务状态
Broker  Producer: 事务ID=123的事务成功了吗?
Producer: 让我查一下事务日志...
Database: 事务ID=123的状态是SUCCESS
Producer  Broker: 成功了,COMMIT

Step 6: 提交消息
Broker: 好的,我把消息设为可见

结果:✅ 扣款成功 + 消息发送成功(通过回查恢复)

🎯 核心要点

1. 为什么需要事务日志表?

// ❌ 错误做法:没有事务日志
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
    try {
        orderService.createOrder(orderDTO);
        return RocketMQLocalTransactionState.COMMIT;
    } catch (Exception e) {
        return RocketMQLocalTransactionState.ROLLBACK;
    }
}

@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
    // ❌ 无法知道之前的事务是否成功
    // 只能返回UNKNOW或瞎猜
    return RocketMQLocalTransactionState.UNKNOW;
}

// ✅ 正确做法:使用事务日志
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
    // 1. 记录事务日志
    transactionLogService.save(transactionId, PROCESSING);
    
    try {
        // 2. 执行业务
        orderService.createOrder(orderDTO);
        
        // 3. 更新事务日志
        transactionLogService.update(transactionId, SUCCESS);
        
        return RocketMQLocalTransactionState.COMMIT;
    } catch (Exception e) {
        transactionLogService.update(transactionId, FAILED);
        return RocketMQLocalTransactionState.ROLLBACK;
    }
}

@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
    // ✅ 查询事务日志,准确返回状态
    TransactionLog log = transactionLogService.get(transactionId);
    
    if (log.getStatus() == SUCCESS) {
        return RocketMQLocalTransactionState.COMMIT;
    } else {
        return RocketMQLocalTransactionState.ROLLBACK;
    }
}

2. 事务日志和业务操作必须在同一个事务

@Transactional
public void createOrder(OrderDTO orderDTO, String transactionId) {
    // 1. 记录事务日志
    TransactionLog log = new TransactionLog();
    log.setTransactionId(transactionId);
    log.setStatus(TransactionStatus.PROCESSING);
    transactionLogMapper.insert(log);  // 和业务在同一个事务
    
    // 2. 创建订单
    Order order = new Order();
    order.setUserId(orderDTO.getUserId());
    order.setAmount(orderDTO.getAmount());
    orderMapper.insert(order);
    
    // 3. 更新事务日志
    log.setStatus(TransactionStatus.SUCCESS);
    log.setOrderId(order.getId());
    transactionLogMapper.update(log);  // 和业务在同一个事务
    
    // 要么都成功,要么都失败
}

3. 回查频率和超时设置

# application.yml
rocketmq:
  producer:
    # 回查间隔时间(默认60秒)
    check-interval: 60000
    
    # 最大回查次数(默认15次)
    max-check-times: 15

回查时间线

0s:   发送半消息
6s:   第1次回查
66s:  第2次回查
126s: 第3次回查
...
15次回查后还是UNKNOW → 删除消息

💡 最佳实践

1. 幂等性设计

/**
 * 消费者必须保证幂等性
 * 因为消息可能重复消费
 */
@Service
public class OrderConsumerIdempotent implements RocketMQListener<OrderDTO> {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    @Override
    public void onMessage(OrderDTO orderDTO) {
        String messageId = orderDTO.getMessageId();
        String redisKey = "msg:processed:" + messageId;
        
        // 检查是否已处理
        Boolean isFirst = redisTemplate.opsForValue()
            .setIfAbsent(redisKey, "1", 7, TimeUnit.DAYS);
        
        if (isFirst != null && isFirst) {
            // 首次处理
            processOrder(orderDTO);
        } else {
            // 重复消息,跳过
            log.warn("重复消息,跳过: {}", messageId);
        }
    }
}

2. 事务日志定期清理

/**
 * 定期清理过期的事务日志
 */
@Scheduled(cron = "0 0 2 * * ?")  // 每天凌晨2点
public void cleanExpiredLogs() {
    // 删除30天前的已完成事务日志
    transactionLogMapper.deleteExpired(30);
    log.info("事务日志清理完成");
}

3. 监控和告警

/**
 * 监控事务消息状态
 */
@Component
public class TransactionMessageMonitor {
    
    @Scheduled(fixedDelay = 60000)  // 每分钟
    public void checkPendingTransactions() {
        // 查询处理中的事务(超过5分钟)
        List<TransactionLog> pending = transactionLogMapper
            .selectPendingTransactions(5);
        
        if (!pending.isEmpty()) {
            // 发送告警
            alertService.send(
                "事务消息告警",
                "有" + pending.size() + "个事务长时间未完成"
            );
        }
    }
}

🆚 与其他方案对比

事务消息 vs 本地消息表

维度事务消息本地消息表
实现复杂度简单 ⭐⭐复杂 ⭐⭐⭐⭐
性能高 ⭐⭐⭐⭐中 ⭐⭐⭐
依赖依赖MQ只依赖DB
消息可靠性
适用场景RocketMQ环境任何环境

本地消息表实现

// 需要额外的定时任务扫描本地消息表
@Scheduled(fixedDelay = 1000)
public void scanAndSend() {
    // 1. 查询待发送的消息
    List<LocalMessage> messages = messageMapper.selectPending();
    
    // 2. 逐条发送
    for (LocalMessage message : messages) {
        try {
            mqProducer.send(message.getTopic(), message.getContent());
            
            // 3. 标记为已发送
            message.setStatus(MessageStatus.SENT);
            messageMapper.update(message);
        } catch (Exception e) {
            log.error("消息发送失败", e);
        }
    }
}

// 比事务消息复杂很多!

🎯 面试高频问题

Q1:事务消息如何保证最终一致性?

A

  1. 半消息机制

    • 消息先发送但不可见
    • 本地事务执行完才提交
  2. 回查机制

    • 网络故障时通过回查恢复
    • 最多回查15次
  3. 事务日志

    • 记录本地事务状态
    • 回查时准确返回

Q2:如果本地事务成功但提交消息失败怎么办?

A

通过回查机制解决:

1. 本地事务成功(已记录事务日志)
2. 提交消息时网络故障
3. Broker超时未收到确认
4. Broker发起回查
5. 查询事务日志,发现是SUCCESS
6. 返回COMMIT,消息投递成功

最终结果:✅ 一致

Q3:事务消息有什么限制?

A

  1. 不支持延迟消息
  2. 不支持批量发送
  3. 单条消息大小限制4MB
  4. 回查次数限制15次
  5. 只支持RocketMQ

🎉 总结

核心要点 ✨

  1. 三个核心

    • 半消息(Half Message)
    • 本地事务执行
    • 消息回查(Check)
  2. 关键实现

    • 事务监听器
    • 事务日志表
    • 幂等性设计
  3. 适用场景

    • 订单创建
    • 账户变更
    • 积分变动

记忆口诀 📝

事务消息三步走,
半消息本地事务回查有。

第一步发半消息,
消息存储不可见。
第二步执行事务,
成功失败记日志。
第三步提交回滚,
网络故障回查救。

事务日志很重要,
回查状态全靠它。
幂等设计不能少,
重复消费要防好。

最终一致有保证,
分布式事务不用愁!

📚 参考资料

  1. RocketMQ官方文档 - 事务消息
  2. 分布式事务解决方案

最后送你一句话

"分布式事务没有银弹,选择适合的方案才是王道。"

愿你的事务消息稳定可靠,最终一致! 💳✨


表情包时间 🎭

没有事务消息前:
😱 本地事务成功了,消息发送失败了
💔 数据不一致了...

有了事务消息后:
😊 半消息保存好
✅ 本地事务执行
🎉 消息成功投递

开发者:
😎 终于不用担心数据不一致了!