高併發場景-訂單庫存防止超賣

背景

在電商系統中買商品過程,先加入購物車,而後選中商品,點擊結算,即會進入待支付狀態,後續支付。過程須要檢驗庫存是否足夠,保證庫存不被超賣。前端

場景一:買家須要購買數量能夠多件場景二:秒殺活動,到時間點只能購買一件git

目的

  • 防止相同用戶重複下單
  • 檢查庫存準確數量
  • 防止扣錯庫存數量
  • 扣庫存時性能效率提高、不阻塞用戶

主要解決手段

  • 利用redis的incr、decr的原子性作操做
  • redis的lpush、rpop的原子性作操做,可是這個只能一個一個的扣,但不能原子地同時扣多個
  • sql樂觀鎖

交互流程

商品扣庫存.jpg

主要環節:購物車->結清->支付redis

本文講述結清時,扣庫存環節,分佈式系統產生訂單環節後續文章再詳細分析。sql

備註:挺推薦使用https://www.processon.com/在線來作流程圖的數據庫

1、防止重複

利用redis分佈式鎖

用分佈式鎖,是爲了防刷、防止同一個用戶同一秒裏面把購物車裏的商品進行屢次結算,防止前端代碼出問題觸發兩次。利用Jedis客戶端編寫分佈式鎖後端

String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);複製代碼

lockKey是redis的Key,爲用戶id+商品id+商品數量組成,這樣同一秒中只能有一次處理邏輯。requestId是redis的value,實際是當前線程id,表示有一條線程佔用。緩存

你們要注意這種分佈式鎖寫法,是同時設定超時時間的。有些分佈式鎖的文章多是比較舊版的redis不支持同時設置超時時間,他就一條語句先設置key value,另外一條語句後設置超時時間。因此你們留意一下。安全

2、扣減庫存

安全扣減庫存方案有不少說法,列一下幾個方案和我推薦的方案。網絡

方案一:分佈式鎖

有的文章會用redis分佈式鎖來作保證扣庫存數量準確的環節,讓點擊結算時,後端邏輯會查詢庫存和扣庫存的update語句同時只有一條線程可以執行,以商品id爲分佈式鎖的key,鎖一個商品。可是這樣,其餘購買相同商品的用戶將會進行等待。併發

  • 優勢:這樣作雖然安全
  • 缺點:可是失去的是性能問題。

方案二:分佈式鎖+分段緩存

也有文章會說借鑑ConcurrenthashMap,分段鎖的機制,把100個商品,分在3個段上,key爲分段名字,value爲庫存數量。用戶下單時對用戶id進行%3計算,看落在哪一個redis的key上,就去取哪一個。

如key1=product-01,value1=33;key2=product-02,value2=33;key3=product-03,value3=33;

其實會有幾個問題:

  • 一個是用戶想買34件的時候,要去兩個片查
  • 一個片上賣完了爲0,又要去另一個片查
  • 取餘方式計算每一片數量,除不盡時,讓最後一片補,如100/3=33.33。

缺點:

  • 方案複雜
  • 有遺留問題

方案三: redis的lpush rpop

redis隊列的lpush、rpop都是隻能每次進出一個,對於購買多個數量的狀況下不適用,只適用於秒殺狀況購買一個的場景、或者搶紅包的場景,因此以爲不是很通用。

備註:這個搶紅包場景之後再分享。

方案四:推薦使用redis原子操做+sql樂觀鎖

利用Redis increment 的原子操做,保證庫存數安全

  1. 先查詢redis中是否有庫存信息,若是沒有就去數據庫查,這樣就能夠減小訪問數據庫的次數。
    獲取到後把數值填入redis,以商品id爲key,數量爲value。
    注意要設置序列化方式爲StringRedisSerializer,否則不能把value作加減操做。
    還須要設置redis對應這個key的超時時間,以防全部商品庫存數據都在redis中。
  2. 比較下單數量的大小,若是夠就作後續邏輯。
  3. 執行redis客戶端的increment,參數爲負數,則作減法。由於redis是單線程處理,而且由於increment讓key對應的value 減小後返回的是修改後的值
    有的人會不作第一步查詢直接減,其實這樣不太好,由於當庫存爲1時,不少作減3,或者減30狀況,其實都是不夠,這樣就白減。
  4. 扣減數據庫的庫存,這個時候就不須要再select查詢,直接樂觀鎖update,把庫存字段值減1 。
  5. 作完扣庫存就在訂單系統作下單。

