1. 程式人生 > >SpringBoot(19)學習之使用RabbitMQ實現高併發介面優化

SpringBoot(19)學習之使用RabbitMQ實現高併發介面優化

使用RabbitMQ改寫秒殺功能

實現思路

思路:減少資料庫訪問

具體的實現流程就是

1.系統初始化,把商品庫存數量載入到Redis

2.收到請求,Redis預減庫存,庫存不足,直接返回,否則3

3.請求入隊,立即返回排隊中

4.請求出隊,生成訂單,減少庫存

5.客戶端輪詢,是否秒殺成功

其中4和5是同時併發處理的。

具體實現

系統初始化,把商品庫存數量載入到Redis

如何在初始化的時候就將庫存資料存入快取中

通過實現InitializingBean介面中的一個方法:afterPropertiesSet()

系統初始化會首先呼叫該函式:

 /**
     * 系統初始化會呼叫該函式
     * @throws
Exception */
@Override public void afterPropertiesSet() throws Exception { List<GoodsVo> goodsVoList = goodsService.listGoodsVo(); if (goodsVoList == null){ return; } for (GoodsVo goodsVo:goodsVoList){ //預先把商品庫存載入到redis中 redisService.set(GoodsKey.getSeckillGoodsStock,""
+goodsVo.getId(),goodsVo.getStockCount()); localOverMap.put(goodsVo.getId(),false); } }

收到請求,Redis預減庫存,庫存不足,直接返回,否則請求入隊,立即返回排隊中

首先需要一個RabbitMQ的佇列

使用Direct交換機模式

/**
     * Direct 交換機模式
     */
    //佇列
    @Bean
    public Queue secKill_QUEUE() {
        return new Queue(SECKILL_QUEUE,true
); }

佇列訊息的傳送

 public void sendSecKillMessage(SecKillMessage secKillMessage) {
        String msg = RedisService.Bean2String(secKillMessage);
        logger.info("send SecKill message: " + msg);
        amqpTemplate.convertAndSend(MQConfig.SECKILL_QUEUE, msg);

    }

秒殺的實現

//預先減庫存
long stock = redisService.decr(GoodsKey.getSeckillGoodsStock,""+goodsId);
if (stock < 0){
localOverMap.put(goodsId,true);
return Result.error(CodeMsg.SECKILL_OVER);
}
//判斷是否已經秒殺到了
SecKillOrder order = orderService.getOrderByUserIdGoodsId(user.getId(),goodsId);
if (order != null){
return Result.error( CodeMsg.SECKILL_REPEATE);
}
//壓入RabbitMQ佇列
SecKillMessage secKillMessage = new SecKillMessage();
secKillMessage.setUser(user);
secKillMessage.setGoodsId(goodsId);
mqSender.sendSecKillMessage(secKillMessage);
return Result.success(0);    //排隊中

請求出隊,生成訂單,減少庫存

其實就是RabbitMQ的隊列出隊去處理相關的業務

 @RabbitListener(queues = MQConfig.SECKILL_QUEUE)
    public void receive(String message){
        logger.info("receive message" + message);
        SecKillMessage secKillMessage = RedisService.String2Bean(message,SecKillMessage.class);
        SecKillUser user = secKillMessage.getUser();
        long goodsId = secKillMessage.getGoodsId();

        //判斷庫存
        GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
        int stock = goods.getStockCount();
        if (stock <= 0){
            return;
        }
        //判斷是否已經秒殺到了
        SecKillOrder order = orderService.getOrderByUserIdGoodsId(user.getId(),goodsId);

        if (order != null){
            return;
        }
        //減庫存 下訂單 寫入秒殺訂單
        //訂單的詳細資訊
        OrderInfo orderInfo = secKillService.secKill(user, goods);
    }

客戶端輪詢,是否秒殺成功

//秒殺的結果

    /**
     * orderId:秒殺成功
     * -1: 秒殺失敗
     * 0:排隊中
     * @param model
     * @param user
     * @param goodsId
     * @return
     */
    @RequestMapping(value = "/result",method = RequestMethod.GET)
    @ResponseBody
    public Result<Long> miaoshaResult(Model model, SecKillUser user, @RequestParam("goodsId") long goodsId){
        model.addAttribute("user",user);
        if (user == null){
            return Result.error(CodeMsg.SESSION_ERROR);
        }
        long result = secKillService.getSecKillResult(user.getId(),goodsId);
        return Result.success(result);
    }

secKillService.getSecKillResult():

