秒杀项目系列之八: 交易性能优化技术之事务型消息(保证最终一致性利器:rocketmq的两段式事务提交协议+事务回查)

  1. 截止到目前,下单方案
  • 验证下单状态(下单商品是否存在、用户是否合法、购买数量是否正确)
  • 验证秒杀活动信息
  • 落单减库存(减redis库存和回补redis库存、rocketmq消息异步写入数据库)
  • 订单入库
  • 更改销量
  1. 目前存在问题
    如果落单减库存操作成功,但是之后的操作中,比如订单入库失败,由于rocketmq消息已经消费成功也不能回滚,则会导致多扣库存的问题.

  2. 改进方案
    那么把rocketmq异步发送消息放在事务的最后面呢?
    还有可能存在由于内存不足等原因导致事务commit失败的问题,事务回滚,但是消息已经被消费,还是会多扣库存.

  3. 再次改进
    那么把rocketmq异步发送消息放在事务提交之后呢?
    如果发送消息发送失败,但是事务已经提交不能再回滚,那么会导致库存没有扣减成功,但是订单已经生成.最后导致订单数多于库存数.

  4. 再次改进

  • 方案
    采用2PC(two-phase commit protocol两段式提交协议)+补偿机制(事务回查)的分布式事务功能,通过消息队列 RocketMQ 版事务消息能达到分布式事务的最终一致。
  • 2PC两段式提交协议流程图
    在这里插入图片描述
  • 秒杀环境中的具体实现方案
    • 将库存扣减信息和增加销量信息发送半消息(将消息写入到一个特定的topic中,而不是真正的消费端接收的topic中)
    • 执行创建订单的事务,创建订单中的扣减库存和增加销量的操作都是在redis中
    • 如果创建订单事务操作成功,返回COMMIT_MESSAGE,则投递消息,消费端消费消息
    • 如果创建订单事务操作失败,返回ROLLBACK_MESSAGE,事务回滚,消息放弃
    • 如果既没有返回COMMIT_MESSAGE也没有返回ROLLBACK_MESSAGE,而是处于UNKNOW状态,则使用checkLocalTransaction()方法进行回查,最多回查15次,间隔1分钟,如果仍然处于UNKNOW状态,则放弃该消息.
    • checkLocalTransaction()的回查机制具体是通过消息中的item_id查看库存流水中的状态信息,状态1表示初始状态,返回UNKNOW; 2表示下单扣减库存成功,返回COMMIT_MESSAGE; 3表示下单回滚, 返回ROLLBACK_MESSAGE.
  1. 库存数据库最终一致性保证
  • 方案
    • 引入库存操作流水
    • 引入rocketmq事务型消息机制
  • 存在的问题
    • redis不可用时如何处理
    • 扣减流水错误如何处理
  • 根据业务场景决定高可用技术的实现
    • 宁可少卖,不能超卖原则(实在没货,也无法调货,超卖发不了货会导致投诉)
    • 方案
      • redis可以比实际数据库中少
        redis若产生问题,不能回源到数据库,因为这时数据库的状态不一定正常,可能还有数据没有异步写入到数据库.
      • 超时释放
        在rocketmq事务型消息创建订单时,若卡在这,既没有返回COMMIT_MESSAGE也没有返回ROLLBACK_MESSAGE,那么等一段时间后,就需要超时释放,将库存加回去.
  1. 库存售罄
  • 当前存在的问题
    即使库存售罄,还是可以执行初始化库存流水initStockLog()的操作
  • 解决方案
    • 设置库存售罄标识,并将该标识达到缓存上
    • 售罄后不去操作后续流程,包括初始化流水操作,直接返回前端库存售罄,下单失败
    • 售罄后通知各系统售罄
      rocketmq异步消息通知的方式实现,各系统清除商品缓存,并回源获取数据库库存.
    • 回补上新
      当商家补库存的时候,要清除售罄标识.
  1. 核心代码具体实现
  • MqProducer.java消息生产者代码

    @PostConstruct
    public void init() throws MQClientException {
          
          
        transactionMQProducer = new TransactionMQProducer("transaction_producer_group");
        transactionMQProducer.setNamesrvAddr(nameAddr);
        transactionMQProducer.start();
        transactionMQProducer.setTransactionListener(new TransactionListener() {
          
          
            @Override
    //   在发送消息后调用,用于执行本地事务,如果本地事务执行成功,rocketmq再提交消息
            public LocalTransactionState executeLocalTransaction(Message message, Object args) {
          
          
                Integer userId = (Integer) ((Map)args).get("userId");
                Integer promoId = (Integer) ((Map)args).get("promoId");
                Integer itemId = (Integer) ((Map)args).get("itemId");
                Integer amount = (Integer) ((Map)args).get("amount");
                String stockLogId = (String) ((Map)args).get("stockLogId");
                // 真正要做的事:创建订单
                try {
          
          
                    orderService.createOrder(userId, itemId, promoId, amount, stockLogId);
                } catch (BusinessException e) {
          
          
                    e.printStackTrace();
                    // 抛出任何异常都要回滚
                    // 设置对应的stocklog为回滚状态
                    StockLogDO stockLogDO = stockLogDOMapper.selectByPrimaryKey(stockLogId);
                    stockLogDO.setStatus(3);
                    stockLogDOMapper.updateByPrimaryKeySelective(stockLogDO);
                    return LocalTransactionState.ROLLBACK_MESSAGE;
                }
                return LocalTransactionState.COMMIT_MESSAGE;
            }
    
            // 如果消息发送状态是UNKNOW,则调用该方法进行回查,最多回查15次,间隔1分钟
            @Override
            public LocalTransactionState checkLocalTransaction(MessageExt msg) {
          
          
                // 根据是否扣减库存成功,来判断是否返回ROLLBACK_MESSAGE还是COMMIT_MESSAGE,或者是继续UNKNOWN
                String jsonString = new String(msg.getBody());
                Map<String, Object> map = JSON.parseObject(jsonString, Map.class);
                String stockLogId = (String) map.get("stockLogId");
                StockLogDO stockLogDO = stockLogDOMapper.selectByPrimaryKey(stockLogId);
                if(stockLogDO == null){
          
          
                    return LocalTransactionState.UNKNOW;
                }
                // 2表示下单扣减库存成功
                if(stockLogDO.getStatus() == 2){
          
          
                    return LocalTransactionState.COMMIT_MESSAGE;
                    // 1表示初始状态,说明卡在orderService.createOrder()那了
                }else if(stockLogDO.getStatus() == 1){
          
          
                    return LocalTransactionState.UNKNOW;
                }
                return LocalTransactionState.ROLLBACK_MESSAGE;
            }
        });
    }
    
    // 事务型同步库存扣减消息
    public Boolean transactionAsyncReduceStockAndAddSales(Integer userId, Integer itemId, Integer promoId, Integer amount, String stockLogId) {
          
          
    // bodyMap为rocketmq要传输的消息
        Map<String, Object> bodyMap = new HashMap<>();
        bodyMap.put("itemId", itemId);
        bodyMap.put("amount", amount);
        bodyMap.put("stockLogId", stockLogId);
    // argsMap为其他的一些参数
        Map<String, Object> argsMap = new HashMap<>();
        argsMap.put("userId", userId);
        argsMap.put("promoId", promoId);
        argsMap.put("itemId", itemId);
        argsMap.put("amount", amount);
        argsMap.put("stockLogId", stockLogId);
        Message message = new Message(topicName, "increase",
                JSON.toJSON(bodyMap).toString().getBytes(StandardCharsets.UTF_8));
        TransactionSendResult transactionSendResult = null;
        try {
          
          
            transactionSendResult = transactionMQProducer.sendMessageInTransaction(message, argsMap);
        } catch (MQClientException e) {
          
          
            e.printStackTrace();
            return false;
        }
        if(transactionSendResult.getLocalTransactionState() == LocalTransactionState.ROLLBACK_MESSAGE){
          
          
            return false;
        }else if(transactionSendResult.getLocalTransactionState() == LocalTransactionState.COMMIT_MESSAGE){
          
          
            return true;
        }
        return false;
    }
    
  • OrderServiceImpl.java创建订单代码

    @Override
    @Transactional
    public OrderModel createOrder(Integer userId, Integer itemId, Integer promoId, Integer amount, String stockLogId) throws BusinessException {
          
          
        // 1.校验下单状态,下单的商品是否存在,用户是否合法,购买数量是否正确
    //        ItemModel itemModel = itemService.getItemById(itemId);
        ItemModel itemModel = itemService.getItemByIdInCache(itemId);
        if(itemModel == null){
          
          
            throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "商品信息不存在");
        }
    //        UserModel userModel = userService.getUserById(userId);
        UserModel userModel = userService.getUserByIdInCache(userId);
        if(userModel == null){
          
          
            throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "用户未注册");
        }
        if(amount <= 0 || amount > 99){
          
          
            throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "数量信息不正确");
        }
        // 校验秒杀活动信息
        if(promoId != null){
          
          
            // 校验该商品是否有活动
            if(promoId.intValue() != itemModel.getPromoModel().getId()){
          
          
                throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "活动信息不正确");
                // 校验活动是否正在进行中
            }else if(itemModel.getPromoModel().getStatus() != 2){
          
          
                throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "活动未开始");
            }
        }
        // 2.落单减库存
        boolean result = itemService.decreaseStock(itemId, amount);
        if(!result){
          
          
            throw new BusinessException((EmBusinessError.STOCK_NOT_ENOUGH));
        }
    
        // 3.订单入库
        OrderModel orderModel = new OrderModel();
        orderModel.setItemId(itemId);
        orderModel.setUserId(userId);
        orderModel.setAmount(amount);
        if(promoId != null){
          
          
            orderModel.setItemPrice(itemModel.getPromoModel().getPromoItemPrice());
        }else{
          
          
            orderModel.setItemPrice(itemModel.getPrice());
        }
        orderModel.setPromoId(promoId);
        orderModel.setOrderPrice(orderModel.getItemPrice().multiply(BigDecimal.valueOf(amount)));
        // 生成交易流水号(订单号)
        orderModel.setId(generateOrderNo());
    
        OrderDO orderDO = convertFromOrderModel(orderModel);
        orderDOMapper.insertSelective(orderDO);
    
        // 4. 商品销量增加,先增加到缓存中,然后通过rocketmq事务消息机制发送消息
        itemService.increaseSales(itemId, amount);
    
        // 设置库存流水状态为成功
        StockLogDO stockLogDO = stockLogDOMapper.selectByPrimaryKey(stockLogId);
        if(stockLogDO == null){
          
          
            throw new BusinessException(EmBusinessError.UNKNOWN_ERROR);
        }
        // status为2表示扣减库存成功
        stockLogDO.setStatus(2);
        stockLogDOMapper.updateByPrimaryKeySelective(stockLogDO);
    
        // 5. 返回前端
        return orderModel;
    }
    
  • decreaseStock()落单减库存代码

    @Override
    @Transactional
    public boolean decreaseStock(Integer itemId, Integer amount) throws BusinessException {
          
          
    //        int affectedRow = itemStockDOMapper.decreaseStock(itemId, amount);
        // 减缓存中的库存操作,返回剩余库存数量
        long result = redisTemplate.opsForValue().increment("promo_item_stock_" + itemId, amount * -1);
        if(result > 0){
          
          
            return true;
    //            打上库存售罄标识
        }else if(result == 0){
          
          
            redisTemplate.opsForValue().set("promo_item_stock_invalid_" + itemId, "true");
    //            返回更新库存成功
            return true;
        }else{
          
          
            // 更新库存失败,将库存补回去
            increaseStock(itemId, amount);
            return false;
        }
    }
    
  • increaseSales()增加销量代码

    @Override
    @Transactional
    public void increaseSales(Integer itemId, Integer amount) {
          
          
        redisTemplate.opsForValue().increment("promo_item_sales_" + itemId, amount);
    }
    
  • MqConsumer.java消息消费者代码

    package com.kenai.mq;
    import com.alibaba.fastjson.JSON;
    import com.kenai.dao.ItemDOMapper;
    import com.kenai.dao.ItemStockDOMapper;
    import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
    import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
    import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
    import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
    import org.apache.rocketmq.client.exception.MQClientException;
    import org.apache.rocketmq.common.message.Message;
    import org.apache.rocketmq.common.message.MessageExt;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Component;
    import javax.annotation.PostConstruct;
    import javax.annotation.Resource;
    import java.util.List;
    import java.util.Map;
    
    @Component
    public class MqConsumer {
          
          
        private DefaultMQPushConsumer consumer;
    
        @Resource
        private ItemStockDOMapper itemStockDOMapper;
    
        @Resource
        private ItemDOMapper itemDOMapper;
    
        @Value("${mq.nameserver.addr}")
        private String nameAddr;
    
        @Value("${mq.topicname}")
        private String topicName;
    
        @PostConstruct
        public void init() throws MQClientException {
          
          
            consumer = new DefaultMQPushConsumer("stock_consumer_group");
            // consumer连接nameserver
            consumer.setNamesrvAddr(nameAddr);
            // consumer订阅所有stock topic消息
            consumer.subscribe(topicName, "*");
            // 当消息推送过来之后的处理方式
            consumer.registerMessageListener(new MessageListenerConcurrently() {
          
          
                @Override
                public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
          
          
                    // 实现库存真正到数据库内扣减的逻辑
                    Message msg = msgs.get(0);
                    String jsonString = new String(msg.getBody());
                    Map<String, Object> map = JSON.parseObject(jsonString, Map.class);
                    Integer itemId = (Integer) map.get("itemId");
                    Integer amount = (Integer) map.get("amount");
                    itemStockDOMapper.decreaseStock(itemId, amount);
                    itemDOMapper.increaseSales(itemId, amount);
                    // 说明该消息已经被消费
                    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                }
            });
            consumer.start();
        }
    }
    
  • publishPromo()发布促销活动及初始化redis销量和库存代码

    /**
     * 发布促销活动
     * @param promoId
     */
    @Override
    public void publishpromo(Integer promoId) {
          
          
        // 通过活动id获取活动
        PromoDO promoDO = promoDOMapper.selectByPrimaryKey(promoId);
        if(promoDO.getItemId() == null || promoDO.getItemId() == 0){
          
          
            return;
        }
        ItemModel itemModel = itemService.getItemById(promoDO.getItemId());
        // 将库存同步到redis中
        redisTemplate.opsForValue().set("promo_item_stock_" + itemModel.getId(), itemModel.getStock());
        // 将销量同步到redis中
        redisTemplate.opsForValue().set("promo_item_sales_" + itemModel.getId(), itemModel.getSales());
    }
    

猜你喜欢

转载自blog.csdn.net/qq_26496077/article/details/113529644