【秒杀系统】交易性能优化之缓存库存

交易性能瓶颈

  • 交易验证完全依赖数据库
    @Override
    @Transactional
    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){
            //(1)校验对应活动是否存在这个适用商品
            if(promoId.intValue() != itemModel.getPromoModel().getId()){
                throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,"活动信息不正确");
                //(2)校验活动是否正在进行中
            }else if(itemModel.getPromoModel().getStatus().intValue() != 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.setUserId(userId);
        orderModel.setItemId(itemId);
        orderModel.setAmount(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;
    }

对下单进行压测
【秒杀系统】交易性能优化之缓存库存_第1张图片【秒杀系统】交易性能优化之缓存库存_第2张图片
mysql 服务器 top -H
【秒杀系统】交易性能优化之缓存库存_第3张图片
聚合报告
【秒杀系统】交易性能优化之缓存库存_第4张图片

整体下单流程,有 6 次对数据库的操作

【秒杀系统】交易性能优化之缓存库存_第5张图片

交易验证的优化

用户风控策略优化:对获取用户实时信息进行优化,判断用户 ID 是否存在、是否有异常情况是最基本,可以将这些操作放入到 redis 缓存中

    @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;
    }
    @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;
    }

活动校验策略优化:活动是有一个开始时间和结束时间的,活动的数据是要修改的,引入活动发布流程清除缓存,要有一个紧急下线能力

库存行锁

扣减库存缓存化

要确保 item_id 上是有索引的,如果没有索引就会锁整张表
【秒杀系统】交易性能优化之缓存库存_第6张图片
为 item_id 字段添加索引

alter table item_stock add unique index item_id_index(item_id)

串行话减库存是一个性能瓶颈避免不了,优化方案就是放入到 redis 缓存中这样就非常快了,内存不靠谱会丢,所以 redis 要异步同步数据库

方案一

( 1 ) 活动发布同步库存进缓存

    @Override
    public void publishPromo(Integer promoId) {
        //通过活动id获取活动
        PromoDO promoDO = promoDOMapper.selectByPrimaryKey(promoId);
        if(promoDO.getItemId() == null || promoDO.getItemId().intValue() == 0){
            return;
        }
        ItemModel itemModel = itemService.getItemById(promoDO.getItemId());

        //将库存同步到redis内
        redisTemplate.opsForValue().set("promo_item_stock_"+itemModel.getId(), itemModel.getStock());
    }

( 2 ) 下单交易减缓存库存

问题:数据库记录不一致,下单的时候也要去减缓存的库存

    @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.intValue() * -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;
        }

    }

这种方案在生产环境中是不能使用的,缓存和数据库的记录不一致

优化,引入异步消息队列 rocketmq

方案二

异步同步数据库

( 1 ) 活动发布同步库存进缓存

( 2 ) 下单交易减缓存库存

( 3 ) 异步消息扣减数据库内库存

【秒杀系统】交易性能优化之缓存库存_第7张图片
【秒杀系统】交易性能优化之缓存库存_第8张图片
【秒杀系统】交易性能优化之缓存库存_第9张图片

缓存库存接入异步化

消息发送方

@Component
public class MqProducer {

    private DefaultMQProducer producer;

    private TransactionMQProducer transactionMQProducer;

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

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


    @Autowired
    private OrderService orderService;

    @Autowired
    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 msg, Object arg) {
                //真正要做的事  创建订单
                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;
            }

            @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){
        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("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 {

            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;
        }

    }

    //同步库存扣减消息
    public boolean asyncReduceStock(Integer itemId,Integer amount)  {
        Map<String,Object> bodyMap = new HashMap<>();
        bodyMap.put("itemId",itemId);
        bodyMap.put("amount",amount);

        Message message = new Message(topicName,"increase",
                JSON.toJSON(bodyMap).toString().getBytes(Charset.forName("UTF-8")));
        try {
            producer.send(message);
        } catch (MQClientException e) {
            e.printStackTrace();
            return false;
        } catch (RemotingException e) {
            e.printStackTrace();
            return false;
        } catch (MQBrokerException e) {
            e.printStackTrace();
            return false;
        } catch (InterruptedException e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }
}

消息接收方

@Component
public class MqConsumer {

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

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

    @Autowired
    private ItemStockDOMapper itemStockDOMapper;

    @PostConstruct
    public void init() throws MQClientException {
        consumer = new DefaultMQPushConsumer("stock_consumer_group");
        consumer.setNamesrvAddr(nameAddr);
        consumer.subscribe(topicName,"*");

        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                //实现库存真正到数据库内扣减的逻辑
                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();

    }
}

异步同步数据库的问题

( 1 ) 异步消息发送失败

( 2 ) 扣减操作执行失败

( 3 ) 下单失败无法正确回补库存

事务型消息

落单减库存先从 redis 里面扣,扣完发送消息给异步回调,在异步回调内扣减数据库,如果异步消息发生任何异常,再把 redis 对应库存加回来,如果库存更新失败,也把 redis 对应库存加回来
【秒杀系统】交易性能优化之缓存库存_第10张图片
decreaseStock 有事务标签,decreaseStock 依赖于外部事务,decreaseStock 和外部事物同时成功同时失败
在这里插入图片描述
现在 decreaseStock 是在 redis 内存当中,如果说 decreaseStock 操作成功异步消息也发送过去了、redis也扣减成功了、消费端也将库存扣减掉了,但是用户这边订单入库产生了异常,返回给用户的结果是下单失败,库存就白白损失掉了,虽然没有造成超卖的现象,但是却少卖了。造成这个问题本质的原因就在于一个分布式事务的问题,也就是说这个 decreaseStock 操作在发送 asyncReduceStock 异步消息之前,没有办法完全确认 itemService.decreaseStock(itemId,amount) 这个落单减库存操作是否成功

