事务型消息

在秒杀项目开发中,面对下单与库存异步处理问题,采用Springboot结合RocketMQ事务解决。通过TransactionMQProducer监听,利用executeLocalTransaction和checkLocalTransaction处理事务。流程包括:controller调用mq事务,消息队列异步扣减库存,库存流水状态标记,防止库存击穿。但目前流程中仍存在Redis库存不一致性的挑战。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在进行秒杀项目的开发过程中,遇到了一个问题,下单-库存减少
异步处理库存,下单后,进行redis操作,订单入库,销量增加,异步操作数据库库存
一、直接使用Springboot提供方法

// Springboot 提供的事务操作方法 当前事务操作成功 方法调用
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
          @Override
            public void afterCommit() {
                // 异步更新库存
                boolean mqResult = itemService.asyncDecreaseStock(itemId, amount);
                if (!mqResult) {
                   itemService.increaseStock(itemId, amount);
                                       throw new BusinessException(EmBusinessError.MQ_SEND_FAIL);
               }
           }
        });

二、通过库存流水进行状态标识,配合rocketmq事务

  • RocketMQ TransactionMQProducer 设置listener
  • executeLocalTransaction 进行事务方法的处理
  • checkLocalTransaction 进行异常事务的处理

具体的操作逻辑
1、controller 进行 mq异步事务调用

if (redisTemplate.hasKey("promo_stock_invalid_" + itemId)) {
            throw new BusinessException(EmBusinessError.STOCK_NOT_ENOUGH);
        }
        // 加入库存流水
        String stockLogResultId = itemService.initStockLog(itemId, amount);

        // 完成下单事务消息
        if (!mqProducer.transactionAsyncReduceStock(userModel.getId(), itemId, promoId, amount, stockLogResultId)) {
            throw new BusinessException(EmBusinessError.UNKNOW_ERROR,"下单失败"); 
    }

2、通过消息队列 事务型异步扣减库存

// 事务型异步库存扣减消息
    public boolean transactionAsyncReduceStock(String userId, Integer itemId, Integer promoId, Integer amount, String stockLogId) {
        Map<String, Object> bodyMap = new HashMap<>();
        bodyMap.put("itemId", itemId);
        bodyMap.put("amount", amount);
        bodyMap.put("stockLogId", stockLogId);

        Map<String, Object> argsMap = new HashMap<>();
        argsMap.put("itemId", itemId);
        argsMap.put("amount", amount);
        argsMap.put("promoId", promoId);
        argsMap.put("userId", userId);
        argsMap.put("stockLogId", stockLogId);
        Message message = new Message(topicName, "increase", JSON.toJSON(bodyMap).toString().getBytes(Charset.forName("UTF-8")));
        TransactionSendResult transactionSendResult = null;
        try {
            transactionSendResult = transactionMQProducer.sendMessageInTransaction(message, argsMap);
        } catch (MQClientException e) {
            e.printStackTrace();
            return false;
        }

        if (transactionSendResult.getLocalTransactionState() == LocalTransactionState.COMMIT_MESSAGE) {
            return true;
        } else {
            return false;
        }
    }

3、回到 transactionMQProducer的监听中 进行逻辑处理


        transactionMQProducer = new TransactionMQProducer("transaction_producer_group");
        transactionMQProducer.setNamesrvAddr(nameAddr);
        transactionMQProducer.start();
        transactionMQProducer.setTransactionListener(new TransactionListener() {
            @Override
            public LocalTransactionState executeLocalTransaction(Message message, Object args) {
                // 真正要做的事 创建订单
                String userId = (String)((Map)args).get("userId");
                Integer itemId = (Integer)((Map)args).get("itemId");
                Integer amount = (Integer)((Map)args).get("amount");
                Integer promoId = (Integer)((Map)args).get("promoId");
                String stockLogId = (String)((Map)args).get("stockLogId");
                try {
                    orderService.createOrder(userId, itemId, promoId, amount, stockLogId);
                } catch (BusinessException e) {
                    e.printStackTrace();

                    // 状态设置为回滚状态
                    StockLogDO stockLogDO = stockLogDOMapper.selectByPrimaryKey(stockLogId);
                    stockLogDO.setStatus(3);
                    stockLogDOMapper.updateByPrimaryKey(stockLogDO);

                    return LocalTransactionState.ROLLBACK_MESSAGE;
                }
                return LocalTransactionState.COMMIT_MESSAGE;
            }

            @Override
            public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
                // 根据是否扣减库存成功 判断是否 commit rollback unknown
                String jsonString = new String(messageExt.getBody());
                Map<String, Object> map = JSON.parseObject(jsonString, Map.class);
                Integer itemId = (Integer) map.get("itemId");
                Integer amount = (Integer) map.get("amount");
                String stockLogId = (String) map.get("stockLogId");
                StockLogDO stockLogDO = stockLogDOMapper.selectByPrimaryKey(stockLogId);
                if (stockLogDO == null) {
                    return LocalTransactionState.UNKNOW;
                }

                if (stockLogDO.getStatus() == 2) {
                    return LocalTransactionState.COMMIT_MESSAGE;
                } else if (stockLogDO.getStatus() == 1) {
                    return LocalTransactionState.UNKNOW;
                }

                return LocalTransactionState.ROLLBACK_MESSAGE;
            }
        });

