目錄
下面的案例來自筆者的實際工作經歷,涉及到的系統是筆者負責開發和維護的,一個國外的電商平臺。
如果你對電商系統有所瞭解,將有助於你理解下面提到的業務。
如果你沒有相關的知識背景,也沒有關係,我會盡可能簡化地將業務講給你,並且只要求你理解關鍵概念即可。
背景
事情的起因是平臺的某位高階主管的一封郵件,其中提到商品全量庫存的實時性太低,需要各個部門的人協力解決。
庫存同步相關概念
先介紹一下電商平臺的一些基本概念。
庫存就是倉庫中某個SKU(最小庫存單元)在倉庫中實際有量。
比如某型號灰色8核16G記憶體的膝上型電腦就是一個SKU,在倉庫中這個SKU有100臺,那麼它的庫存量就是100。
- 全量和增量庫存
倉庫每天都會把自己實際的庫存量統計出來,這就是全量庫存,倉庫把庫存量傳送給各個銷售終端,這就是全量庫存同步。
同時,為了保證庫存的實時性,防止超賣(賣出比實際庫存量更多的商品,倉庫無法發出貨品,有可能導致客訴)和倉庫有貨但客戶買不到的情況,倉庫會把庫存的變化量也實時分發到各個終端,這個庫存的變化量就是增量庫存。
舉例來說,上面的那個SKU膝上型電腦有一臺送到攝影棚去拍照了,那麼這臺就無法銷售了,倉庫就會推送一個-1的增量庫存到銷售終端;而如果它收到了消費者的退貨,退貨入庫以後,將會推送一個+1的增量庫存。
- 多店鋪與分盤
電商平臺一般都會有多個店鋪入駐,例如3C這個分類下面,可能有蘋果、華為、三星、小米等店鋪。
不同店鋪的庫存是獨立的。
有時候一個SKU在多家店鋪都有售,iPhone X/太空灰色/256GB 在 XXX蘋果平臺旗艦店 、XXX手機大世界店、 XX蘋果折扣店 就是三個不同的庫存記錄。
這就是多店鋪庫存。
作為分銷商,它的倉庫中放著不同平臺、不同品牌的商品。例如上面的手機,在深圳、廣州、上海三個地區倉庫都有貨,並且是分別賣給天貓和京東的,那麼它的庫存記錄就有6條,分別是:
No. | 倉庫 | 渠道 |
---|---|---|
1 | 深圳 | 天貓 |
2 | 深圳 | 京東 |
3 | 廣州 | 天貓 |
4 | 廣州 | 京東 |
5 | 上海 | 天貓 |
6 | 上海 | 京東 |
這就是分盤庫存。
- 庫存清點時間和最後更新時間
在實際操作中,為了保證資料的準確性,平臺會對庫存的時間進行校驗。
例如,倉庫在凌晨 01:00 清點出某SKU庫存量為100,則這條庫存記錄的庫存清點時間就是01:00。
倉庫在01:00清點完以後,在02:00收到了一個退貨,那麼就會推送一個+1的增量到平臺。
一般情況下,全量先發出,平臺應該先收到全量100,再收到增量+1,最後為101。
但如果由於某個中間環節出了問題,先收到增量+1,在收到全量100,那麼最終的庫存量將是100。全量庫存會直接覆蓋平臺現在的庫存量。
因此,如果有一個最後更新時間,記錄是02:00收到的增量,那麼當01:00的全量過來的時候,由於比增量時間要早,將被平臺視為作廢。
庫存流轉過程
實際的庫存資料流轉過程往往不是 「倉庫——>平臺」 這麼短的鏈路,實際鏈路總是很長的:
不同系統的效能不同,實現方式不同,越長的鏈路時延問題就越嚴重。
方案
問題分析
想要解決問題,首先要分析問題。作為平臺技術負責人,我先統計了平臺最近一個月的庫存同步時間,大約是150分鐘,並且每隔幾天會延長几分鐘。
然後我統計了最近一段時間全量庫存的資料變化量,僅僅10天就增加了5w。
- 問題定義: 目前看來,從平臺角度來講,隨著庫存資料量的增加,處理時間不斷延長,再加上整個鏈路很長,造成全量庫存資料的實時性很差。
頭腦風暴
分析完問題,我立即召開了團隊的人員討論解決方案,經過大家討論,可以優化的環節是下面幾個:
- 提升硬體配置
當你拼命練跑步避免遲到的時候,也許給你一輛車就解決問題了。
部門服務的資源緊張,配置極低。
- 修改訊息處理邏輯
目前庫存資料拆分粒度很細(分店鋪分倉庫分門店),加上網路時延,會造成處理時間延長。
- 優化訊息處理的邏輯
庫存資料由訊息中心統一處理,訊息中心會處理訂單、商品、價格、會員、庫存等等多種型別的資料,效率不高。
- 優化全量庫存同步
從平臺處理資料的程式碼流程著手優化。
確定方案
對於方案1需要金主批錢,方案2需要多個系統修改,這些不好談;方案3需要改動整體架構,工作量巨大。
對於解決燃眉之急,方案4的可行性最高,改動量和影響範圍最小。
細化方案
方案4優化全量庫存同步,具體細化為下面三個方面
- 業務精簡和標準化
- 資料處理高效能
- 佇列操作高效能
下面在實施的時候一一詳細說明。
實施
業務精簡和標準化
業務精簡和標準化分為下面幾個方面:
- 全增隔離
目前全量和增量庫存同步使用同一個佇列名,通過欄位判斷是全量還是增量。 這樣增加了程式碼的複雜度,而且原子性不好。 全量庫存單獨佇列,與增量同步隔離。
- 剝離日誌
修改庫存以後需要記錄詳細變更日誌,日誌的實時性要求不高,將改操作剝離為單獨的佇列進行處理。
- 剝離新建
目前同步庫存之前先判斷該商品是否存在,如果存在再判斷該商品在庫存表是否有記錄,如果沒有則新建記錄,有則更新庫存。
由於隨著資料量的增加,新建的記錄(每天1k到3k之間)所佔的比重越來越小,因此將新建的操作也單獨剝離為一個佇列進行處理。
優化訊息處理的邏輯
平臺為分散式系統,訊息處理需要從註冊中心呼叫遠端Dubbo服務,首先將資料處理移動到Dubbo服務中完成,避免了頻繁呼叫Dubbo服務,另外使用多執行緒處理訊息,最大限度利用多核心的優勢。
關於執行緒池的使用,可以參考這篇文章:使用ThreadPoolExecutor構造執行緒池
//構造執行緒池
private static ExecutorService executorService = new ThreadPoolExecutor(
16,
32,
10L,
TimeUnit.MINUTES,
new LinkedBlockingQueue<Runnable>(
2048),new ThreadFactoryBuilder()
.setNameFormat("BatchSyncFullInventory-Pool-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy());
複製程式碼
經過上面的優化,目前處理的時間有了大幅度降低:
佇列操作高效能
經過上面的優化,發現每處理2k條訊息,處理時間在1s以內,但出隊時間接近15s。
因此,下面的優化重點是提高佇列的操作效能。
由於Redis頻繁的操作,會造成RTT(網路時延)不斷延長,可以使用管道來降低RTT。
下面是Spring Data Redis使用管道的方式:
//從佇列中迴圈取出訊息, 使用管道, 減少網路傳輸時間
List<Object> msgList = redisTemplate.executePipelined(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
for (int i = 0; i < batchSize; i++) {
connection.rPop(getQuenueName().getBytes());
}
return null;
}
});
複製程式碼
理論上是這樣的,需要有實際的資料支撐,因此需要通過做實驗來驗證方案的效果。
首先,在測試環境對比了三種不同的出隊方式的效能,分別是單執行緒迴圈出隊、多執行緒迴圈出隊和單執行緒管道出隊。
測試發現使用管道出隊兩千次,只需要70毫秒左右。
最終,使用了 管道+多執行緒,庫存訊息的處理時間降到了30分鐘左右:
關於管道的使用,可以參考這篇文章:Redis管道技術
CPU使用過高
雖然釋出到生產以後,處理時間有了大幅度降低,但是經過監控發現,Redis的CPU使用率一直居高不下。
對於監聽佇列的場景,一個簡單的做法是當發現佇列返回的內容為空的時候,就讓執行緒休眠幾秒鐘,等佇列中累積了一定量資料以後再通過管道去取,這樣就既能享受管道帶來的高效能,又避免了CPU使用率過高的風險。
//如果訊息的內容為空, 則休眠[10]秒鐘以後再繼續取資料,防止大批量地讀取redis造成CPU消耗過高
if (CollectionUtils.isEmpty(messageList)) {
Thread.currentThread().sleep(10 * 1000);
continue;
}
複製程式碼
總結
- 方案設計:頭腦風暴與可行性評估
- 邏輯精簡化 : 剝離不必要的操作
- 流程標準化 : 梳理統一業務的流程
- 執行緒池實現高效能併發 : Executor Service
- 管道實現高效能佇列 : Redis Pipelining
作為一個工程師,要知道自己能力的邊界在哪裡,利用有限的資源讓方案落地。
這裡優化的經歷,是想讓大家對電商相關的業務有所瞭解,另外對處理問題的解決思路有所借鑑。