天天看點

庫存領域核心能力--庫存預占 建設實踐

作者:京東雲開發者

前言

本文總結庫存領域建設庫存預占能力時遇到的問題以及解決方案。感謝【金鵬】、【孫靜】、【陳瑞】同學在本文撰寫中提供的内容及幫助!

1、庫存預占業務概述

消費者拍下商品訂單後,庫存系統先為該訂單預留庫存,這個預留庫存的動作被稱為庫存預占。

在系統中,庫存預占主要是對庫存資料進行扣減操作。例:假如一個商品有5個可用庫存,訂單購買了1個此商品,庫存系統需要把可用庫存的數量由5扣減為4

庫存預占屬于物流核心流程。如果預占能力出問題,可能會導緻商品無法正常售賣或者出現超賣。

2、庫存預占能力建設面臨的挑戰及應對

秒殺活動、直播促銷等業務場景,往往會出現短時間内多個訂單都去預占某一個或幾個商品庫存的情況。如何處理高并發場景中對熱點商品進行庫存扣減操作,是庫存預占業務要面臨的主要技術挑戰

庫存領域核心能力--庫存預占 建設實踐



2.1 性能挑戰

多個線程并發對同一個資料庫商品資料做庫存扣減時,資料庫中會加鎖來保障資料被正确操作。當商品資料足夠【熱】時,大量的鎖等待會導緻性能問題,見下圖:

庫存領域核心能力--庫存預占 建設實踐



過往線上業務對固定商品的預占峰值在數百次/秒,而使用正常資料庫預占方案,經壓測資料驗證,僅能支撐50次/秒。一旦發生熱點商品高頻預占,TP99就會飙升,如果高頻預占時間較長,會給系統帶來穩定性風險

2.1.1 解決方案調研

1、異步限流。讓熱點不那麼熱

在業務上允許的情況下,減緩預占操作速度,進而降低熱點熱度,緩解庫存系統的性能壓力。見下圖:

庫存領域核心能力--庫存預占 建設實踐



優點:邏輯簡單,改造風險較小。從整個調用鍊路的角度去優化問題,而不是隻優化瓶頸點

缺點:與下單方的互動機制需要支援異步機制,可能涉及流程改造;

2、商品庫存橫向拆分,提升資料庫處理能力,降低并發請求時資料庫鎖的影響。将一個熱點拆成多個不那麼熱的點

(1)商品入庫時,将數量拆分為N份,放入N個表或者一個表的N行中

(2)預占時,根據預占單據号取餘數,通路不同的資料源進行預占

假如單條記錄支撐的性能是50單/秒,那麼拆分成3份以後,支撐的性能就能提升到100+單/秒。見下圖:

庫存領域核心能力--庫存預占 建設實踐



優點:上遊無感覺,改造可控制在庫存領域

缺點:1、邏輯相對複雜,改造風險高

2、業務有損。存在有庫存,但是預占不到的情況。例:(1)3個資料源都隻有1個可用庫存,但是訂單上數量為2,預占不成功 (2)第一個資料源已經沒有庫存,其他資料源有庫存,但是訂單路由到了第一個資料源。可以使用其他子庫重試、子庫存加和後重試等方式解決此問題,但是邏輯複雜

3、使用緩存抗寫流量。提升熱點處理能力

熱點商品預占的耗時主要集中在資料庫操作上,使用處理速度更快的redis緩存來替代資料庫來提供預占能力,見下圖:

庫存領域核心能力--庫存預占 建設實踐



緩存的處理能力比DB高的多,經壓測,可以支撐熱點1200單/秒

優點:上遊無感覺,改造可控制在庫存領域;

缺點:處理邏輯複雜。需要增加緩存處理邏輯;需要保障緩存-db的資料一緻性

2.1.2 各方案對比及選型

方案 是否能無損支援業務 實作成本
異步限流 否,僅無損支援與下單方異步互動的場景
商品庫存橫向拆分 否,會出現有庫存但無法預占的情況
緩存抗寫流量

結合業務現狀,目前存在部分KA商家體量大,改造風險高,但是這部分KA商家,訂單已經與下單方是異步互動,是以這部分使用【異步限流】方案;其他商家統一使用【緩存抗寫流量】的方案

2.1.3 性能優化成果

通過優化,成功将熱點商品預占TPS從50提升到1200,提升了24倍。TP99降低到130ms,降低至原時長的4.3%(從3000ms到130ms)。

橙色部分為優化後的結果:

庫存領域核心能力--庫存預占 建設實踐



2.2 線程同步問題

問題定義:多個線程操作查詢、操作同一個商品的庫存,使庫存資料混亂

庫存領域核心能力--庫存預占 建設實踐



DB預占模式

解決方案:利用mysql事務、行鎖機制來避免線程之間互相影響,在sql語句中操作變化量