如果 createOrder 方法上这个大事务成功之后我们在做对应的 redis 扣减,再发送异步消息,但是回想以下,都没有去扣减 redis,怎么知道对应的扣存必定有呢

redis 照常去扣,这个大事务提交失败我们把 redis 再加回来这的确是其中的一个方案,但是 decreaseStock 方法中更新库存成功的这条异步消息我们什么时候去发,是不是等事务成功之后我们在发送消息呢?

在 ItemService 中另外开一个方法异步更新库存

    @Override
    public boolean asyncDecreaseStock(Integer itemId, Integer amount) {
        boolean mqResult = mqProducer.asyncReduceStock(itemId,amount);
        return mqResult;
    }

返回给前端之前,也就是异步更新库存最后再发,等到你前面所有事务都执行完了最后在发送,但是这样代码逻辑还是有问题的,我们异步更新了库存的操作,并且根据更新对应 mq 发送状态去回补对应的库存,再让整个下单失败,看上去没有什么问题,但是 Spring 的 @Transactional 标签只有等到整个方法返回的时候才会去提交事务,也就是说mq发送成功了也返回给前端了,但是最后一次提交事务可能由于网络原因等因素发生了失败,那这个库存还是被白白的扣掉了

【秒杀系统】交易性能优化之缓存库存_第11张图片

事务型同步库存扣减消息

只要数据库的事务提交,对应的消息必定可以发送成功,数据库内事务回滚了,消息必定不发送,数据库状态未知,消息等待

发送的消息是事务型消息,有一个二阶段提交的概念,消息发送出去之后 broken 收到消息,但是它的状态不是可被消费的状态而是一个准备的状态,在这个状态下我们的这条消息是不会被消费者看到的,然后在本地执行 重写的 方法,在这个方法里面要写我们真正要做的事比如创建订单

sendResult = transactionMQProducer.sendMessageInTransaction(message,argsMap);

【秒杀系统】交易性能优化之缓存库存_第12张图片
Myproducer 的 init 方法

@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 msg, Object arg) {
                //真正要做的事  创建订单
                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;
            }

            @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;
            }
        });

LocalTransactionState 的三种状态

public enum LocalTransactionState {
    COMMIT_MESSAGE,
    ROLLBACK_MESSAGE,
    UNKNOW,
}
  ROLLBACK_MESSAGE:回滚事务

  COMMIT_MESSAGE: 提交事务

  UNKNOW: broker 会定时的回查 Producer 消息状态,直到彻底成功或失败。

当 executeLocalTransaction 方法返回 ROLLBACK_MESSAGE 时,表示直接回滚事务,当返回 COMMIT_MESSAGE 提交事务

当返回 UNKNOW时,Broker 会在一段时间之后回查 checkLocalTransaction,根据 checkLocalTransaction 返回状态执行事务的操作(回滚或提交)

整理

在 OrderController 中注入MqProducer ,createOrder方法最后通过mqProducer开启了异步发送事务型消息的操作

if (!mqProducer.transactionAsyncReduceStock(userModel.getId(), itemId, promoId, amount)) {
            throw new BusinessException(EmBusinessError.UNKNOWN_ERROR, "下单失败");
        }

MqProducer中transactionAsyncReduceStock中bodyMap 用来扣减对应的库存,参数设置到argsMap 里面传递到sendMessageInTransaction方法里面,sendMessageInTransaction就会往消息中间件投递一个prepare消息同时回调一个executeLocalTransaction方法在这个方法内部我们真正的调用了createOrder完成了订单的创建以及redis扣减库存的操作并且返回事务状态是提交还是回滚

    //事务型同步库存扣减消息
    public boolean transactionAsyncReduceStock(Integer 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("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 {

            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;
        }

    }

消息中间件不知道消息是何种状态比如数据库压力大的时候创建订单用了很长时间,无法返回对应状态
【秒杀系统】交易性能优化之缓存库存_第13张图片
那么消息默认就是UNKOWN状态,消息中间件就会定期回调checkLocalTransaction这个方法,它要做的就是根据是否扣减库存成功,来判断要返回COMMIT,ROLLBACK还是继续UNKNOWN,但是现在有的数据还无法判断,除非可以根据itemId和amount可以判断哪笔订单状态的流水,才能知道订单状态是什么样子的
【秒杀系统】交易性能优化之缓存库存_第14张图片

checkLocalTransaction 优化

库存流水线状态

新建数据库表订单流水表
【秒杀系统】交易性能优化之缓存库存_第15张图片
OrderController 经过了几番改造,发送事务型消息的机制驱动下单同时根据回调状态来决定这个消息是否发送还是回滚

下单之前先加入库存流水
【秒杀系统】交易性能优化之缓存库存_第16张图片
checkLocalTransaction 有了这个流水账就可以追踪到对应的下单状态

库存数据库最终一致性保证

方案

( 1 ) 引入库存操作流水

( 2 ) 引入事务性消息机制

问题

( 1 ) redis 不可用时如何处理

( 2 ) 扣减流水错误如何处理

业务场景决定高可用技术实现

设计原则

宁可少卖,不能超卖

方案

redis可以比实际数据库中少

超时释放

库存售罄

库存售罄标识

售罄后不去操作后续流程

售罄后通知各系统售罄

回补上新

【秒杀系统】交易性能优化之缓存库存_第17张图片
OrderController
【秒杀系统】交易性能优化之缓存库存_第18张图片

你可能感兴趣的:(秒杀,redis,java)