3、库存流水状态标记 库存击穿
通过库存流水表 可以查到每一单的 订单状态 再进行库存的对于处理

// 初始化库存流水
    @Override
    public String initStockLog(Integer itemId, Integer amount) {
        StockLogDO stockLogDO = new StockLogDO();
        stockLogDO.setItemId(itemId);
        stockLogDO.setAmount(amount);
        stockLogDO.setStatus(1);
        stockLogDO.setStockLogId(UUID.randomUUID().toString().replace("-", ""));

        stockLogDOMapper.insert(stockLogDO);

        return stockLogDO.getStockLogId();
    }

// 加入库存流水
        String stockLogResultId = itemService.initStockLog(itemId, amount);





@Override
    @Transactional
    public boolean decreaseStock(Integer itemId, Integer amount) throws BusinessException {
        // 操作失败 结果是0
//        int affectedRow = itemStockDOMapper.decreaseStock(itemId, amount);
        long result = redisTemplate.opsForValue().increment("promo_stock"+itemId, amount.intValue() * -1);
        if (result > 0) {
            return true;
        } else if (result == 0) {
            redisTemplate.opsForValue().set("promo_stock_invalid_" + itemId, "true");
            return true;
        } else {
            increaseStock(itemId, amount);
            return false;
        }
    }


if (redisTemplate.hasKey("promo_stock_invalid_" + itemId)) {
            throw new BusinessException(EmBusinessError.STOCK_NOT_ENOUGH);
        }

4、注意
目前的流程,虽然说mysql库存的问题得到了解决,但是redis 的库存还是会减掉,并且会存在不一致性,这个后面就要讨论,根据运营进行调整

### 实现和使用 RocketMQ 事务消息 #### 创建生产者并初始化事务监听器 为了在 RocketMQ 中实现事务消息,首先需要创建一个 `TransactionListener` 接口的实例。该接口有两个主要的方法:一个是用于执行本地事务逻辑;另一个是在消息状态不确定的情况下由 MQ 调用来确认事务状态。 ```java public class TransactionProducer { private static final Logger logger = LoggerFactory.getLogger(TransactionProducer.class); public void init() throws MQClientException { // 初始化事务监听器 TransactionListener transactionListener = new TransactionListenerImpl(); // 创建事务消息生产者组名,并指定 name server 地址 TransactionMQProducer producer = new TransactionMQProducer("please_rename_unique_group_name"); producer.setNamesrvAddr("localhost:9876"); // 注册事务监听器 producer.setTransactionListener(transactionListener); // 启动生产者 producer.start(); } } ``` #### 执行本地事务逻辑 当发送半消息(即准备阶段的消息)成功之后,会触发本地事务处理函数,在这个过程中应该完成具体的业务逻辑操作,比如更新数据库记录等。需要注意的是,此时并不立即提交这条消息给消费者端消费,而是等待后续的状态回调通知再做进一步动作[^2]。 ```java class TransactionListenerImpl implements TransactionListener { @Override public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { String[] strs = new String(msg.getBody()).split("_"); int value = Integer.parseInt(strs[strs.length - 1]); // 假设这里是实际要做的业务逻辑... if (value >= 0 && value < 50) { return LocalTransactionState.COMMIT_MESSAGE; } else if (value >= 50 && value < 100){ return LocalTransactionState.ROLLBACK_MESSAGE; } return LocalTransactionState.UNKNOW; } ... } ``` #### 处理未知状态的消息 如果由于网络波动等原因导致无法及时获取到确切的结果,则会在一定时间间隔后收到一次来自 Broker 的查询请求,这时就需要根据实际情况再次判断当前事务的具体情况并向其反馈最终结果[^3]。 ```java @Override public LocalTransactionState checkLocalTransaction(MessageExt msg) { // 这里可以根据msg中的信息去检查对应的本地事务状态, // 并返回相应的枚举值表示是否已经完成了整个流程。 // 如果仍然不清楚则继续返回UNKNOW让Broker稍后再试 // 示例代码省略具体实现细节... return LocalTransactionState.UNKNOW; } ``` 通过上述方式可以在 RocketMQ 上面构建起一套完整的分布式事务管理体系,从而确保即使在网络不稳定或者其他异常情况下也能够保持数据之间的一致性和可靠性[^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值