事务型消息

在进行秒杀项目的开发过程中,遇到了一个问题,下单-库存减少
异步处理库存,下单后,进行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 bodyMap = new HashMap<>();
        bodyMap.put("itemId", itemId);
        bodyMap.put("amount", amount);
        bodyMap.put("stockLogId", stockLogId);

        Map 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 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 的库存还是会减掉,并且会存在不一致性,这个后面就要讨论,根据运营进行调整

你可能感兴趣的:(Java)