*樣例場景:*

  1. 假設兩個用戶在第一步查詢獲得庫存等於10,A用戶走到第二步扣10件,同時一秒內B用戶走到第二部扣3件。
  2. 由於redis單線程處理,若A用戶線程先執行redis語句,那麼如今庫存等於0,B就只能失敗,就不會出更新數據庫了。
public void order(OrderReq req) {
        String key = "product:" + req.getProductId();
        // 第一步:先檢查 庫存是否充足
        Integer num = (Integer) redisTemplate.get(key);
          if (num == null){
          // 去查數據庫的數據
          // 而且把數據庫的庫存set進redis,注意使用NX參數表示只有當沒有redis中沒有這個key的時候才set庫存數量到redis
          //注意要設置序列化方式爲StringRedisSerializer,否則不能把value作加減操做
          // 同時設置超時時間,由於不能讓redis存着全部商品的庫存數,以避免佔用內存。
           if (count >=0) {
            //設置有效期十分鐘
            redisTemplate.expire(key, 60*10+隨機數防止雪崩, TimeUnit.SECONDS);
        }
          // 減小常常訪問數據庫,由於磁盤比內存訪問速度要慢
        }
        if (num < req.getNum()) {
            logger.info("庫存不足");
        }
        // 第二步:減小庫存
        long value = redisTemplate.increment(key, -req.getNum().longValue());
        // 庫存充足
        if (value >= 0) {
            logger.info("成功購買");
            // update 數據庫中商品庫存和訂單系統下單,單的狀態未待支付
            // 分開兩個系統處理時,能夠用LCN作分佈式事務,可是也是有機率會訂單系統的網絡超時
            // 也可使用最終一致性的方式,更新庫存成功後,發送mq,等待訂單建立生成回調。
            boolean res= updateProduct(req);
              if (res)
                createOrder(req);
        } else {
            // 減了後小小於0 ,如兩我的同時買這個商品,致使A人第一步時看到還有10個庫存,可是B人買9個先處理完邏輯,
            // 致使B人的線程10-9=1, A人的線程1-10=-9,則如今須要增長剛剛減去的庫存,讓別人能夠買1個
            redisTemplate.increment(key, req.getNum().longValue());
            logger.info("恢復redis庫存");
        }
    }複製代碼

update使用樂觀鎖

updateProduct方法中執行的sql以下:

update Product set count = count - #{購買數量} where id = #{id} and count - #{購買數量} >= 0;複製代碼

雖然redis已經防止了超賣,可是數據庫層面,爲了也要防止超賣,以防redis崩潰時沒法使用或者不須要redis處理時,則用樂觀鎖,由於不必定所有商品都用redis。

利用sql每條單條語句都是有事務的,因此兩條sql同時執行,也就只會有其中一條sql先執行成功,另一條後執行,也如上文說起到的場景同樣。

簡單說一下分佈式事務:

分開兩個系統處理庫存和訂單時,這個時候能夠用LCN框架作分佈式事務,可是由於是http請求的,也是有機率會訂單系統的網絡超時,致使未返回結果。

其實也可使用最終一致性的方式,數據表記錄一條交互流水記錄,更新庫存成功後,更新這個交互流水記錄的庫存操做字段爲已處理,訂單處理字段爲處理中,而後發送mq,等待訂單建立生成回調。也要作定時任務作主動查詢訂單系統的結果,以防沒有結果回來。

方案優點

  • 不須要頻繁訪問數據庫商品庫存還有多少
  • 不阻塞其餘用戶
  • 安全扣減庫存量
  • 內存訪問庫存數量,減小數據庫交互

高併發額外優化

  • 用戶訪問下單是,前端ui可讓用戶觸發結算後,把按鈕置灰色,防止重複觸發。
  • 能夠按照庫存數量來選定是否要用redis,由於若是庫存數量少,或者說最近下單次數少的商品,就不用放redis,由於少人看和買的狀況下,沒必要放redis致使佔用內存。
  • 若是到時間點搶購時,可使用mq隊列形式,用戶觸發購買商品後,進入隊列,讓用戶的頁面一直在轉圈圈,等輪到他買的時候再進入結算頁面,結算頁面的後續流程和本文一致。
    ---

歡迎關注

個人公衆號 :地藏思惟地藏思惟

掘金:地藏Kelvin

簡書:地藏Kelvin

CSDN:地藏Kelvin

個人Gitee: 地藏Kelvin gitee.com/dizang-kelv…

相關文章
相關標籤/搜索