RocketMQ事务消息实现订单创建 + 扣减库存

如果通过事务消息实现"订单创建 + 扣减库存"?

发送方

java 复制代码
//创建消息发送者,同时设置2个监听,一个是本地事务(在这里我们可以创建订单,根据订单的创建,我们返回成功和失败),一个是回查方法。
TransactionMQProducer producer = new TransactionMQProducer("ORDER_TRANSACTION_GROUP");
producer.setNamesrvAddr(nameServer);

// 设置事务监听器
producer.setTransactionListener(new TransactionListener() {
    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        return orderService.executeLocalTransaction(msg, arg); //executeLocalTransaction返回的结果,来决定消费者是否收到消息。
    }
    
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        return orderService.checkLocalTransaction(msg);
    }
});
java 复制代码
@Transactional
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
    OrderCreateRequest request = (OrderCreateRequest) arg;
    
    try {
        // 1. 创建订单记录,状态为"待处理"
        Order order = new Order();
        order.setOrderId(request.getOrderId());
        order.setUserId(request.getUserId());
        order.setProductId(request.getProductId());
        order.setQuantity(request.getQuantity());
        order.setStatus(OrderStatus.PENDING.getCode()); // 初始状态
        order.setCreateTime(new Date());
        
        orderMapper.insert(order);
        
        // 2. 这里可以添加其他本地业务逻辑...
        
        // 本地事务执行成功,提交消息
        return LocalTransactionState.COMMIT_MESSAGE;
        
    } catch (Exception e) {
        // 本地事务执行失败,回滚消息
        return LocalTransactionState.ROLLBACK_MESSAGE;
    }
}


/**
 * 事务回查:Broker未收到事务状态时调用
 */
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
    try {
        // 解析消息中的订单ID
        String body = new String(msg.getBody(), StandardCharsets.UTF_8);
        Map<String, Object> messageBody = JSON.parseObject(body, Map.class);
        String orderId = (String) messageBody.get("orderId");
        
        // 查询订单状态
        Order order = orderMapper.selectByOrderId(orderId);
        
        if (order == null) {
            // 订单不存在,回滚消息
            return LocalTransactionState.ROLLBACK_MESSAGE;
        }
        
        // 根据订单状态决定是否提交消息
        if (order.getStatus() == OrderStatus.PENDING.getCode()) {
            // 订单处于待处理状态,认为本地事务已提交
            return LocalTransactionState.COMMIT_MESSAGE;
        } else {
            // 订单已取消或其他状态,回滚消息
            return LocalTransactionState.ROLLBACK_MESSAGE;
        }
        
    } catch (Exception e) {
        // 回查异常,返回未知状态,Broker会继续回查
        return LocalTransactionState.UNKNOW;
    }
}

RocketMQ 事务消息的执行流程如下:

当调用 transactionProducer.sendMessageInTransaction方法时,会依次执行以下操作:

发送半消息(这个是自动做的):首先向 Broker 发送一条"半消息",该消息对消费者不可见

执行本地事务:半消息发送成功后(只有发送成功才会进到这里),自动回调 executeLocalTransaction方法,在此方法中执行订单创建等本地业务逻辑

返回事务状态:根据本地事务执行结果,向 Broker 返回 COMMIT 或 ROLLBACK 状态

异常处理机制:
如果订单创建成功后,Broker 发生宕机导致无法接收状态确认,当 Broker 恢复后,会通过事务回查机制主动查询事务状态。此时在回查方法中,我们可以根据订单是否存在返回相应的事务状态,确保消息的最终一致性。

消费端

接下来就是库存扣减:扣减成功才给MQ返回消息成功,否则就是失败。失败没关系,会一直重试。当然也要加个逻辑,如果是库存没了,那也应该是成功。不然一直重试。

java 复制代码
@Service  
public class InventoryService {
    
    @Autowired
    private InventoryMapper inventoryMapper;
    
