高併發下的庫存扣減方案

高併發下的庫存扣減方案

背景

直接進入主題:如果老闆讓你設計一套高併發下的庫存扣減方案,不能出現超買超賣。你是否有相似的工作經驗?是否有方案的設計思路?近些年在營銷項目組的工作經驗讓我對【庫存扣減】的方案有了些許認知,接下來的文章,帶着大家感受下從0-1的庫存扣減方案的的誕生,歡迎大家的指導!
在這裏插入圖片描述

那年還很low(DB)

剛開始我們的營銷項目組身單力薄,人微言輕;那時營銷業務纔剛開始發展,此時我們把業務放到第一位,技術方案爲滿足時間內業務發展所讓步。大家應該可以猜到,這個時候我們很low的庫存扣減方案-直接上數據庫。根據業務訴求,每個活動會保存一份實時變化的庫存,參與活動時,實時扣減數據庫,操作步驟如下:

  • ①接收業務扣減庫存請求
  • ②數據庫查詢單個活動庫存,並對查詢的單條數據加鎖(select * from 庫存表 for update)
  • ③驗證庫存是否滿足扣減數量,滿足則扣除

在這裏插入圖片描述

線上遇到了問題(DB鎖優化)

可想而知,好景不長,當線上業務逐漸增長,數據庫出現了瓶頸。表現主要是數據庫性能低,大量的資源處於等待鎖的狀態,影響線上業務,影響用戶體驗。當然這裏主要原因還是活動庫存記錄爲熱點數據,當收到大量扣減庫存請求時,針對同一個活動的庫存扣減會因爲數據庫行鎖導致大量線程等待。

此時爲了即可解決生產出現的數據庫鎖問題,臨時方案爲:調整上方步驟的第②步,由【select * from 庫存表 for update】 調整爲【select * from 庫存表 for update nowait】,由之前的數據庫等待鎖改成了非等待鎖。也就是說當有線程1正在查詢且更新活動A庫存時,併發情況下,如果同一時間線程2也需要更新活動A庫存則會提示資源忙且退出操作。
當然這個犧牲了一定的用戶體驗;完全不能用於秒殺類活動。

在這裏插入圖片描述

先扛着(DB異步+同步)

臨時解決了生產問題之後,我們着手調整庫存扣減方案,希望在準確性和用戶體驗上找到一個更好的方案。於是有了這個數據庫的升級版方案。此方案還是主要採用數據庫鎖+異步扣減庫存,步驟如下:

  • 活動配置時,首先初始化活動庫存閥值(到達閥值後庫存扣減由異步轉爲同步)
  • 接收扣減庫存的請求,[同步]或者[異步]扣減庫存
  • 業務成功入庫存扣減請求表(同步-覈算,異步-未覈算)
  • 定時任務異步更新庫存,如果庫存低於閥值,則修改活動庫存狀態爲同步扣減

當然此方案也有弊端:由於活動形式多、優惠力度不同、權益樣式多等特點,合理的設置閥值比較困難;異步轉同步期間,可能會出現超買超賣;同步扣成本雖然與之前相比添加了重試次數,但對用戶體驗還是有很大的犧牲。
在這裏插入圖片描述

破斧沉舟(redis分佈式緩存+redis分佈式鎖+異步)

以上種種方案,都是以數據庫鎖爲基礎做的優化,衆所周知數據庫資源是我們最寶貴且最容易成爲短板的稀缺資源之一。所以我們必須要進一步改進,然後有了下面的方案…

首先我們對於數據庫的[庫存]依然採用異步處理的方式同步,同時使用能夠滿足高併發業務且基於內存的分佈式緩存redis做活動庫存的緩存。業務處理時,先以redis保存的已用庫存爲準做庫存的check和扣減,當緩存丟失時,再實時同步已有庫存到緩存,大概的步驟如下:

  • 活動申請時REDIS初始化活動【已用庫存】,數值爲0(redis作爲分佈式緩存)
  • 接受庫存扣減請求,使用redis的INCR命令更新單個活動的已用庫存數量(redis作爲計數器)
  • 業務處理完成後,同一事務內入庫主業務表、庫存扣減請求表
  • task定時任務採用流式處理實時更新數據庫庫存,同時比對REDIS保存的已用庫存,異常則告警。

REDIS作爲一個高性能的非關係型key-value數據庫,可作爲分佈式緩存、計數器、分佈式鎖、分佈式隊列等。是一種能夠很好解決庫存扣減的方式的技術之一。

在這裏插入圖片描述

結束語

有時候解決問題不僅靠技術的支持,也需要有合理的業務方案;技術+業務+當時利弊的權衡也許能設計出符合現狀的合理的且可持續發展的方案。

正如CAP定理,在一個分佈式系統中,一致性(Consistency)、可用性(Availability)、分區容錯性(Partition tolerance),這三個要素最多隻能同時實現兩點,不可能三者兼顧。

附錄(思考)

  • Redis緩存丟失之後同步緩存是否有問題?有什麼解決方案?
  • 庫存扣減主邏輯中先查詢redis緩存,存在則扣減是否有問題?有什麼解決方案?
  • 普通活動和熱點活動的發現以及庫存扣減隔離…

答案:

  • 關於以上我們的最終方案(redis+異步),其實是以用戶體驗優先,並不是強一致性。當redis因爲服務重啓、緩存淘汰等某些原因key丟失時,也有可能會因爲同步redis緩存和數據insert庫存扣減表併發執行,導致redis緩存數據不準確,需要靠後期定時任務比對矯正才能保證最終一致性。
  • 查詢存在,但恰好更新之前丟失,導致redis緩存庫存異常,可以用redis+luna腳本保證redis操作的原子性(redis本身的事務不想我們平時用的spring事務支持異常回滾)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章