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

如果要設計一套秒殺系統,那我想你的老闆肯定會先對你說:千萬不要超賣,這是大前提。

如果你第一次接觸秒殺,那你可能還不太理解,庫存 100 件就賣 100 件,在數據庫裏減到 0 就好了啊,這有什麼麻煩的?是的,理論上是這樣,但是具體到業務場景中,“減庫存”就不是這麼簡單了。

例如,我們平常購物都是這樣,看到喜歡的商品然後下單,但並不是每個下單請求你都最後付款了。你說系統是用戶下單了就算這個商品賣出去了,還是等到用戶真正付款了纔算賣出了呢?這的確是個問題!

我們可以先根據減庫存是發生在下單階段還是付款階段,把減庫存做一下劃分。

減庫存有哪幾種方式

在正常的電商平臺購物場景中,用戶的實際購買過程一般分爲兩步:下單和付款。你想買一臺 iPhone 手機,在商品頁面點了“立即購買”按鈕,覈對信息之後點擊“提交訂單”,這一步稱爲下單操作。下單之後,你只有真正完成付款操作才能算真正購買,也就是俗話說的“落袋爲安”。

那如果你是架構師,你會在哪個環節完成減庫存的操作呢?總結來說,減庫存操作一般有如下幾個方式:

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

以上這幾種減庫存的方式都會存在一些問題,下面我們一起來看下。

減庫存可能存在的問題

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

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

既然“下單減庫存”可能導致惡意下單,從而影響賣家的商品銷售,那麼有沒有辦法解決呢?你可能會想,採用“付款減庫存”的方式是不是就可以了?的確可以。但是,“付款減庫存”又會導致另外一個問題:庫存超賣。

假如有 100 件商品,就可能出現 300 人下單成功的情況,因爲下單時不會減庫存,所以也就可能出現下單成功數遠遠超過真正庫存數的情況,這尤其會發生在做活動的熱門商品上。這樣一來,就會導致很多買家下單成功但是付不了款,買家的購物體驗自然比較差。

可以看到,不管是“下單減庫存”還是“付款減庫存”,都會導致商品庫存不能完全和實際售賣情況對應起來的情況,看來要把商品準確地賣出去還真是不容易啊!

那麼,既然“下單減庫存”和“付款減庫存”都有缺點,我們能否把兩者相結合,將兩次操作進行前後關聯起來,下單時先預扣,在規定時間內不付款再釋放庫存,即採用“預扣庫存”這種方式呢?

這種方案確實可以在一定程度上緩解上面的問題。但是否就徹底解決了呢?其實沒有!針對惡意下單這種情況,雖然把有效的付款時間設置爲 10 分鐘,但是惡意買家完全可以在 10 分鐘後再次下單,或者採用一次下單很多件的方式把庫存減完。針對這種情況,解決辦法還是要結合安全和反作弊的措施來制止。

例如,給經常下單不付款的買家進行識別打標(可以在被打標的買家下單時不減庫存)、給某些類目設置最大購買件數(例如,參加活動的商品一人最多隻能買 3 件),以及對重複下單不付款的操作進行次數限制等。

針對“庫存超賣”這種情況,在 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),可以在數據庫層上對單行記錄做到併發排隊。

你可能有疑問了,排隊和鎖競爭不都是要等待嗎,有啥區別?

如果熟悉 MySQL 的話,你會知道 InnoDB 內部的死鎖檢測,以及 MySQL Server 和 InnoDB 的切換會比較消耗性能,淘寶的 MySQL 核心團隊還做了很多其他方面的優化,如 COMMIT_ON_SUCCESS 和 ROLLBACK_ON_FAIL 的補丁程序,配合在 SQL 裏面加提示(hint),在事務裏不需要等待應用層提交(COMMIT),而在數據執行完最後一條 SQL 後,直接根據 TARGET_AFFECT_ROW 的結果進行提交或回滾,可以減少網絡等待時間(平均約 0.7ms)。據我所知,目前阿里 MySQL 團隊已經將包含這些補丁程序的 MySQL 開源。

另外,數據更新問題除了前面介紹的熱點隔離和排隊處理之外,還有些場景(如對商品的 lastmodifytime 字段的)更新會非常頻繁,在某些場景下這些多條 SQL 是可以合併的,一定時間內只要執行最後一條 SQL 就行了,以便減少對數據庫的更新操作。

總結一下

今天,我圍繞商品減庫存的場景,介紹了減庫存的三種實現方案,以及分別存在的問題和可能的緩解辦法。最後,我又聚焦秒殺這個場景說了如何實現減庫存,以及在這個場景下做到極致優化的一些思路。

當然減庫存還有很多細節問題,例如預扣的庫存超時後如何進行庫存回補,再比如目前都是第三方支付,如何在付款時保證減庫存和成功付款時的狀態一致性,這些都是很大的挑戰。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章