交易性能優化技術之緩存庫存

性能瓶頸

分析下單時,應用的模型如下:

在這裏插入圖片描述

訪問數據庫的次數如下:(訪問6次數據庫;是否能對訪問數據庫的次數進行一定程序的優化,採用緩存的方式)
在這裏插入圖片描述

性能優化

交易驗證優化(緩存用戶信息,)

原來對用戶,商品進行校驗的方式:都需要訪問數據庫


        1.校驗下單狀態,下單的商品是否存在,用戶是否合法,購買數量是否正確
        ItemModel itemModel = itemService.getItemById(itemId);
        if(itemModel == null){
            throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,"商品信息不存在");
        }

可以將商品的詳情以及用戶的詳情存入到redis緩存中,之後用戶再次下單的時候可以從緩存中取出數據

在這裏插入圖片描述
在這裏插入圖片描述

問題是:這裏的商品信息若在之後用戶下過單之後,可能進行更改就存在一個redis緩存與數據庫緩存不一致的情況。

代碼

應用

  //使用緩存校驗itemModel
        ItemModel itemModel = itemService.getItemByIdInCache(itemId);
        if(itemModel == null){
            throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,"商品信息不存在");
        }

        UserModel userModel = userService.getUserByIdInCache(userId);
        if(userModel == null){
            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;

    }


扣減庫存優化

在這裏插入圖片描述

在訪問數據庫進行庫存扣減的問題時,由於數據庫行鎖的存在,可能存在數據庫瓶頸的問題

可以將數據庫中的stock緩存到緩存中,庫存緩存化,之後再採用消息隊列將其異步執行更新mysql數據庫中的內容;

在這裏插入圖片描述

庫存緩存化

  • 活動發佈同步庫存進緩存
  • 下單交易減緩存庫存

在這裏插入圖片描述

代碼

活動發佈同步庫存進緩存

/活動函發佈
    void publishPromo(Integer promoId);
    @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.落單減庫存
        boolean result = itemService.decreaseStock(itemId,amount);
//減去緩存中的庫存
        long result = redisTemplate.opsForValue().increment("promo_item_stock_"+itemId,amount.intValue()*-1);


異步同步數據庫–採用消息隊列

異步消息扣減數據庫內存庫

在這裏插入圖片描述

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

代碼
生產者



package com.imooc.miaoshaproject.mq;


import com.alibaba.fastjson.JSON;
import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;

/**
 * Created by hzllb on 2019/2/23.
 */
@Component
public class MqProducer {

    private DefaultMQProducer producer;



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

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






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


    //同步庫存扣減消息
    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;
    }
}

消息消費者:數據庫中庫存的扣減操作



package com.imooc.miaoshaproject.mq;

import com.alibaba.fastjson.JSON;
import com.imooc.miaoshaproject.dao.ItemDOMapper;
import com.imooc.miaoshaproject.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.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.List;
import java.util.Map;

/**
 * Created by hzllb on 2019/2/23.
 */
@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();

    }
}

異步同步數據庫執行失敗–採取事務

在這裏插入圖片描述

事務型信息 將下單整個邏輯

在這裏插入圖片描述

防止商品售罄–在緩存中加入一個標誌

在緩存中加入一個標誌,進行判斷:當緩存中的庫存小於0時,則在緩存中設置一個標誌,每次用戶下單的時候都需要先判斷這個標誌是否存在,若標誌存在,則說明沒有庫存,直接返回不存在

在這裏插入圖片描述


//防止商品售罄
        //判斷商品是否已經售罄,若對應的售罄key存在則直接返回下單失敗
        if(redisTemplate.hasKey("promo_item_stock_invalid_"+itemId)){
            throw new BusinessException(EmBusinessError.STOCK_NOT_ENOUGH);
        }

隊列異步性事務消息(當全部操作成功,之前發送到隊列中的消息才能被消費,在數據庫中扣減庫存才能成功)

mq異步事務


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

mqproducer
在這裏插入圖片描述


   //事務型異步庫存扣減消息
    public boolean transactionAsyncReduceStock(Integer userId,Integer promoId,Integer itemId,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 transactionSendResult=null;
        try {
            //發送的是事務型消息
            //發送完成之後事務處於prepare狀態
            //1、往消息隊列中投遞prepare消息,維護在message broker的中間件上
            //2、在本地執行localTRranscatopn
           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;
        }else{
            return false;
        }




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


        producer.setNamesrvAddr(nameAddr);
        producer.start();

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




        transactionMQProducer.setTransactionListener(new TransactionListener() {

            /**
             *  COMMIT_MESSAGE,
                ROLLBACK_MESSAGE,  將之前prepare消息發送
                UNKNOW;
             * @param message
             * @param arg
             * @return
             */
            @Override
            public LocalTransactionState executeLocalTransaction(Message message, 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 {
                    //在這裏真正調用createOrder方法,完成訂單的創建以及對應redis
                    orderService.createOrder(userId, itemId,promoId,amount,stockLogId);

                } catch (BusinessException e) {
                    e.printStackTrace();
                    //發生異常,事務回滾
                    return LocalTransactionState.ROLLBACK_MESSAGE;

                }

                //只有當COMMIT完成之後,才發生後消息的回調
                return LocalTransactionState.COMMIT_MESSAGE;
            }

            @Override
            public LocalTransactionState checkLocalTransaction(MessageExt msg) {
                //根據是否扣減庫存成功來判斷要返回COMMIT,ROLLBACK還是繼續UNKWON
                //實現庫存真正到數據庫內扣減的邏輯

                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;

            }
        });
    }



創建訂單creatOrder


 @Override
    @Transactional
    public OrderModel createOrder(Integer userId, Integer itemId, Integer promoId, Integer amount,String stockLogId) throws BusinessException {
//        1.校驗下單狀態,下單的商品是否存在,用戶是否合法,購買數量是否正確
//        ItemModel itemModel = itemService.getItemById(itemId);
//        if(itemModel == null){
//            throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,"商品信息不存在");
//        }

        //使用緩存校驗itemModel
        ItemModel itemModel = itemService.getItemByIdInCache(itemId);
        if(itemModel == null){
            throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,"商品信息不存在");
        }

        //緩存
        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){
            //(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);

        //設置庫存流水狀態位成功
        StockLogDO stockLogDO = stockLogDOMapper.selectByPrimaryKey(stockLogId);
        if(stockLogDO==null){
            throw new BusinessException(EmBusinessError.UNKNOWN_ERROR);
        }else{
            stockLogDO.setStatus(2);
            stockLogDOMapper.updateByPrimaryKeySelective(stockLogDO);
        }

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


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章