分布式事务:使用RocketMQ来实现

欢迎关注微信公众号:互联网全栈架构

随着微服务的普及和落地,分布式事务就变成了一个绕不过去的坎,由于各个微服务操作的数据可能位于不同的数据库或者不同的机器上面,而这些操作要么全部成功,要么全部失败,所以就需要使用分布事务来进行处理。

比如用户在商城下单,就需要在订单服务中创建一个订单,同时在库存服务中扣减库存,而订单服务和库存服务往往连接的是不同的数据库,这就涉及到分布式事务了。

分布式事务相对复杂,而且也有多种解决方案,今天我们RocketMQ来实现分布式事务,达到最终一致性。文章模拟刚才提到的下单场景,提供尽可能全面的代码片段,供参考

RocketMQ提供了事务消息的功能,它大致分为如下几个阶段:

    • 生产者发送half消息,half能够被broker接收,但不能被消费;

    • 生产者发送half消息后,执行本地的数据库事务(比如生成订单数据);

    • 根据本地事务的执行结果,生产者向broker发送“提交”或者“回滚”的请求;如果本地事务执行成功,则发送“提交”,half消息就变成了消费端可以接收的正式消息,否则,发送“回滚”,half消息就被删除,从而不会触发后续的数据库事务(比如扣减库存)。

接下来就是代码部分,主要使用Spring Boot+MyBatis+RocketMQ。首先引入依赖,如果需要详细的代码部分,也可以与我私信。然后配置yml,也是较为常规的做法,同时也需要定义一些实体类、DAO、mapper文件等。

然后是订单服务的代码:

@Service
@Slf4j
publicclass OrderService {
    @Autowired
    private RocketMQTemplate rocketMQTemplate;
    @Autowired
    private OrderDao orderDao;

    privatestaticfinal String TX_ORDER_TOPIC = "order_tx_topic";
    privatestaticfinal String TX_DEDUCT_STOCK_TAG = "deduct_stock";

    public String createOrder(OrderCreateDTO createDTO) {
        String orderNo = generateOrderNo();

        // 构造事务消息
        OrderTxMessage txMessage = new OrderTxMessage(
                orderNo,
                createDTO.getProductId(),
                createDTO.getQuantity()
        );

        // 发送事务消息
        TransactionSendResult result = rocketMQTemplate.sendMessageInTransaction(
                TX_ORDER_TOPIC + ":" + TX_DEDUCT_STOCK_TAG,
                MessageBuilder.withPayload(txMessage)
                        .setHeader("order_no", orderNo)
                        .build(),
                createDTO
        );

        log.info("Transaction message sent: {}", result.getMsgId());
        return orderNo;
    }

    @Transactional
    public boolean handleLocalTransaction(OrderCreateDTO createDTO, String orderNo) {
        try {
            // 1. 创建订单(初始状态为0-创建中)
            Order order = new Order();
            order.setOrderNo(orderNo);
            order.setUserId(createDTO.getUserId());
            order.setProductId(createDTO.getProductId());
            order.setQuantity(createDTO.getQuantity());
            order.setAmount(createDTO.getAmount());
            order.setStatus(0);
            orderDao.insert(order);

            // 2. 预留其他本地事务操作(如扣减账户余额等)

            returntrue;
        } catch (Exception e) {
            log.error("Local transaction failed", e);
            thrownew RuntimeException("订单创建失败");
        }
    }

    public boolean checkLocalTransaction(String orderNo) {
        Order order = orderDao.selectByOrderNo(orderNo);
        return order != null && order.getStatus() != 0;
    }

    private String generateOrderNo() {
        return"ORD" + System.currentTimeMillis() + ThreadLocalRandom.current().nextInt(1000, 9999);
    }
}

接下来以及事务消息的监听器,它会根据本地事务执行的情况,返回对应的状态。如果本地事务执行失败,或者在代码里直接返回ROLLBACK或者UNKNOWN,那么消息端就接收不到消息:

@Component
@RocketMQTransactionListener
@Slf4j
publicclass OrderTransactionListener implements RocketMQLocalTransactionListener {

    @Autowired
    private OrderService orderService;
    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        try {
            MessageHeaders headers = msg.getHeaders();
            String orderNo = headers.get("order_no", String.class);
            OrderCreateDTO createDTO = (OrderCreateDTO) arg;
            System.out.println("executeLocalTransaction:" + createDTO);

            boolean success = orderService.handleLocalTransaction(createDTO, orderNo);

            if (success) {
                log.info("Local transaction committed: {}", orderNo);
                return RocketMQLocalTransactionState.COMMIT;
            }
            return RocketMQLocalTransactionState.ROLLBACK;
        } catch (Exception e) {
            log.error("Transaction execution failed", e);
            return RocketMQLocalTransactionState.ROLLBACK;
        }
    }

    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
        MessageHeaders headers = msg.getHeaders();
        String orderNo = headers.get("order_no", String.class);
        System.out.println("checkLocalTransaction:" + JSONObject.toJSONString(headers));

        try {
            boolean exists = orderService.checkLocalTransaction(orderNo);
            return exists ? RocketMQLocalTransactionState.COMMIT
                    : RocketMQLocalTransactionState.ROLLBACK;
        } catch (Exception e) {
            log.error("Transaction check failed", e);
            return RocketMQLocalTransactionState.UNKNOWN;
        }
    }
}

最后是库存的消费服务:

@Service
@RocketMQMessageListener(
        topic = "order_tx_topic",
        selectorExpression = "deduct_stock",
        consumerGroup = "inventory_consumer_group",
        consumeTimeout = 3000
)
@Slf4j
publicclass InventoryConsumer implements RocketMQListener<OrderTxMessage> {
    @Autowired
    private InventoryDao inventoryDao;
    @Autowired
    private OrderDao orderDao;

    @Override
    @Transactional
    public void onMessage(OrderTxMessage message) {
        String orderNo = message.getOrderNo();

        // 幂等性检查:订单是否已经处理过
        Order order = orderDao.selectByOrderNo(orderNo);
        if (order == null) {
            log.warn("Order not found: {}", orderNo);
            return;
        }
        if (order.getStatus() == 2) {
            log.info("Order already processed: {}", orderNo);
            return;
        }

        // 获取当前库存信息
        Inventory inventory = inventoryDao.selectByProductId(message.getProductId());
        if (inventory == null) {
            thrownew RuntimeException("商品不存在");
        }

        // 使用乐观锁扣减库存
        int affectedRows = inventoryDao.deductStock(
                message.getProductId(),
                message.getQuantity()
        );

        if (affectedRows == 0) {
            thrownew RuntimeException("库存扣减失败,请重试");
        }

        log.info("库存扣减成功,订单号:{}", orderNo);
    }
}

可以使用以下的测试类来进行测试:

@SpringBootTest
class OrderServiceTest {
    @Autowired
    private OrderService orderService;

    @Test
    void testCreateOrder() {
        OrderCreateDTO dto = new OrderCreateDTO();
        dto.setUserId("U1001");
        dto.setProductId("P2001");
        dto.setQuantity(2);
        dto.setAmount(new BigDecimal("199.99"));

        String orderNo = orderService.createOrder(dto);
        assertNotNull(orderNo);
    }
}

通过这样的方式,达到了最终一致性的目的。当然,如果订单服务提交成功,然而库存服务还是提交失败,这时候就需要重试或者通过人工介入的方式来解决。

欢迎添加我的微信(请备注“互联网全栈架构”),我们一起在提升编程技术这条路上精进:

图片

创作不易,烦请点个在看、点个赞。

有任何问题,也欢迎留言讨论。

推荐阅读:

Redis集群碎碎念:数据也能“跳槽”

Redis集群碎碎念:参数配置

Redis集群碎碎念:集群总线是个啥?

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值