這個是真的厲害,高併發場景下的訂單和庫存處理方案,講的很詳細了!

前言

以前一直有小夥伴私信我問我高併發場景下的訂單和庫存處理方案,我最近也是由於加班的緣由比較忙,就一直沒來得及回覆。今天好不容易閒了下來想了想不如寫篇文章把這些都列出來的,讓你們都能學習到,說一千道一萬都不如滿滿的乾貨來的實在,乾貨都下面了!前端

介紹

前提:分佈式系統,高併發場景
商品A只有100庫存,如今有1000或者更多的用戶購買。如何保證庫存在高併發的場景下是安全的。
預期結果:1.不超賣 2.很多賣 3.下單響應快 4.用戶體驗好mysql

下單思路面試

  1. 下單時生成訂單,減庫存,同時記錄庫存流水,在這裏須要先進行庫存操做再生成訂單數據,這樣庫存修改爲功,響應超時的特殊狀況也能夠經過第四步定時校驗庫存流水來完成最終一致性。
  2. 支付成功刪除庫存流水,處理完成刪除可讓庫存流水數據表數據量少,易於維護。
  3. 未支付取消訂單,還庫存+刪除庫存流水
  4. 定時校驗庫存流水,結合訂單狀態進行響應處理,保證最終一致性

(退單有單獨的庫存流水,申請退單插入流水,退單完成刪除流水+還庫存)redis

何時進行減庫存

  • 方案一:加購時減庫存。
  • 方案二:確認訂單頁減庫存。
  • 方案三:提交訂單時減庫存。
  • 方案四:支付時減庫存。

分析sql

  • 方案一:在這個時間內加入購物車並不表明用戶必定會購買,若是這個時候處理庫存,會致使想購買的用戶顯示無貨。而不想購買的人一直佔着庫存。顯然這種作法是不可取的。惟品會購物車鎖庫存,可是他們是另外一種作法,加入購物車後會有必定時效,超時會從購物車清除。
  • 方案二:確認訂單頁用戶有購買慾望,可是此時沒有提交訂單,減庫存會增長很大的複雜性,並且確認訂單頁的功能是讓用戶確認信息,減庫存不合理,但願你們對該方案發表一下觀點,本人暫時只想到這麼多。
  • 方案三:提交訂單時減庫存。用戶選擇提交訂單,說明用戶有強烈的購買慾望。生成訂單會有一個支付時效,例如半個小時。超過半個小時後,系統自動取消訂單,還庫存。
  • 方案四:支付時去減庫存。好比:只有100個用戶能夠支付,900個用戶不能支付。用戶體驗太差,同時生成了900個無效訂單數據。

因此綜上所述:
選擇方案三比較合理。數據庫

重複下單問題緩存

  1. 用戶點擊過快,重複提交。
  2. 網絡延時,用戶重複提交。
  3. 網絡延時高的狀況下某些框架自動重試,致使重複請求。
  4. 用戶惡意行爲。

解決辦法

  1. 前端攔截,點擊後按鈕置灰。安全

  2. 後臺:
    (1)redis 防重複點擊,在下單前獲取用戶token,下單的時候後臺系統校驗這個 token是否有效,致使的問題是一個用戶多個設備不能同時下單。
//key , 等待獲取鎖的時間 ,鎖的時間
    redis.lock("shop-oms-submit" + token, 1L, 10L);

redis的key用token + 設備編號 一個用戶多個設備能夠同時下單。網絡

//key , 等待獲取鎖的時間 ,鎖的時間
    redis.lock("shop-oms-submit" + token + deviceType, 1L, 10L);

(2)防止惡意用戶,惡意*** : 一分鐘調用下單超過50次 ,加入臨時黑名單 ,10分鐘後纔可繼續操做,一小時容許一次跨時段弱校驗。使用reids的list結構,過時時間一小時併發

/**
     * @param token
     * @return true 可下單
     */
    public boolean judgeUserToken(String token) {
        //獲取用戶下單次數 1分鐘50次
        String blackUser = "shop-oms-submit-black-" + token;
        if (redis.get(blackUser) != null) {
            return false;
        }
        String keyCount = "shop-oms-submit-count-" + token;
        Long nowSecond = LocalDateTime.now().toEpochSecond(ZoneOffset.of("+8"));
        //每一小時清一次key 過時時間1小時
        Long count = redis.rpush(keyCount, String.valueOf(nowSecond), 60 * 60);
        if (count < 50) {
            return true;
        }
        //獲取第50次的時間
        List<String> secondString = redis.lrange(keyCount, count - 50, count - 49);
        Long oldSecond = Long.valueOf(secondString.get(0));
        //now > oldSecond + 60 用戶可下單
        boolean result = nowSecond.compareTo(oldSecond + 60) > 0;
        if (!result) {
            //觸發限制,加入黑名單,過時時間10分鐘
            redis.set(blackUser, String.valueOf(nowSecond), 10 * 60);
        }
        return result;
    }

如何安全的減庫存

多用戶搶購時,如何作到併發安全減庫存?

  • 方案1: 數據庫操做商品庫存採用樂觀鎖防止超賣:
