簡單庫存場景的資料庫實現
一般來說,從資料庫層面講,庫存業務會分為兩步,第一步是插入一條記錄到扣減明細表inventory_detail,第二步是對庫存扣減表inventory的一條記錄進行扣減,這兩步往往是在一個事務中實現的。
資料庫業務架構圖如下,所有的請求均發往同一個Database。
從上文的架構圖不難看出,所有的商品的庫存信息都存在單一的表和庫裡,當商品種類繁多或者業務並發請求暴漲時,單實例的資料庫顯然會成為容量或者性能瓶頸。該資料庫架構一般只是功能性的實現,主要用於微型庫存系統或者測試使用。
高並發庫存系統的資料庫實現
為了解決單實例存在的容量和性能上限問題,阿里巴巴所有的庫存系統在十年前就實現了分庫分表設計,主要通過數據的水平拆分實現不同商品的庫存扣減請求路由到不同的資料庫。基本資料庫架構圖如下
從上圖不難看出,庫存扣減表和扣減明細表一般都使用商品id作為片鍵,這樣可以保證滿足整個系統在高並發扣減請求的同時,同一商品的庫存扣減操作和添加明細操作在同一個事務中實現。如果數據分布和業務請求足夠均勻,理論上經過分庫分表設計後,整個系統的吞吐量將會是線性的增長,主要取決於分實例的數量。
熱點行更新
在電商業務中,商家活動比如秒殺不可避免。秒殺活動會給電商庫存系統帶來巨大的挑戰,尤其體現在資料庫層面。因為往往一個商品id對應於資料庫的一行記錄,所以在DB架構上按照商品維度做了分庫分表也是無效的。而更新這行記錄時必然需要給這行記錄加X鎖。熱點商品的庫存扣減本質上就是熱點行更新的能力,高並發的同行更新會造成嚴重的行鎖等待現象,從而導致資料庫的threads_running和rt飆升,造成雪崩。在當前的官方mysql中,一般單行更新的QPS在500以內,對於熱點商品的秒殺需求,這個量往往是不達標的。
阿里巴巴PolarDB-X資料庫團隊基於以上場景的需求,針對內核優化,引入了先進的水車模型,在識別出熱點SQL後,實現了在內核層面優化處理,相比官方MySQL提高了10倍以上的熱點行扣減能力,廣泛應用於集團電商庫存集群,資金平臺,權益發放平臺等核心資料庫集群。
其主要的核心思想是:針對應用層SQL做輕量化改造,帶上"熱點行SQL"的標籤,當這種SQL進入內核後,在內存中維護一個hash表,將主鍵或唯一鍵相同的請求(一般也就是同一商品id)hash到同一個地方做請求的合併,經過一段時間後(默認100us)統一提交,從而實現了將串行處理變成了批處理,讓每個熱點行更新請求並不需要都去掃描和更新btree。
類似原理,阿里雲RDS資料庫團隊同樣在內核層面針對熱點行更新做了大量的優化,核心思路為引入SQL語句的排隊機制,將可能存在行鎖衝突的語句放在一個組內排序,從而減少行鎖衝突帶來的額外系統開銷。Statement Queque和Inventory Hint可以結合使用,不過在事務中,熱點行更新必須是該事務的最後一條記錄,因為commit on success的機制存在,一旦該SQL執行成功就會自動提交或自動回滾。簡單的使用範例如下
begin;insert into inventory_detail(inventory_id,user_id) values(1,1);update /*+ ccl_queue_value(1) commit_on_success rollback_on_fail target_affect_row(1) */ inventory set inventory_count=inventory_count+1 where inventory_id=1
更多文檔參考inventory hint statement queue
業務架構優化
冪等性實現
在庫存資料庫系統中,一般都會在更新庫存記錄後,寫入一條庫存扣減明細的流水記錄,用於後續可能存在的查詢需求。舉個例子,在集團的權益發放平臺中,庫存流水記錄主要用於實現庫存扣減的冪等性,即同一個用戶只能領取一次權益。在系統的實際運行過程中,可能因為一些網絡故障等其他原因,當底層資料庫的扣減成功以後並沒有成功返回給用戶時,用戶可能會有重試操作,這時就必須避免庫存記錄的重複扣減情況。所以針對這些情況,應用在設計時會考慮先查詢一遍庫存流水記錄,如果該用戶已經領取過該權益,則不再重複扣減,直接返回。為了實現這種強冪等性的需求,庫存扣減和插入流水就必須在同一個事務中,滿足同時成功或同時失敗。
基於緩存的分桶扣減方案
在更大規模,針對單一商品的超高並發扣減的庫存集群中,可能基於資料庫內核的改造優化還無法滿足業務需求。單一商品的超高並發扣減可能會影響到同一資料庫實例上的其他商品扣減,同一個資料庫實例上也可能存在多個熱點商品造成互相影響,這時就需要考慮在業務和資料庫架構上再做一次升級,我們引入基於緩存的分桶扣減方案。
下圖為該方案資料庫架構圖,基於緩存的分桶扣減方案的主要思路為
1、普通的非熱點商品,或者並發度不夠大的熱點商品走強冪等性的分庫分表+資料庫內核改造優化2、超大熱點商品,針對該商品再做多key拆分,先走弱冪等性的緩存扣減,緩存扣減後,異步往DB寫入一條庫存流水記錄,後續再做緩存與資料庫的庫存總量同步
在分桶方案詳細落地實現上,需要考慮的細節問題會多很多,比較重要的有以下幾點
1、分桶管理
為了更通俗和直觀的描述,緩存集群的一個key就對應于于一個"分桶"。要實現一個基於緩存分桶方案的高擴展性的庫存系統,分桶的設計至關重要,比如一個熱點商品應該對應多少個分桶,分桶的數量能否根據當前的業務變化做到彈性的伸縮
1、分桶預分配庫存:當分桶初始化後,每個分桶應該保存多少庫存量。不一定在預分配庫存階段將該商品的庫存數量從DB全部分配到緩存中,可能是一種漸進式的分配策略,DB作為庫存總池子2、分桶擴容/縮容:分桶數量的變化,擴縮容操作本質上是調整桶映射管理內的信息,加入或者減少桶,桶信息一旦增加或者減少了,扣減鏈路會秒級感知到,然後將用戶流量引導或者移除出去。從上面的DB架構圖可以看出,比較簡單的實現方式就是根據當前熱點商品的桶數量取模3、桶內庫存數量擴容/縮容:即每個分桶內該商品的庫存數量變化,擴容場景主要用於當該分桶內庫存接近扣減完成時,系統自動去MySQL庫存集群總池子裡撈一部分過來放進桶內。縮容場景主要場景在於桶下線後將桶內剩餘的庫存回收到庫存總池子中4、合併展示:在基於緩存的分桶設計中,由於同一種熱點商品拆分成了多個key,所以在前端界面展示上同樣會帶來挑戰,需要做庫存的合併2、超賣問題
一個較為簡單的處理超賣問題的思路是預留一部分庫存,當庫存數量低於之前定義的預留值時,直接返回前端庫存扣減完畢,從而避免造成超賣。
3、碎片問題
在一些類庫存系統的設計中,考慮到系統的兼容性和支持的扣減種類,或許扣減的是商品的庫存數量,或許是紅包的金額(將帶小數的紅包金額轉換成整型數扣減)。所謂碎片問題,舉個例子,假如扣減的是紅包金額,假設紅包金額至少要發1塊錢,換算成整型數也就是100,在多個分桶扣減的情況下,最後部分分桶的剩餘庫存值可能低於100,而所有分桶加起來的總額又大於100。如果不做處理,就會造成資損。
應對這種極端場景,系統需要在檢測到存在碎片時,自動將存在碎片的分桶下線納入庫存總池子,由DB總池子再分出少量的緩存key來進行扣減,多次循環直到不存在碎片為止。或者針對出現這種情況後,由於庫存總量已經基本扣減完畢,在納入DB總池子後直接在DB側扣減。