//獲取結果
    /**
     * orderId :成功
     * -1:失敗
     * 0: 排隊中
     * @param userId
     * @param goodsId
     * @return
     */
    public  long getSecKillResult(Long userId, long goodsId) {
        SecKillOrder order = orderService.getOrderByUserIdGoodsId(userId,goodsId);
        if (order != null){
            return order.getOrderId();
        }else {
            boolean isOver = getGoodsOver(goodsId);
            if (isOver){
                return -1;
            }else {
                return 0;
            }
        }

    }

這裡涉及到了redis的訪問,就是redis中有商品的數量,通過該引數判斷賣沒賣完,當一次性來了多於商品數目的請求的時候,redis預減庫存,減為負數,其實在這個時候在來商品購買請求的時候就不需要在訪問redis了。因為商品已經賣完了,這個時候就做一個標記,先判斷記憶體這個標記,如果庫存已經小於0了,就不再訪問redis,這樣就減少了redis的訪問次數。

沒有訂單有兩種情況,賣完了失敗,和排隊中,

在上面的秒殺那做個標記。這個商品是否秒殺完了。存入redis中。

之後去判斷是否存在這個key就知道是哪種情況,這樣

//事務,原子性操作
    @Transactional
    public OrderInfo secKill(SecKillUser user, GoodsVo goods) {

        //減庫存 下訂單 寫入秒殺訂單 必須是同時完成的
        boolean success = goodsService.reduceStock(goods);
        //減庫存成功了才進行下訂單
        if (success) {
            return orderService.createOrder(user, goods);
        }else{ //說明商品秒殺完了。做一個標記
            setGoodsOver(goods.getId());
            return null;
        }
    }

    //獲取結果
    /**
     * orderId :成功
     * -1:失敗
     * 0: 排隊中
     * @param userId
     * @param goodsId
     * @return
     */
    public  long getSecKillResult(Long userId, long goodsId) {
        SecKillOrder order = orderService.getOrderByUserIdGoodsId(userId,goodsId);
        if (order != null){
            return order.getOrderId();
        }else {
            boolean isOver = getGoodsOver(goodsId);
            if (isOver){
                return -1;
            }else {
                return 0;
            }
        }

    }

    public void setGoodsOver(Long goodsId) {
        redisService.set(SecKillKey.isGoodsOver,""+goodsId,true);
    }

    public boolean getGoodsOver(Long goodsId) {
        return redisService.exists(SecKillKey.isGoodsOver,""+goodsId);
    }
}

相對應的前端的修改

原來的detail頁面中秒殺事件函式:

function doMiaosha(){
    $.ajax({
        url:"/miaosha/do_miaosha",
        type:"POST",
        data:{
            goodsId:$("#goodsId").val(),
        },
        success:function(data){
            if(data.code == 0){
                window.location.href="/order_detail.htm?orderId="+data.data.id;
            }else{
                layer.msg(data.msg);
            }
        },
        error:function(){
            layer.msg("客戶端請求有誤");
        }
    });
}

秒殺到商品就直接返回,現在後端改為訊息佇列,所以需要增加函式進行判斷,必要時需要輪詢:

if(data.code == 0){
    window.location.href="/order_detail.htm?orderId="+data.data.id;
}else{
    layer.msg(data.msg);
}

所以將其改為:

//其他的部分省略
...
if(data.code == 0){
    //window.location.href="/order_detail.htm?orderId="+data.data.id;
    //秒殺到商品的時候,這個時候不是直接返回成功,後端是進入訊息佇列,所以前端是輪詢結果,顯示排隊中
    getMiaoshaResult($("#goodsId").val());
}else{
    layer.msg(data.msg);
}
...
function getMiaoshaResult(goodsId) {
    g_showLoading();
    $.ajax({
        url:"/miaosha/result",
        type:"GET",
        data:{
            goodsId:$("#goodsId").val(),
        },
        success:function(data){
            if(data.code == 0){
                var result = data.data;
                //失敗---    -1
                if(result <= 0){
                    layer.msg("對不起,秒殺失敗!");
                }
                //排隊等待,輪詢---   0
                else if(result == 0){//繼續輪詢
                    setTimeout(function () {
                        getMiaoshaResult(goodsId);
                    },50);
                }
                //成功----   1
                else {
                    layer.msg("恭喜你,秒殺成功,檢視訂單?",{btn:["確定","取消"]},
                        function () {
                            window.location.href="/order_detail.htm?orderId="+result;
                        },
                        function () {
                            layer.closeAll();
                        }
                    );
                }
            }else{
                layer.msg(data.msg);
            }
        },
        error:function(){
            layer.msg("客戶端請求有誤");
        }
    });
}

壓測

測試環境 1g + 4核 + 50000個請求

這裡寫圖片描述