庫存領域核心能力--庫存預占 建設實踐



a、定位庫存。使用商品id、倉庫id、庫存狀态等資訊來定位庫存id

b、操作庫存。根據庫存id扣減庫存,set 目前庫存=目前庫存+操作量。該步驟mysql會在id上加互斥鎖,避免不同線程之間的互相影響。這裡使用批量更新,來提升一單操作多商品的性能

UPDATE stock
    SET stock_num = stock_num + CASE id
        WHEN 1 THEN 'value1'
        WHEN 2 THEN 'value2'
        WHEN 3 THEN 'value3'
    END
WHERE id IN (1,2,3)

           

c、校驗庫存。為了防止超賣,根據庫存id查詢庫存,如果訂單中任一商品庫存被扣減為小于0,則抛出異常,使用資料庫事務機制進行復原

緩存(redis)預占模式

解決方案:将redis操作放入lua腳本中,利用redis單線程執行以及lua腳本執行過程中不會被其他操作語句插入的特性,避免線程間互相影響

庫存領域核心能力--庫存預占 建設實踐



2.3 死鎖問題

•死鎖産生的原因

1、預占流程間死鎖。多個訂單預占商品,包含多個相同商品,多線程并發請求時,線程之間持有對方依賴的鎖,然後等待對方釋放自己依賴的鎖。見下圖:

庫存領域核心能力--庫存預占 建設實踐



2、多流程間死鎖。多種單據(預占、采購、取消等)操作多種類型庫存,多線程并發請求時,線程之間持有對方依賴的鎖,然後等待對方釋放自己依賴的鎖。

注:目前物流庫存平台需要進行操作的庫存資料可以分為倉庫庫存、邏輯庫存、批次庫存。其中邏輯庫存、批次庫存可以看作對某一個倉庫庫存進行不同次元的拆分。

庫存領域核心能力--庫存預占 建設實踐



•如何避免死鎖

鎖排序,保持鎖的順序一緻。在多個事務請求資源的情況下,要保持鎖的請求順序一緻,進而保障線程順序執行。僞代碼如下:

public  Result handleOccupyRequest(List<CalcOccupyRequest> paramList) {
    //XX業務邏輯
    
    //Long類型比較器,根據庫存id進行排序
    Comparator<Long> comparator = new Comparator<Long>() {
        @Override
        public int compare(Long o1, Long o2) {
            return o1.compareTo(o2);
        }
    };
    //對要操作的各類庫存進行排序
    if(saleableStockIds!=null){
        Collections.sort(saleableStockIds, comparator);
    }
    if (otherStockIds!=null){
        Collections.sort(otherStockIds, comparator);
    }
    
    //XX業務邏輯
}

           

2.4 資料一緻性問題

問題定義:redis、db作為兩個獨立的資料源,都需要維護庫存資料,如何保障兩個資料源的最終一緻性?

這個問題又可以拆解為

1、如何從流程處理機制上保障redis-db之間的資料最終一緻性?

2、萬一出現了不一緻,如何發現及解決?

如何從流程處理機制上保障redis-db之間的資料最終一緻性

庫存領域核心能力--庫存預占 建設實踐



•初始化流程方案。使用 鎖定db庫存+redis事務來保障資料一緻性。見下圖:

庫存領域核心能力--庫存預占 建設實踐



•資料同步流程。使用mq重試+任務系統兜底來保障同步能完成

庫存領域核心能力--庫存預占 建設實踐



萬一出現了不一緻,如何發現及解決

•db庫存和redis庫存在不斷的變動中,尤其是同步過程,一定會存在明顯的延遲,怎麼判斷資料是否一緻?

◦引入緩存操作量資料,最大可能的消除同步延遲對資料一緻性比對的影響。操作緩存時,将操作量增加到【緩存操作量資料】中,db同步完成後,從【緩存操作量資料】中減去操作量。資料一緻性比對公式為:【緩存庫存資料】+【緩存操作量資料】=【db庫存資料】

◦使用多次比對來盡量消除剩餘延遲的影響。設定比對次數門檻值,多次比對中隻要有一次判斷 緩存-db資料是一緻的,則判斷為通過一緻性校驗

•庫存每天進行數百萬次變動,如何在節約性能的同時保障及時發現不一緻?

◦針對庫存低于一定水位線的商品,每次庫存變動後都進行一緻性比對,避免因為資料不一緻影響業務

◦庫存充足的商品,設定比對時間間隔,在間隔内區間内,隻進行一次比對,避免影響系統性能

庫存領域核心能力--庫存預占 建設實踐



3、庫存預占處理流程

庫存領域核心能力--庫存預占 建設實踐



緩存處理流程:

庫存領域核心能力--庫存預占 建設實踐