7. 秒殺系統-- “減庫存” 設計的核心邏輯

減庫存的幾種方式

總結來說,減庫存操作一般有如下幾個方式:

  • 下單減庫存,即當買家下單後,在商品的總庫存中減去買家購買數量。下單減庫存是最簡單的減庫存方式,也是控制最精確的一種,下單時直接通過數據庫的事務機制控制商品庫存,這樣一定不會出現超賣的情況。但是你要知道,有些人下完單可能並不會付款。
  • 付款減庫存,即買家下單後,並不立即減庫存,而是等到有用戶付款後才真正減庫存,否則庫存一直保留給其他買家。但因爲付款時才減庫存,如果併發比較高,有可能出現買家下單後付不了款的情況,因爲可能商品已經被其他人買走了。
  • 預扣庫存,這種方式相對複雜一些,買家下單後,庫存爲其保留一定的時間(如 10 分鐘),超過這個時間,庫存將會自動釋放,釋放後其他買家就可以繼續購買。在買家付款前,系統會校驗該訂單的庫存是否還有保留:如果沒有保留,則再次嘗試預扣;如果庫存不足(也就是預扣失敗)則不允許繼續付款;如果預扣成功,則完成付款並實際地減去庫存。

減庫存可能存在的問題

       由於購物過程中存在兩步或者多步的操作,因此在不同的操作步驟中減庫存,就會存在一些可能被惡意買家利用的漏洞,例如發生惡意下單的情況。

下單減庫存

       即用戶下單後就減去庫存,正常情況下,買家下單後付款的概率會很高,所以不會有太大問題。但是有一種場景例外,就是當賣家參加某個活動時,此時活動的有效時間是商品的黃金售賣時間,如果有競爭對手通過惡意下單的方式將該賣家的商品全部下單,讓這款商品的庫存減爲零,那麼這款商品就不能正常售賣了。要知道,這些惡意下單的人是不會真正付款的,這正是“下單減庫存”方式的不足之處。

付款減庫存

      採用“付款減庫存”的方式可以解決 ”下單減庫存中“ 存在的惡意下單行爲。但是會導致另一個問題即:庫存超賣。假如有 100 件商品,就可能出現 300 人下單成功的情況,因爲下單時不會減庫存,所以也就可能出現下單成功數遠遠超過真正庫存數的情況,這尤其會發生在做活動的熱門商品上。這樣一來,就會導致很多買家下單成功但是付不了款,買家的購物體驗自然比較差。

預扣庫存

      預扣庫存就是將上面的兩種方法進行了結合,將兩次操作進行前後關聯起來,下單時先預扣,在規定時間內不付款再釋放庫存。這種方案確實可以在一定程度上緩解上面的問題,但卻並沒有徹底的解決,針對惡意下單這種情況,雖然把有效的付款時間設置爲 10 分鐘,但是惡意買家完全可以在 10 分鐘後再次下單,或者採用一次下單很多件的方式把庫存減完。針對這種情況,解決辦法還是要結合安全和反作弊的措施來制止。針對惡意下單這種情況,雖然把有效的付款時間設置爲 10 分鐘,但是惡意買家完全可以在 10 分鐘後再次下單,或者採用一次下單很多件的方式把庫存減完。針對這種情況,解決辦法還是要結合安全和反作弊的措施來制止。

庫存超賣

       針對此情況,在 10 分鐘時間內下單的數量仍然有可能超過庫存數量,遇到這種情況我們只能區別對待:對普通的商品下單數量超過庫存數量的情況,可以通過補貨來解決;但是有些賣家完全不允許庫存爲負數的情況,那只能在買家付款時提示庫存不足。

