通過請求隊列的方式來緩解高並發搶購(初探)


通過請求隊列的方式來緩解高並發搶購(初探)

 

一、背景

       在移動互聯網高速發展的時代,各種電商平台的搶購業務變得越來越火爆,搶購業務所帶來的高並發問題值得我們去探索,主要涉及的方面包括處理和響應速度、數據的一致性等。搶購開放的一瞬間,可能有成千上萬的下訂單請求發送到服務器去處理,如果只是簡單的請求處理響應方式,不做任何處理,導致的結果很可能是很多客戶很長時間得不到響應,根本不知道自己是否下訂單成功,或者下訂單的數量已經超過了商品的數量,這就導致了超發的問題。

二、設計思路

      1、用戶在下訂單之前當然是先查詢到這個商品,在這個查詢的時候,將數據庫中商品的剩余數量存到redis中;

      2、服務器在一瞬間接到成千上萬的下訂單請求,在控制層沒有直接處理請求數據,而是先根據redis中商品的剩余數量來判斷,如果>0,就將請求放到請求隊列中,否則直接響

           應客戶端“賣完了”;

      3、考慮到數據的一致性,隊列的容量就是商品的剩余數量,隊列采用的是線程安全的隊列LinkedBlockingQueue(單台服務器),然后通過新的線程異步處理這些請求,多台服務器的話,可以考慮使用消息隊列MQ,單獨用一台服務器去處理消息隊列中的請求;

     4、客戶端發送訂單請求之后,會收到響應,要么是剩余數量不足(賣完了),要么是請求已經被放到隊列中,為下一步的輪詢訂單做准備;

     5、如果響應狀態是賣完了,直接提示客戶,如果請求已經放入隊列中,就可以根據用戶id和商品id去輪詢訂單了;

三、實現步驟

      說明:用java語言,springmvc框架+redis實現

    1、准備工作,查詢商品信息,將剩余數量同步到redis中

         Jedis jedis = jedisPool.getResource();
         BuyGood good=buyGoodService.getById(good_id);
        jedis.set("residue"+good_id, good.getResidue()+"");
        jedisPool.returnResource(jedis);

  2、下訂單的方法,下面直接展示代碼,包括請求對象,控制層方法,請求處理線程類的具體實現

     2.1 請求封裝對象

public class BuyRequest {
    private int good_id;//商品id
    private int user_id;//用戶ID
    private int order_id;//訂單id
    private BuyOrders buyOrders;//訂單信息
    private int response_status;//0:未處理;1:正常;2:異常
    
    public BuyOrders getBuyOrders() {
        return buyOrders;
    }

    public void setBuyOrders(BuyOrders buyOrders) {
        this.buyOrders = buyOrders;
    }


    public int getGood_id() {
        return good_id;
    }

    public void setGood_id(int good_id) {
        this.good_id = good_id;
    }

    public int getOrder_id() {
        return order_id;
    }

    public void setOrder_id(int order_id) {
        this.order_id = order_id;
    }

    public int getResponse_status() {
        return response_status;
    }

    public void setResponse_status(int response_status) {
        this.response_status = response_status;
    }

    public int getUser_id() {
        return user_id;
    }

    public void setUser_id(int user_id) {
        this.user_id = user_id;
    }
}

2.2 處理請求的controller

@Controller
@RequestMapping("/buy")
public class BuyController {
    
    private static BuyQueue<BuyRequest> buyqueue =null;//線程安全的請求隊列

@RequestMapping("/addOrders.do")
    @ResponseBody
    public Object addOrders(BuyRequest buyrequest){
        Map<String, Object> results = new HashMap<>();
        Jedis jedis = jedisPool.getResource();
        try {
            //下訂單之前,先獲取商品的剩余數量
        int residue = Integer.valueOf(jedis.get("residue"+buyrequest.getGood_id()));
        if(residue<1){//如果剩余數量不足,直接響應客戶端“賣完了”
            results.put("msg", "賣完了");
            results.put("done", false);
            BaseLog.info("addOrders results="+JSON.toJSONString(results));
            return results;
        }
        //如果還有剩余商品,就准備將請求放到請求隊列中
        if(buyqueue==null){//第一次初始化請求隊列,隊列的容量為當前的商品剩余數量
            buyqueue=new BuyQueue<BuyRequest>(residue);
            }
        if(buyqueue.remainingCapacity()>0){//當隊列的可用容量大於0時,將請求放到請求隊列中
            buyqueue.put(buyrequest);
        }else{//當請求隊列已滿,本次請求不能處理,直接響應客戶端提示請求隊列已滿
            results.put("msg", "搶購隊列已滿,請稍候重試!");
            results.put("done", false);
            return results;
        }
            
        if(!DealQueueThread.excute){//如果線程類的當前執行標志為未執行,即空閑狀態,通過線程池啟動線程
        DealQueueThread dealQueue = new DealQueueThread(buyqueue);
        ThreadPoolUtil.pool.execute(dealQueue);
        BaseLog.info("Thread.activeCount()="+Thread.activeCount());
        }
       //請求放入到隊列中,即完成下單請求
            results.put("done", true);
            results.put("msg", "下訂單成功");
      
        } catch (Exception e) {
            results.put("done", false);
            results.put("msg", "下單失敗");
            BaseLog.info("addOrders results="+JSON.toJSONString(results));
            BaseLog.error("addOrders",e);
        }finally{
            jedisPool.returnResource(jedis);
        }
        return results;
    }

}