sql:update sku_stock set stock = stock - num where sku_code = '' and stock - num > 0;

分析
高併發場景下,假設庫存只有 1件 ,兩個請求同時進來,搶購該商品.
數據庫層面會限制只有一個用戶扣庫存成功。在併發量不是很大的狀況下能夠這麼作。可是若是是秒殺,搶購,瞬時流量很高的話,壓力會都到數據庫,可能拖垮數據庫。

  • 方案2:利用Redis單線程 強制串行處理
/**
     * 缺點併發不高,同時只能一個用戶搶佔操做,用戶體驗很差!
     *
     * @param orderSkuAo
     */
    public boolean subtractStock(OrderSkuAo orderSkuAo) {
        String lockKey = "shop-product-stock-subtract" + orderSkuAo.getOrderCode();
        if(redis.get(lockKey)){
            return false;
        }
        try {
            lock.lock(lockKey, 1L, 10L);
            //處理邏輯
        }catch (Exception e){
            LogUtil.error("e=",e);
        }finally {
            lock.unLock(lockKey);
        }
        return true;
    }

分析
利用Redis 分佈式鎖,強制控制同一個商品處理請求串行化,缺點併發不高 ,處理比較慢,不適合搶購,高併發場景。用戶體驗差,可是減輕了數據庫的壓力。

  • 方案3 :redis + mq + mysql 保證庫存安全,知足高併發處理,但相對複雜。
/**
     * 扣庫存操做,秒殺的處理方案
     * @param orderCode
     * @param skuCode
     * @param num
     * @return
     */
    public boolean subtractStock(String orderCode,String skuCode, Integer num) {
        String key = "shop-product-stock" + skuCode;
        Object value = redis.get(key);
        if (value == null) {
            //前提 提早將商品庫存放入緩存 ,若是緩存不存在,視爲沒有該商品
            return false;
        }
        //先檢查 庫存是否充足
        Integer stock = (Integer) value;
        if (stock < num) {
            LogUtil.info("庫存不足");
            return false;
        } 
       //不可在這裏直接操做數據庫減庫存,不然致使數據不安全
       //由於此時可能有其餘線程已經將redis的key修改了
        //redis 減小庫存,而後才能操做數據庫
        Long newStock = redis.increment(key, -num.longValue());
        //庫存充足
        if (newStock >= 0) {
            LogUtil.info("成功搶購");
            //TODO 真正扣庫存操做 可用MQ 進行 redis 和 mysql 的數據同步,減小響應時間
        } else {
            //庫存不足,須要增長剛剛減去的庫存
            redis.increment(key, num.longValue());
            LogUtil.info("庫存不足,併發");
            return false;
        }
        return true;
    }

分析
利用Redis increment 的原子操做,保證庫存安全,利用MQ保證高併發響應時間。可是事須要把庫存的信息保存到Redis,並保證Redis 和 Mysql 數據同步。缺點是redis宕機後不能下單。
increment 是個原子操做。

綜上所述

方案三知足秒殺、高併發搶購等熱點商品的處理,真正減扣庫存和下單能夠異步執行。在併發狀況不高,日常商品或者正常購買流程,能夠採用方案一數據庫樂觀鎖的處理,或者對方案三進行從新設計,設計成支持單訂單多商品便可,但複雜性提升,同時redis和mysql數據一致性須要按期檢查。

訂單時效問題
超過訂單有效時間,訂單取消,可利用MQ或其餘方案回退庫存。

設置定時檢查
Spring task 的cron表達式定時任務
MQ消息延時隊列

訂單與庫存涉及的幾個重要知識

TCC 模型:Try/Confirm/Cancel:不使用強一致性的處理方案,最終一致性便可,下單減庫存,成功後生成訂單數據,若是此時因爲超時致使庫存扣成功可是返回失敗,則經過定時任務檢查進行數據恢復,若是本條數據執行次數超過某個限制,人工回滾。還庫存也是這樣。
冪等性:分佈式高併發系統如何保證對外接口的冪等性,記錄庫存流水是實現庫存回滾,支持冪等性的一個解決方案,訂單號+skuCode爲惟一主鍵(該表修改頻次高,少建索引)
樂觀鎖:where stock + num>0
消息隊列:實現分佈式事務 和 異步處理(提高響應速度)
redis:限制請求頻次,高併發解決方案,提高響應速度
分佈式鎖:防止重複提交,防止高併發,強制串行化
分佈式事務:最終一致性,同步處理(Dubbo)/異步處理(MQ)修改 + 補償機制

寫在最後的話

你們看完有什麼不懂的能夠在下方留言討論,也能夠私信問我通常看到後我都會回覆的。也歡迎你們關注個人公衆號:前程有光,金三銀四跳槽面試季,整理了1000多道將近500多頁pdf文檔的Java面試題資料,文章都會在裏面更新,整理的資料也會放在裏面。最後以爲文章對你有幫助的話記得點個贊哦,點點關注不迷路,天天都有新鮮的乾貨分享!

相關文章
相關標籤/搜索