大型秒殺中的減庫存設計

       目前來看,業務系統中最常見的就是預扣庫存方案,像你在買機票、買電影票時,下單後一般都有個“有效付款時間”,超過這個時間訂單自動釋放,這都是典型的預扣庫存方案。

       由於參加秒殺的商品,一般都是“搶到就是賺到”,所以成功下單後卻不付款的情況比較少,再加上賣家對秒殺商品的庫存有嚴格限制,所以秒殺商品採用“下單減庫存”更加合理。另外,理論上由於“下單減庫存”比“預扣庫存”以及涉及第三方支付的“付款減庫存”在邏輯上更爲簡單,所以性能上更佔優勢。

       下單減庫存”在數據一致性上,主要就是保證大併發請求時庫存數據不能爲負數,也就是要保證數據庫中的庫存字段值不能爲負數,一般我們有多種解決方案:一種是在應用程序中通過事務來判斷,即保證減後庫存不能爲負數,否則就回滾;另一種辦法是直接設置數據庫的字段數據爲無符號整數,這樣減後庫存字段值小於零時會直接執行 SQL 語句來報錯;再有一種就是使用 CASE WHEN 判斷語句,例如這樣的 SQL 語句:

UPDATE item SET inventory = CASE WHEN inventory >= xxx THEN inventory-xxx ELSE inventory END

秒殺減庫存的極致優化

       在交易環節中,“庫存”是個關鍵數據,也是個熱點數據,因爲交易的各個環節中都可能涉及對庫存的查詢。但是,我在前面介紹分層過濾時提到過,秒殺中並不需要對庫存有精確的一致性讀,把庫存數據放到緩存(Cache)中,可以大大提升讀性能。解決大併發讀問題,可以採用 LocalCache(即在秒殺系統的單機上緩存商品相關的數據)和對數據進行分層過濾的方式,但是像減庫存這種大併發寫無論如何還是避免不了,這也是秒殺場景下最爲核心的一個技術難題。

       秒殺商品和普通商品的減庫存還是有些差異的,例如商品數量比較少,交易時間段也比較短,因此這裏有一個大膽的假設,就是把秒殺商品減庫存直接放到緩存系統中實現,也就是直接在緩存中減庫存或者在一個帶有持久化功能的緩存系統(如 Redis)中完成。如果你的秒殺商品的減庫存邏輯非常單一,比如沒有複雜的 SKU 庫存和總庫存這種聯動關係的話,這種方案是可行的。但是如果有比較複雜的減庫存邏輯,或者需要使用事務,還是必須在數據庫中完成減庫存。

      由於 MySQL 存儲數據的特點,同一數據在數據庫裏肯定是一行存儲(MySQL),因此會有大量線程來競爭 InnoDB 行鎖,而併發度越高時等待線程會越多,TPS(Transaction Per Second,即每秒處理的消息數)會下降,響應時間(RT)會上升,數據庫的吞吐量就會嚴重受影響。這就可能引發一個問題,就是單個熱點商品會影響整個數據庫的性能, 導致 0.01% 的商品影響 99.99% 的商品的售賣,這是我們不願意看到的情況。一個解決思路是遵循前面介紹的原則進行隔離,把熱點商品放到單獨的熱點庫中。但是這無疑會帶來維護上的麻煩,比如要做熱點數據的動態遷移以及單獨的數據庫等。而分離熱點商品到單獨的數據庫還是沒有解決併發鎖的問題,此時可以採用以下這兩種方法:

  • 應用層做排隊。按照商品維度設置隊列順序執行,這樣能減少同一臺機器對數據庫同一行記錄進行操作的併發度,同時也能控制單個商品佔用數據庫連接的數量,防止熱點商品佔用太多的數據庫連接。
  • 數據庫層做排隊。應用層只能做到單機的排隊,但是應用機器數本身很多,這種排隊方式控制併發的能力仍然有限,所以如果能在數據庫層做全局排隊是最理想的。阿里的數據庫團隊開發了針對這種 MySQL  的 InnoDB 層上的補丁程序(patch),可以在數據庫層上對單行記錄做到併發排隊。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章