预约活动(秒杀)项目中如何高效的保证下单交易成功?保证redis,mysql的最终一致性?

预约活动(秒杀)项目中如何高效的保证下单交易成功?保证redis,mysql的最终一致性?

在这里插入图片描述


前言

`在秒杀项目中,秒杀下单的过程中都需要经历四步,分别为:
1.校验下单状态,商品是否存在,用户是否合法,购买数量是否正确
2.落单减库存,支付减库存
3.订单入库
4.返回前端

这个也就是秒杀项目中的优化核心。下面来讲解是如何进行秒杀过程优化的。
参考工程连接地址:https://gitee.com/llbnk/book_activity_platform


提示:以下是本篇文章正文内容,下面案例可供参考

一、redis引入

1.redis实现对用户账户缓存和商品缓存

可以看到最开始的项目,没有redis的引入,是需要进行多次查询数据库操作。
可以看到在校验环节查询了三次表一次Item表,一次userInfo表,一次promo活动信息表。
第二扣库存环节扣减了stock表的库存。所以需要操作两次stock表一次查询一次。
第三订单入库相当于插入order表这么一次mysql操作。
一共6次操作。

代码

OrderController

//订单创建
    @PostMapping(value = "/order/createOrder", consumes = {CONTENT_TYPE_FORMED})
    public CommonReturnType createItem(@RequestParam(name = "itemId")Integer itemId,
                                       @RequestParam(name = "amount")Integer amount,
                                       @RequestParam(name = "promoId",required = false)Integer promoId) throws BusinessException {

        //判断是否登录
        Boolean isLogin = (Boolean) httpServletRequest.getSession().getAttribute("IS_LOGIN");
        if(isLogin == null || !isLogin){
            throw new BusinessException(EmBusinessError.USER_NOT_LOGIN, "请登录后再进行预约");
        }

        //获取用户的登录信息
        UserModel userModel = (UserModel) httpServletRequest.getSession().getAttribute("LOGIN_USER");
        if(redisTemplate.hasKey("promo_item_stock_invalid_"+itemId)){
            throw new BusinessException(EmBusinessError.STOCK_NOT_ENOUGH);
        }

        OrderModel orderModel = orderService.createOrder(userModel.getId(), itemId, promoId, amount);
		 return CommonReturnType.create(null);
    }

OrderServiceImpl

	//promoId非空表示已预约活动价格下单(也就是秒杀商品Id)
    public OrderModel createOrder(Integer userId, Integer itemId, Integer promoId, Integer amount) throws BusinessException {

        //1.校验下单状态,商品是否存在,用户是否合法,购买数量是否正确
        ItemModel itemModel = itemService.getItemById(itemId);
        if (itemModel == null) {
            throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "商品信息不存在");
        }

        UserModel userModel = userService.getUserById(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.equals(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);
        orderModel.setItemPrice(itemModel.getPrice());
        //orderModel.setOrderPrice(itemModel.getPrice().multiply(new BigDecimal(amount)));
        //orderModel.setOrderPrice(itemModel.getPrice().multiply(new BigDecimal(amount)));

        if (promoId != null) {
            //商品价格取特价
            orderModel.setItemPrice(itemModel.getPromoModel().getPromoItemPrice());
        } else {
            orderModel.setItemPrice(itemModel.getPrice());
        }
        orderModel.setPromoId(promoId);
        orderModel.setOrderPrice(orderModel.getItemPrice().multiply(new BigDecimal(amount)));

        //生成交易流水号
        orderModel.setId(generateOrderNo());

        OrderDO orderDO = convertFromOrderModel(orderModel);
        orderDOMapper.insertSelective(orderDO);

        //加上商品的销量
        itemService.increaseSales(itemId, amount);

        //4.返回前端
        return orderModel;
    }

如果由redis预先缓存user信息,和item信息相当于在第一步校验下单的过程省去了两次查表,这就极大的提升了下单的速度。
OrderServiceImpl

 		@Transactional
    promoId非空表示已预约活动价格下单
    public OrderModel createOrder(Integer userId, Integer itemId, Integer promoId,
                                  Integer amount,String stockLogId) throws BusinessException {
   
        //1.校验下单状态,商品是否存在,用户是否合法,购买数量是否正确
        verifyingUserInformation(itemId,userId,amount,promoId);

        //2.落单减库存,支付减库存
        orderDecreaseStock(itemId, amount);

        //3.订单入库
        OrderModel orderModel = orderintoMysql(itemId, userId, amount, promoId);

        //4.返回前端
        return orderModel;
    }
	/**负责createOrder中的用户校验工作*/
    private void verifyingUserInformation(Integer itemId, Integer userId,
                                          Integer amount,Integer promoId) throws BusinessException {
        //这一步是没有缓存的直接查mysql    ItemModel itemModel = itemService.getItemById(itemId);
        //这一步是走缓存的
        ItemModel itemModel = itemService.getItemByIdInCache(itemId);

        if (itemModel == null) {
            throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "商品信息不存在");
        }
        /**这个地方如果采用token登录模式这个地方是不用放进redis中进行验证user信息的,他这个采用的是oss模型所以需要
         * 将用户信息缓存在redis中提高效率   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.equals(itemModel.getPromoModel().getId())) {
                throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "活动信息不正确");
            } else if (itemModel.getPromoModel().getStatus() != 2) {
                //活动是否正在进行中
                throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "活动还未开始");
            }
        }
    }

ItemServiceImpl

/**用于商品活动缓存设置,因为生成订单需要两部分,第一部分需要验证商品,第二部分要验证用户*/
    @Override
    public ItemModel getItemByIdInCache(Integer id) {
        ItemModel itemModel = (ItemModel) redisTemplate.opsForValue().get("item_validate_" + id);
        if (itemModel == null) {
            itemModel = this.getItemById(id);
            redisTemplate.opsForValue().set("item_validate_" + id, itemModel);
            redisTemplate.expire("item_validate_" + id, 10, TimeUnit.MINUTES);
        }
        return itemModel;
    }

UserServiceImpl

 @Override
    public UserModel getUserByIdInCache(Integer id) {
        UserModel userModel = (UserModel) redisTemplate.opsForValue().get("user_validate_"+id);
        if(userModel == null){
            userModel = this.getUserById(id);
            redisTemplate.opsForValue().set("user_validate_"+id,userModel);
            redisTemplate.expire("user_validate_"+id,10, TimeUnit.MINUTES);
        }
        return userModel;
    }

2.redis实现扣减库存优化

如果每次下单都需要去数据库中去减库存,这样又1w条请求来,都要查询去更改同一件商品的stock数量,会很慢。因为在更改数据的时候会触发行锁。这样其他线程请求就需要去等当前线程更改完数据库才能进行更改,效率太低了。

代码

ItemStockDOMapper.xml

  <update id="decreaseStock">
    <!--
    这个update语句的意思是通过itemid来进行更新stock而输入的数是amount
    这个amount必须小于库存数量,而且在这个地方是做减法
    -->
    update item_stock
    set stock = stock - #{amount}
    <!--每次操作都会在这增加行锁-->
    where item_id = #{itemId} and stock >= #{amount}
  </update>

如果我们直接用redis来进行更下单扣减库存操作呢?
1、redis先从数据库中取出库存数量
2、有下单请求时,每次来的请求都去扣减redis中的数量(因为redis是内存级别的效率非常高)。
3、最终直到redis中的库存减到0,停止售卖次商品。
OrderServiceImpl

/**负责createOrder中的减库存操作*/
    private void orderDecreaseStock(Integer itemId, Integer amount) throws BusinessException {
        boolean result = itemService.decreaseStock(itemId, amount);
        if (!result) {
            throw new BusinessException(EmBusinessError.STOCK_NOT_ENOUGH);
        }
    }

ItemServiceImpl

@Override
    @Transactional
    public boolean decreaseStock(Integer itemId, Integer amount) {
            Long result = redisTemplate.opsForValue()
                    .increment("promo_item_stock_" + itemId, amount.intValue() * -1);
            if (result >= 0) { // > 变 >= 再分化
			    return true;
            }else {
                //库存为负,此次交易失败,补回redis中库存
                redisTemplate.opsForValue().increment("promo_item_stock_" + itemId, amount.intValue());
                return false;
            }
    }

好处:因为redis是内存级别的效率非常高
坏处:redis和数据库中的数据不同步,如果redis宕机会导致用户购买商品的数量信息全部丢失。简单来说就是缓存中的数据未能和数据库中的数据产生任何一致性,这种方法是不可取的


二、RocketMQ引入

1.思考为什么要引入MQ

因为要同步redis和Mysql中的数据同步。

而MQ的左右就是在扣除redis中的stock之后,发送消息给Mysql让数据库扣减库存,异步更新Mysql保证redis和Mysql的最终一致性。

关于rocketMQ的一般使用和简单概念,可看我这篇博客:https://blog.csdn.net/qq_42961251/article/details/127100154?spm=1001.2014.3001.5502

但是每当引入了一个新的结构,系统变得复杂了,此时就需要考虑这个下单过程中会出现那些问题,采取什么样的结构。

2.下单采取的结构

第一种结构采用同步异步更新数据库

RocketMQ采用同步方式发送更新数据库消息,必定等到broker成功发送消息给consumer才能算订单成功,返回给前端。

第二种结构采用的是前端轮询加后端假异步

RocketMQ采用异步发送方式,并不需要等待消息是否发送成功,但是前端不能直接返回给用户,说下单成功,前端采用长轮询方式不断的访问后端是都更新数据库成功了,只有后端都成功才会发给前端消息,所以也称之为假异步。

本项目中我们采用的是第一种结构

2.MQ遇到的问题

MQ发送更新数据库扣减库存的消息,应该在校验环节和订单生成环节都完成之后,在进行MQ的发送。因为spring中的@Transactional只能回滚Mysql并不能回滚MQ发送的消息。如果MQ先发送消息,之后订单orderintoMysql方法出错了,此时就会白白的在Mysql中扣除一个库存导致,订单少买
如何解决这个问题呢?
使用RocketMQ事务机制。

3.RocketMQ事务机制

RocketMQ事务之正常事务过程

可以采用RokcetMQ中的事务发送,将校验和生成订单放在producer的本地事务来做,只有本地事务成功了,才会发送这条消息给Consumer,本地事务失败了,MQ就不会发送消息给broker,整个订单就会回滚。

在这里插入图片描述

RocketMQ事务之正常补偿过程

假设在创建订单createOrder这个过程发生了很长的时间,创建订单在数据库压力比较大的情况下可能用了10s多。这样MQ就发现本地事务一直没有说提交成功还是提交失败回滚,处于一个UNKNOW的状态于是就需要checkLocalTransaction方法回调判断是否下单是否是成功的。
我们需要创建一个新的数据表流水日志表stockLog表。
当前stockLog中的status决定当前订单的状态,2代表成功,3代表回滚,1代表不知道。
每次当订单生成之后改动当前这条流水的status状态,决定是否补偿结果。

代码

MqConsumer

@Component
public class MqConsumer {

    private DefaultMQPushConsumer consumer;

    @Value("${mq.nameserver.addr}")
    private String nameAddr;

    @Value("${mq.topicname}")
    private String topicName;

    @Resource
    private ItemStockDOMapper itemStockDOMapper;

    @PostConstruct
    public void init() throws MQClientException {
        consumer = new DefaultMQPushConsumer("stock_consumer_group");
        consumer.setNamesrvAddr(nameAddr);
        //订阅所有topicName的消息
        consumer.subscribe(topicName,"*");

        //consumer处理过程
        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);
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });


        consumer.start();
    }
}

MqProducer

@Component
public class MqProducer {

    private DefaultMQProducer producer;

    //为了保证MQ发送一定成功所以需要使用rocketmq中的事务
    private TransactionMQProducer transactionMQProducer;

    @Value("${mq.nameserver.addr}")
    private String nameAddr;

    @Value("${mq.topicname}")
    private String topicName;

    @Resource
    private OrderService orderService;

    @Resource
    private StockLogDOMapper stockLogDOMapper;

    @PostConstruct
    public void init() throws MQClientException {
        //做mq producer的初始化
        producer = new DefaultMQProducer("producer_group");
        producer.setNamesrvAddr(nameAddr);
        producer.start();


        transactionMQProducer = new TransactionMQProducer("transaction_producer_group");
        transactionMQProducer.setNamesrvAddr(nameAddr);
        transactionMQProducer.start();

        transactionMQProducer.setTransactionListener(new TransactionListener() {
            @Override
            public LocalTransactionState executeLocalTransaction(Message message, Object arg) {
                //真正要做的事情,就是创建订单  这步就是MQ事务中本地事务部分
                Integer itemId = (Integer) ((Map)arg).get("itemId");
                Integer promoId = (Integer) ((Map)arg).get("promoId");
                Integer userId = (Integer) ((Map)arg).get("userId");
                Integer amount = (Integer) ((Map)arg).get("amount");
                String stockLogId = (String) ((Map)arg).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;
            }


            /**
             * 假设在创建订单createOrder这个过程发生了很长的时间,创建订单在数据库压力比较大的情况下可能用了
             * 10s多。这样MQ就发现本地事务一直没有说提交成功还是提交失败回滚,处于一个UNKNOW的状态
             * 于是就需要checkLocalTransaction回调判断是否下单是否是成功的
             */
            @Override
            public LocalTransactionState checkLocalTransaction(MessageExt msg) {
                //根据是否扣减库存成功,来判断要返回COMMIT,ROLLBACK还是继续UNKNOWN
                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");
                String stockLogId = (String) map.get("stockLogId");
                StockLogDO stockLogDO = stockLogDOMapper.selectByPrimaryKey(stockLogId);
                if(stockLogDO == null){
                    return LocalTransactionState.UNKNOW;
                }
                if(stockLogDO.getStatus().intValue() == 2){
                    return LocalTransactionState.COMMIT_MESSAGE;
                }else if(stockLogDO.getStatus().intValue() == 1){
                    return LocalTransactionState.UNKNOW;
                }
                return LocalTransactionState.ROLLBACK_MESSAGE;
            }
        });
    }

    //事务同步扣减库存方法
    //事务怎么体现的?只要数据库中的数据提交了,对应的一个消息是必定发送成功的,数据库内消息回滚了,那么消息必定不发送。
    public boolean transactionAsyncReduceStock(Integer userId,Integer itemId
                                               ,Integer promoId, Integer amount,String stockLogId){
        HashMap<String, Object> bodyMap = new HashMap<>();
        bodyMap.put("itemId",itemId);
        bodyMap.put("amount",amount);
        bodyMap.put("stockLogId",stockLogId);

        HashMap<String, Object> argsMap = new HashMap<>();
        argsMap.put("itemId",itemId);
        argsMap.put("amount",amount);
        argsMap.put("userId",userId);
        argsMap.put("promoId",promoId);
        argsMap.put("stockLogId",stockLogId);

        Message message = new Message(topicName,"increase",
                JSON.toJSON(bodyMap).toString().getBytes(Charset.forName("UTF-8")));
        TransactionSendResult sendResult = null;
        try {
            //sendMessageInTransaction第二个参数argsMap就是后面会被本地事务中的executeLocalTransaction方法接收到
            sendResult = transactionMQProducer.sendMessageInTransaction(message,argsMap);
        } catch (MQClientException e) {
            e.printStackTrace();
            return false;
        }
        if(sendResult.getLocalTransactionState() == LocalTransactionState.ROLLBACK_MESSAGE){
            return false;
        }else if(sendResult.getLocalTransactionState() == LocalTransactionState.COMMIT_MESSAGE){
            return true;
        }else{
            return false;
        }
    }
}

OrderController

//订单创建
    @PostMapping(value = "/order/createOrder", consumes = {CONTENT_TYPE_FORMED})
    public CommonReturnType createItem(@RequestParam(name = "itemId")Integer itemId,
                                       @RequestParam(name = "amount")Integer amount,
                                       @RequestParam(name = "promoId",required = false)Integer promoId) throws BusinessException {

        //判断是否登录
        Boolean isLogin = (Boolean) httpServletRequest.getSession().getAttribute("IS_LOGIN");
        if(isLogin == null || !isLogin){
            throw new BusinessException(EmBusinessError.USER_NOT_LOGIN, "请登录后再进行预约");
        }

        //获取用户的登录信息
        UserModel userModel = (UserModel) httpServletRequest.getSession().getAttribute("LOGIN_USER");
        
        boolean result = mqProducer.transactionAsyncReduceStock(userModel.getId(), itemId,
                promoId, amount,stockLogId);
        if(!result){
            throw new BusinessException(EmBusinessError.UNKNOW_ERROR,"下单失败");
        }

        return CommonReturnType.create(null);
    }

OrderServiceImpl

 @Transactional
    promoId非空表示已预约活动价格下单
    public OrderModel createOrder(Integer userId, Integer itemId, Integer promoId,
                                  Integer amount,String stockLogId) throws BusinessException {

        //1.校验下单状态,商品是否存在,用户是否合法,购买数量是否正确
        verifyingUserInformation(itemId,userId,amount,promoId);

        //2.落单减库存,支付减库存
        orderDecreaseStock(itemId, amount);

        //自己添加,应对订单出现错误的时候,回滚第二步redis扣减的订单,防止噪声少买
        //注意必须放在订单前面订单失败才能回滚
        toAsynIncreaseRedisStock(itemId,amount);

        //3.订单入库
        OrderModel orderModel = orderintoMysql(itemId, userId, amount, promoId);


        //4.因为采用了MQ事务发送机制,而这个订单成功的时间是不确定的,使用MQ事务有事务补偿机制
        //所以在询问本地事务的时候返回是需要通过流水stock_log的status来判断的,当来到这里的时候说明订单都已经完成了
        //所以此时我们应该将stock_log中的status设置为成功状态
        //(1)问题又来了,当前不是又插入数据库了岂不是并发性又下降了?
        //这种插入数据库是根据stockLogId进行插入的每一笔订单都有自己的stockLogId,而会在stockLogId上添加行锁
        //这个行锁不存在并发关系,而在stock表中大家订单都需要取更改库存这个操作就是并发的,所以相当于将并发改数据库
        //变成了非并发更改流水表中的字段
        //(2)问题又来了,假如说我们当前商品只有100个,但我们有1w个人点击了在OrderController中
        //设置插入数据库,此时就会造成生成了1w个流水信息,产生了极大的浪费
        stockLogToSuccess(stockLogId);
        //5.返回前端
        return orderModel;
    }
		private void stockLogToSuccess(String stockLogId) throws BusinessException {
        	StockLogDO stockLogDO = stockLogDOMapper.selectByPrimaryKey(stockLogId);
        	if(stockLogDO == null){
            throw new BusinessException(EmBusinessError.UNKNOW_ERROR);
        }
        	//如果能拿到这个订单表的字段就将status更改为2
        	stockLogDO.setStatus(2);
        	stockLogDOMapper.updateByPrimaryKeySelective(stockLogDO);
    }

4.RocketMQ的ACK机制

目前已经解决了,producer和broker之间的发送消息事务问题。保证了订单生成成功后,MQ才进行发送消息给Mysql消息。但是目前consumer是否已经能消费成功这也是一个系统问题。假如说,订单生成没问题,redis减库存,然后MQ就发送消息,返回给前端已经下单成功了。但是此时consumer消费出现了错误此时MQ因为ACK机制,会有消息重试机制。默认重试16次,如果16次尝试都失败了。这条消息就进入了死信队列。在死信队列中出现消息,就需要人工介入处理了。
如果想体验consumer插入数据库中的例子可以参考如下代码

代码

MqConsumer

@PostConstruct
    public void init() throws MQClientException {
        consumer = new DefaultMQPushConsumer("stock_consumer_group");
        consumer.setNamesrvAddr(nameAddr);
        //订阅所有topicName的消息
        consumer.subscribe(topicName,"*");

        //consumer处理过程
        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");
                System.out.println(10/0);
                itemStockDOMapper.decreaseStock(itemId,amount);
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });


        consumer.start();
    }

因10/0出错,所以就会不断重试重试,所以ACK机制就是保证Consumer端消费成功。


三、redis完善系统

系统还可以有两个优化。

1.实现redis中Stock数据一致性

@Transactional标签同样不能回滚redis中的stock数据,在进行redis扣减库存之后,如果插入订单失败了,此是redis也没回滚相当于少买了。
方法可以采用TransactionSynchronizationManagerregisterSynchronization方法里面实现事务适配器TransactionSynchronizationAdapter中的afterCompletion方法。这样就在事务回滚的同时保证了redis的一致性。

代码

OrderServiceImpl

@Transactional
    public void toAsynIncreaseRedisStock(Integer itemId, Integer amount) throws BusinessException{
        TransactionSynchronizationManager
                .registerSynchronization(new TransactionSynchronizationAdapter() {
            @Override
            public void afterCompletion(int status) {
                //参考连接:https://www.cnblogs.com/ciel717/p/16190723.html
                /**status 0:外部方法事务提交后执行的业务逻辑
                 *        1:外部方法事务回滚后执行的业务逻辑
                 *        2:外部方法事务异常时执行的业务逻辑
                 */
                if(status!=0){
                    itemService.asynchronousIncreaseStock(itemId,amount);
                }
            }
        });

    }

OrderServiceImpl

@Transactional
    promoId非空表示已预约活动价格下单
    public OrderModel createOrder(Integer userId, Integer itemId, Integer promoId,
                                  Integer amount,String stockLogId) throws BusinessException {
        /**
         * 1.校验下单状态下单商品是否存在,用户是否合法,购买数量是否正确
         * 2.采取落单减库存(一般电商分为两种一种是落单减库存,支付减库存)
         * 落单减库存就是用户先落单了,此时系统就去锁库存了。也就是再支付之前用户就相当于拥有了这个商品。
         * 而支付减库存,就是落单了,系统检测还有库存,但是用户在支付完后,再次系统去锁库存,发现已经没有
         * 库存了只能给用户退款了。
         * 3.订单入库
         * 4.返回前端
         */

        //1.校验下单状态,商品是否存在,用户是否合法,购买数量是否正确
        verifyingUserInformation(itemId,userId,amount,promoId);

        //2.落单减库存,支付减库存
        orderDecreaseStock(itemId, amount);

        //应对订单出现错误的时候,回滚第二步redis扣减的订单,防止噪声少买
        //注意必须放在订单前面订单失败才能回滚
        toAsynIncreaseRedisStock(itemId,amount);

        //3.订单入库
        OrderModel orderModel = orderintoMysql(itemId, userId, amount, promoId);

        stockLogToSuccess(stockLogId);

        //5.返回前端
        return orderModel;
    }

2.redis实现售空机制

假如说我们当前商品只有100个,但我们有1w个人点击了在OrderController中。设置插入数据库,此时就会造成生成了1w个流水信息,产生了极大的浪费。所以我们希望,当卖完这一百个之后,就不再产生订单直接从后台提示前端商品全部销售完毕。
做法就是在redis中设置一个标志位“promo_item_stock_invalid_"+itemId如果是false就是还没销售空,如果是true就是销售空了。

代码

OrderController

//订单创建
    @PostMapping(value = "/order/createOrder", consumes = {CONTENT_TYPE_FORMED})
    public CommonReturnType createItem(@RequestParam(name = "itemId")Integer itemId,
                                       @RequestParam(name = "amount")Integer amount,
                                       @RequestParam(name = "promoId",required = false)Integer promoId) throws BusinessException {

        //判断是否登录
        Boolean isLogin = (Boolean) httpServletRequest.getSession().getAttribute("IS_LOGIN");
        if(isLogin == null || !isLogin){
            throw new BusinessException(EmBusinessError.USER_NOT_LOGIN, "请登录后再进行预约");
        }

        //获取用户的登录信息
        UserModel userModel = (UserModel) httpServletRequest.getSession().getAttribute("LOGIN_USER");

        //为了根据checkLocalTransaction确定消息的状态,需要引入操作流水(操作型数据:log data)
        //他的作用就是先插入一条异步流水,根据这个status用来追踪异步扣减库存这个消息。
        //就是根据你这个流水的状态来确定MQ返回是成功还是失败还是不知道

        //若库存不足直接返回下单失败
        //(2)问题又来了,假如说我们当前商品只有100个,但我们有1w个人点击了在OrderController中
        //设置插入数据库,此时就会造成生成了1w个流水信息,产生了极大的浪费
        if(redisTemplate.hasKey("promo_item_stock_invalid_"+itemId)){
            throw new BusinessException(EmBusinessError.STOCK_NOT_ENOUGH);
        }

        String stockLogId = itemService.initStockLog(itemId, amount);

        //OrderModel orderModel = orderService.createOrder(userModel.getId(), itemId, promoId, amount);
        //因为需要保证MQ中信息发送必须成功,所以采用rocketMQ事务
        boolean result = mqProducer.transactionAsyncReduceStock(userModel.getId(), itemId,
                promoId, amount,stockLogId);
        if(!result){
            throw new BusinessException(EmBusinessError.UNKNOW_ERROR,"下单失败");
        }

        return CommonReturnType.create(null);
    }

ItemServiceImpl

 @Override
    @Transactional
    public boolean SoldOutAsynDecreaseStock(Integer itemId, Integer amount) {
        Long result = redisTemplate.opsForValue()
                .increment("promo_item_stock_" + itemId, amount.intValue() * -1);
        if (result > 0) {
            //更新数据库 (采用异步方式更新mysql)
            //交给asyncDecreaseStock去更新
            return true;
        }else if(result == 0){
            //增加了商品售空的信息!!!防止顶点流水表大量生成废流水
            //通过放在redis中的标志位来判断是否当前商品已经售罄
            //打上库存售罄的标识
            redisTemplate.opsForValue().set("promo_item_stock_invalid_"+itemId, "true");
            redisTemplate.expire("promo_item_stock_invalid_"+itemId,1,TimeUnit.MINUTES);
            return true;

        }else {
            //库存为负,此次交易失败,补回redis中库存
            //redisTemplate.opsForValue().increment("promo_item_stock_" + itemId, amount.intValue());
            asynchronousIncreaseStock(itemId,amount);
            return false;
        }
    }


总结

基本上就是将原来的5次查Mysql+一次并发查Mysql,变成了1次查redis(promo_item_stock_invalid_itemid)+ 2次查redis(user+item)+1次改redis(更改库存数据)+1次插表(order)+RocketMQ事务发送。

  • 10
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值