    /**
     * 消费订单创建消息,扣减库存
     */
    @RabbitListener(selector = "TAGS='CREATE_TAG'")
    public ConsumeConcurrentlyStatus consumeOrderCreateMessage(List<MessageExt> messages,
                                                             ConsumeConcurrentlyContext context) {
        for (MessageExt message : messages) {
            try {
                // 1. 解析消息
                String body = new String(message.getBody(), StandardCharsets.UTF_8);
                Map<String, Object> orderInfo = JSON.parseObject(body, Map.class);
                
                String orderId = (String) orderInfo.get("orderId");
                String productId = (String) orderInfo.get("productId");
                Integer quantity = (Integer) orderInfo.get("quantity");
                
                // 2. 幂等性检查:通过订单ID判断是否已处理过
                if (isMessageProcessed(orderId)) {
                    System.out.println("消息已处理,跳过。orderId: " + orderId);
                    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                }
                
                // 3. 执行库存扣减
                boolean success = deductInventory(productId, quantity, orderId);
                
                if (success) {
                    // 4. 记录消息处理状态,用于幂等性
                    recordMessageProcessed(orderId);
                    System.out.println("库存扣减成功,orderId: " + orderId);
                    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                } else {
                    // 库存不足,重试
                    System.out.println("库存扣减失败,等待重试。orderId: " + orderId);
                    return ConsumeConcurrentlyStatus.RECONSUME_LATER;
                }
                
            } catch (Exception e) {
                // 记录日志,返回重试
                System.err.println("处理消息异常: " + e.getMessage());
                return ConsumeConcurrentlyStatus.RECONSUME_LATER;
            }
        }
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }
    
    /**
     * 扣减库存(保证幂等性)
     */
    @Transactional
    public boolean deductInventory(String productId, Integer quantity, String orderId) {
        // 检查是否已处理过该订单
        if (inventoryMapper.isOrderProcessed(orderId)) {
            return true;
        }
        
        // 检查库存是否充足
        Inventory inventory = inventoryMapper.selectByProductId(productId);
        if (inventory.getAvailableQuantity() < quantity) {
            return false;
        }
        
        // 扣减库存
        int rows = inventoryMapper.deductInventory(productId, quantity);
        if (rows == 0) {
            return false;
        }
        
        // 记录库存操作流水(用于幂等性检查和对账)
        InventoryOperation operation = new InventoryOperation();
        operation.setOrderId(orderId);
        operation.setProductId(productId);
        operation.setQuantity(quantity);
        operation.setType("DEDUCT");
        operation.setCreateTime(new Date());
        inventoryMapper.insertOperation(operation);
        
        return true;
    }
    
    private boolean isMessageProcessed(String orderId) {
        // 查询库存操作流水表,判断订单是否已处理
        return inventoryMapper.isOrderProcessed(orderId);
    }
    
    private void recordMessageProcessed(String orderId) {
        // 记录消息已处理(在deductInventory方法中已记录)
    }
}
相关推荐
百***58142 小时前
Spring Boot 2.7.x 至 2.7.18 及更旧的版本,漏洞说明
java·spring boot·后端
q***64972 小时前
Spring BOOT 启动参数
java·spring boot·后端
百***78452 小时前
Java实战:Spring Boot application.yml配置文件详解
java·网络·spring boot
你不是我我3 小时前
【Java 开发日记】SQL 语句左连接右连接内连接如何使用,区别是什么?
java·javascript·数据库
七夜zippoe3 小时前
Java性能调优工具篇:JMH基准测试与Profiler(JProfiler/Async-Profiler)使用指南
java·开发语言·jprofiler·jmh·async-profiler
從南走到北3 小时前
JAVA国际版二手车交易二手车市场系统源码支持Android+IOS+H5+APP
android·java·ios
Kuo-Teng3 小时前
LeetCode 19: Remove Nth Node From End of List
java·数据结构·算法·leetcode·链表·职场和发展·list
北i3 小时前
TiDB 关联子查询去关联优化实战案例与原理深度解析
java·数据库·sql·tidb
Kuo-Teng3 小时前
LeetCode 21: Merge Two Sorted Lists
java·算法·leetcode·链表·职场和发展