2.3 處理請求的線程類,線程類中涉及到的service代碼就不必寫出來了,你懂的

@Component
public class DealQueueThread implements Runnable {

    private static DealQueueThread dealQueueThread;
    @Autowired
    BuyGoodService buyGoodService;
    @Autowired
    BuyOrdersService buyOrdersService;
    @Autowired
    JedisPool jedisPool;

    private Jedis jedis;

    private BuyQueue<BuyRequest> buyqueue;

    public static boolean excute = false;//線程的默認執行標志為未執行,即空閑狀態

    public DealQueueThread() {

    }

    public DealQueueThread(BuyQueue<BuyRequest> buyqueue) {
        this.buyqueue = buyqueue;
        jedis = dealQueueThread.jedisPool.getResource();
    }

    @PostConstruct
    public void init() {
        dealQueueThread = this;
        dealQueueThread.buyGoodService = this.buyGoodService;
        dealQueueThread.buyOrdersService = this.buyOrdersService;
        dealQueueThread.jedisPool = this.jedisPool;
    }

    @Override
    public void run() {
        try {
            excute = true;//修改線程的默認執行標志為執行狀態
            //開始處理請求隊列中的請求,按照隊列的FIFO的規則,先處理先放入到隊列中的請求
            while (buyqueue != null && buyqueue.size() > 0) {
                BuyRequest buyreq = buyqueue.take();//取出隊列中的請求
                dealWithQueue(buyreq);//處理請求
            }
        } catch (InterruptedException e) {
            BaseLog.error("DealQueueThread:", e);
        } finally {
            excute = false;
        }
    }

    public synchronized void dealWithQueue(BuyRequest buyreq) {
        try {
            //為了盡量確保數據的一致性,處理之前先從redis中獲取當前搶購商品的剩余數量
            int residue = Integer.valueOf(jedis.get("residue" + buyreq.getGood_id()));
            if (residue < 1) {//如果沒有剩余商品,就直接返回
                buyreq.setResponse_status(3);
                return;
            }
            //如果有剩余商品,先在redis中將剩余數量減一,再開始下訂單
            jedis.decr("residue" + buyreq.getGood_id());
            //將數據庫中將剩余數量減一,這一步處理可以在隊列處理完成之后一次性更新剩余數量
            dealQueueThread.buyGoodService.minusResidue(buyreq.getGood_id());

            //處理請求,下訂單
            BuyOrders bo = new BuyOrders();
            bo.setGood_id(buyreq.getGood_id());
            bo.setUser_id(buyreq.getUser_id());
            int order_id = dealQueueThread.buyOrdersService.insert(bo);
            BuyOrders orders = dealQueueThread.buyOrdersService.getById(order_id);
            buyreq.setOrder_id(order_id);//訂單id
            buyreq.setBuyOrders(orders);//訂單信息
            buyreq.setResponse_status(1);//處理完成狀態
        } catch (Exception e) {
            buyreq.setResponse_status(2);//異常狀態
            BaseLog.error("DealQueueThread dealWithQueue:", e);
        }
    }

}

3、輪詢訂單

思路:查詢訂單和剩余數量,有以下三種情況:

1)查到訂單,直接跳轉到確認訂單並支付的頁面完成支付;

2)還沒有查詢到訂單,但是剩余數量大於0,說明請求還在隊列中,繼續輪詢;

3)沒有查到訂單,剩余數量等於或小於0,說明搶購失敗了,直接響應客戶搶購失敗;


經過測試在並發量超過五百的時候會出現超發現象,程序還有待完善,歡迎大家給出自己的見解,謝謝!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM