库存系统难破题?京东到家来分享
http://www.sohu.com/a/194461959_467759
目前,京東到家庫存系統經歷兩年多的線上考驗與技術迭代,現服務著萬級商家、十萬級店鋪的規模,在需求變更與技術演進中,如何做到系統的穩定性與高可用?下面將會給你揭曉答案。
庫存系統技術架構
庫存系統技術架構圖
上圖如果進行總結下,主要體現出以下幾個方面:
完善的基礎設施
強大的基礎服務平臺讓應用、JVM、Docker、物理機所有健康指標一目了然,7*24 小時智能監控告警讓開發無須一直盯著監控;
數據驅動
數據與業務相輔相成,用數據驗證業務需求,迭代業務需求,讓業務需求都盡可能的收益最大化,庫存系統的開發同學只需要關注業務需求;
健全的測試團隊
大版本上線前相應的測試同學會跟進壓測,防止上線后潛在的性能瓶頸。
庫存系統技術架構圖解釋說明
Portal
通過提供商家 PC 端、App 端解決大部分中小商家的日常運營需求,另外提供開放平臺滿足大中型商家系統對接與數據共享互通的問題。
Service
這個板塊涵蓋了整個庫存最核心的 C&B 數據業務。
1、業務類
-
C 正常流程:用戶下單 - 商家揀貨 - 快遞員妥投
-
C 異常流程 - 缺貨:用戶下單 - 商家缺貨 - 用戶協商 - 調整訂單缺貨商品 - 商家揀貨 - 快遞員妥投
-
C 異常流程 - 取消:用戶下單 - 用戶反悔 - 訂單取消
-
C 異常流程 - 風控:用戶下單 - 風控攔截 - 訂單鎖定 - 客服審核 - 訂單取消 / 繼續生產
-
B 正常流程:商家維護可售庫存數量,即時或者定時生效
2、數據類
除了業務類需求外,京東到家還提供了大量有商業價值的數據供商家作業務決策,比如:
-
商品銷量 Top 榜 - 支持分城市分類目篩選
-
熱銷商品庫存不足預警 - 商家 App 版本 Push 通知及待辦事項中可以醒目識別這部分商品并進行維護
-
紅黃線自動下架 - 近七日訂單量大于 5 單,并且被踩率大于等于 20% 的商品,進行下架操作,每日執行。
-
庫存交易流水
3、中間件類
古人行軍打仗,兵馬未動,糧草先行,對于系統來說亦是如此,編碼未動架構先行,架構的技術選型非常重要,在這里給大家分享京東技術體系上萬碼農都在使用的幾個中間件。
-
JSF,類似于 DUBBO, 是一款非常優秀的 RPC 層框架,可以解決應用間的數據通信問題,它最主要的優勢是長連接的實現以及高效的序列化組件。
-
JMQ,JMQ 是京東自主研發的一款消息中間件系統,具有高可用、數據高可靠等特性。廣泛應用于公司內部系統,包括訂單、支付、庫房、交易等場景。在庫存系統中會優先更新 Redis 緩存數據,并發送變更 MQ,供 MySQL 及 ES 異步更新。
-
O2OWORKER,早期淘寶開源的一款產品 TBSCHEDULE,不這個只適用于單項目管理,多個系統使用的話權限無法隔離,另外參數配置過于繁瑣,結合這兩點進行了重構,從而形成了現在的整個京東到家都在使用的任務管理平臺。
DB
1、MySQL
京東到家庫存系統使用的關系型數據庫是 MySQL,低成本、低耦合、輕量級,總之優勢多多。
2、Redis
豐富的數據結構 & 眾多的原子性命令支持,非常適合庫存系統進行緩存查詢及扣減操作。
3、ES
庫存系統的數據量非常大,首先 MySQL 數據庫通過水平擴容來解決單表數據量過大的問題,水平擴容的規則采取的是按門店維度進行分表(1. 目前京東到家還沒有到分庫的階段,2. 按門店維度進行分表數據量會相對均衡一些,所以沒有按照商家維度進行劃分)。
那么在商家 PC 端上查詢所有商品庫存及維護庫存時帶來了難度,比如查詢該商家下所有的商品有多少條,同時處于上架狀態的商品有哪些……,為了解決這一難題,引入了 ES,將數據統一存儲在 ES 集群中,解決一些涉及到聚合查詢的場景。
庫存系統數據流轉
庫存系統數據流轉圖
庫存系統數據流轉圖解釋說明:
庫存系統的數據流轉,指的都是銷售庫存的數據流轉,在京東到家還有自營類業務板塊,即上圖中提到的城市倉,由于它涉及到采購入庫及盤盈盤虧等問題, 所以會由一套 WMS 系統來支撐。
京東到家設計初衷就是希望商家下的商品各門店共享,帶來的問題就是商家新建一個商品時,需要推送到商家下所有的門店中,即所有的門店均可以看到這個商品?;蛘呱碳倚陆ㄒ粋€門店時,需要將商家下所有的商品均推送到這個新建的門店中,所以這采用了 MQ 技術進行異步化批量處理。
寫到這里,相信對大家對庫存系統有了初步的了解,從上圖來看功能上其實并不復雜,但是他面臨的技術復雜度卻是相當高的,比如秒殺品在高并發的情況下如何防止超賣。
另外庫存系統還不是一個純技術的系統,需要結合用戶的行為特點來考慮,比如下文中提到什么時間進行庫存的扣減最合適,我們先拋出幾個問題和大家一起探討下,如有不妥之處,歡迎大家拍磚。
庫存什么時候進行預占 (或者扣減) 呢
商家銷售的商品數量是有限的,用戶下單后商品會被扣減,我們可以怎么實現呢?
舉個例子: 一件商品有 1000 個庫存,現在有 1000 個用戶,每個用戶計劃同時購買 1000 個。
-
實現方案 1:如果用戶加入購物車時進行庫存預占,那么將只能有 1 個用戶將 1000 個商品加入購物車。
-
實現方案 2:如果用戶提交訂單時進行庫存預占,那么將也只能有 1 個用戶將 1000 個商品提單成功,其它的人均提示“庫存不足,提單失敗”。
-
實現方案 3:如果用戶提交訂單 & 支付成功時進行庫存預占,那么這 1000 個人都能生成訂單,但是只有 1 個人可以支付成功,其它的訂單均會被自動取消。
京東到家目前采用的是?方案 2,理由如下:
用戶可能只是暫時加入購物車,并不表示用戶最終會提單并支付。
所以在購物車進行庫存校驗并預占,會造成其它真正想買的用戶不能加入購物車的情況,但是之前加車的用戶一直不付款,最終損失的是公司。
方案 3 會造成生成 1000 個訂單,無論是在支付前校驗庫存還是在支付成功后再檢驗庫存,都會造成用戶準備好支付條件后卻會出現 99.9% 的系統取消訂單的概率,也就是說會給 99.9% 的用戶體驗到不爽的感覺。
數據表明用戶提交訂單不支付的占比是非常小的(相對于加入購物車不購買的行為),目前京東到家給用戶預留的最長支付時間是 30 分鐘,超過 30 分鐘訂單自動取消,預占的庫存自動釋放。
綜上所述,方案 2 也可能由于用戶下單預占庫存但最終未支付,造成庫存 30 分鐘后才能被其它用戶使用的情況,但是相較于方案 1,方案 3 無疑是折中的最好方案。
重復提交訂單的問題?
重復提交訂單造成的庫存重復扣減的后果是比較嚴重的。比如商家設置有 1000 件商品,而實際情況可能賣了 900 件就提示用戶無貨了,給商家造成無形的損失
可能出現重復提交訂單的情況:
-
1、用戶善意行為:app 上用戶單擊“提交訂單”按鈕后由于后端接口沒有返回,用戶以為沒有操作成功會再次單擊“提交訂單”按鈕
-
2、用戶惡意行為:黑客直接刷提單接口,繞過 App 端防重提交功能
-
3、提單系統重試:比如提單系統為了提高系統的可用性,在第一次調用庫存系統扣減接口超時后會重試再次提交扣減請求
好了,既然問題根源縷清楚了,我們一一對癥下藥
-
1、用戶善意行為:App 側在用戶第一次單擊“提交訂單”按鈕后對按鈕進行置灰,禁止再次提交訂單
-
2、用戶惡意行為:采用令牌機制,用戶每次進入結算頁,提單系統會頒發一個令牌 ID(全局唯一),當用戶點擊“提交訂單”按鈕時發起的網絡請求中會帶上這個令牌 ID, 這個時候提單系統會優先進行令牌 ID 驗證,令牌 ID 存在 & 令牌 ID 訪問次數 =1 的話才會放行處理后續邏輯,否則直接返回
-
3、提單系統重試:這種情況則需要后端系統(比如庫存系統)來保證接口的冪等性,每次調用庫存系統時均帶上訂單號,庫存系統會基于訂單號增加一個分布式事務鎖。
偽代碼如下:
庫存數據的回滾機制如何做?
需要庫存回滾的場景也是比較多的,比如:
-
1、用戶未支付:用戶下單后后悔了
-
2、用戶支付后取消:用戶下單 & 支付后后悔了
-
3、風控取消:風控識別到異常行為,強制取消訂單
-
4、耦合系統故障:比如提交訂單時提單系統 T1 同時會調用積分扣減系統 X1、庫存扣減系統 X2、優惠券系統 X3,假如 X1、X2 成功后,調用 X3 失敗,需要回滾用戶積分與商家庫存。
其中場景 1、2、3 比較類似,都會造成訂單取消,訂單中心取消后會發送 MQ 出來,各個系統保證自己能夠正確消費訂單取消 MQ 即可。
而場景 4 訂單其實尚未生成,相對來說要復雜些,如上面提到的,提單系統 T1 需要主動發起庫存系統 X2、優惠券系統 X3 的回滾請求(入參必須帶上訂單號),X2、X3 回滾接口需要支持冪等性。
其實針對場景 4,還存在一種極端情況,如果提單系統 T1 準備回滾時自身也宕機了,那么庫存系統 X2、優惠券系統 X3 就必須依靠自己來完成回滾操作了,也就是說具備自我數據健康檢查的能力,具體來說怎么實現呢?
可以利用當前訂單號所屬的訂單尚未生成的特點,可以通過 worker 機制,每次撈取 40 分鐘(這里的 40 一定要大于容忍用戶的支付時間)前的訂單,調用訂單中心查詢訂單的狀態,確保不是已取消的,否則進行自我數據的回滾。
多人同時購買 1 件商品,如何安全地庫存扣減?
現實中同一件商品可能會出現多人同時購買的情況,我們可以如何做到并發安全呢?
偽代碼片段 1:
偽代碼片段 1 的設計思想是所有的請求過來之后首先加鎖,強制其串行化處理,可見其效率一定不高。
偽代碼片段 2:
這段代碼只是在 where 條件里增加了and stockNum>="+requestBuyNum即可防止超賣的行為,達到了與上述偽代碼 1 的功能。
如果商品是促銷品(比如參與了秒殺的商品)并發扣減的機率會更高,那么數據庫的壓力會更高,這個時候還可以怎么做呢?
海量的用戶秒殺請求,本質上是一個排序,先到先得。但是如此之多的請求,注定了有些人是搶不到的,可以在進入上述偽代碼 Dao 層之前增加一個計數器進行控制,比如有 50% 的流量將直接告訴其搶購失敗,偽代碼如下:
另外同一個用戶,不允許多次搶購同一件商品,我們又該如何做呢?
如果同一個用戶擁有不同的帳號,來搶購同一件商品,上面的策略就失效了。一些公司在發展早期幾乎是沒有限制的,很容易就可以注冊很多個賬號。也即是網絡所謂的“僵尸賬號”,數量龐大,如果我們使用幾萬個“僵尸號”混進去搶購,這樣就可以大大提升我們中獎的概率,那我們如何應對呢?
庫存系統的核心表結構設計
下面列出了庫存系統的核心表結構,提供出來供大家在工作中能夠有所參考。
庫存主表,命名規則:stock_center_00~99 庫存主表
庫存流水表,命名規則:stock_center_flow_00~99 庫存流水表
庫存批量操作日志表,命名規則:batch_upload_log 庫存批量操作日志表
作者介紹
柳志崇,2008 年計算機專業畢業,一直從事于移動互聯網及 O2O 新零售業務領域的工作,參與過京東到家多個億級 PV 系統的研發與架構,對高并發有著豐富的實戰經驗。
本文是聊聊架構社群分享的內容。如果你也有好內容,歡迎你也來社群分享。
Q&A
Q:
億級 PV 系統的架構能否介紹下,怎么做到高并發的?
A:
高并發這個詞業內外使用得很泛濫,因為并沒有一個統一的定義,比如 qps、tps 達到多少就是高并發了。
一個系統設計的早期更多地關注功能迭代,隨著平臺的發展,用戶、商家、商品數據的持續增長,直到有一天有人告訴你,說你的系統太慢了,或者程序處理上的數據不對時,表明你是時候該重視高并發了。
接下來我談談關于庫存系統這塊高并發的思路,供參考。
-
服務接口的無狀態化設計,方便隨時隨地可以水平擴容
-
服務接口的冪等性設計,防止重復提交造成的重復扣減
-
服務接口的限流與截流設計,應對異常流量造成整個系統癱瘓
-
針對讀多寫少的場景進行數據緩存,緩存時還應該注意緩存擊穿的問題
-
庫存數據持續增多時勢必會考慮數據庫分庫分表,分庫分表路由規則設計:1、一定要緊貼業務,否則在一些聚合查詢上非常麻煩;2、避免短期內出現二次擴容的可能性
關于庫存分庫或分表使用什么策略,京東到家接入的商家多半是優質商家,通常一個商家會有多個門店,目前庫存系統采用的門店維度進行分表:
1、目前的體量還沒有進行分庫;
2、分表路由算法是門店編號取模 + 大門店定向路由組合,來避免簡單的取模算法造成表數據分布可能出現的嚴重不平衡問題
Q:
為什么京東到家的庫存和京東商城的庫存要單獨的呢?
A:
京東商城與京東到家是兩款 App,兩款有著各自獨立的消費場景與目標人群,系統設計上是有些差異化的,如果我們從架構的角度來說,他們之間是解耦的,帶來顯而易見的好處就是其中一個掛了不會影響到另外一個。
京東到家比京東商城起步晚了整整 12 年,所以設計之初是借鑒了京東商城的庫存系統的,但是京東到家中涵蓋的服務類商品 (比如上門美甲按摩庫存是具體的人,而一個人是不能同時被預約提供服務的)、外賣類商品 (庫存相對更加簡單,通常只有貨品充足、貨品緊張、無貨幾種狀態) 是京東商城中沒有的。
另外京東到家處于一個產品高速迭代期,可能一周就一個版本,由于與京東商城相互獨立,有問題了影響也可以控制到很小。
Q:
1、定時 40 分撈取,萬一 39 分那時候服務重啟了,錯過這個定時任務撈取,怎么辦?
2、同一局域網中,如果對外 IP 都是同一個,受到限制那怎么辦?
3、取模限制一部分流量搶購失敗,萬一流量在促銷庫存量之內,那是不是最多只能賣出一半?從取???#xff0c;也就是 userID 被模為 1 的用戶,總是搶不到了?
A:
1、這個問題很好,問得很仔細,這一塊就考驗定時任務的調度機制了,首先調度策略是每分鐘執行,檢測前 40 分鐘到前 50 分鐘之間訂單
2、看 doBuy1 方法,這里其實提到了,當同一個公網 IP 訪問請求量超過了預設閾值,就會增加一個驗證碼環節
3、京東到家 App 目前 DAU 過百萬了,取模機制造成的只能賣出一半這個問題還沒有遇到過,不過有這個擔心是好的,實際開發過程中可以設這個取模的值為動態放行流量即可;另外你提到的 userId 被模 1,這個問題是不存在的,因為我不是用 userId 來取模的,請重新讀 buy 方法。
Q:
請問一下在高峰期每秒以下這個表會有多少的 tps?“庫存主表,命名規則:stock_center_00~99”
A:
首先這個表是異步更新的,不會阻塞主流程,目前觀察 tps 可以達到 800
Q:
如果是異步的話,就會有可能出現查庫存和實際庫存有差異。你是先把庫存都放 redis 然后再用 mq 扣減嗎?或者還是有其它解決方案?
A:
對外提供的 curd 都是基于 redis 的,所以不會出現不一致的問題,mysql 異步更新只是為了數據的持久化。
Q:
異地多活,用戶維度單元化下庫存是怎么處理的?
A:
異地多活,目前我們做到了服務的擴機房部署,數據的擴機房準實時備份,還沒有進行用戶維度的單元化。我也沒有實戰過,不過我覺得異地多活,架構設計有幾點需要關注:
-
1、避免冷備,即如果一個機房不出意外,另外一個機房永遠就是一個 backup,對于大型互聯網公司這個成本是非常高的;
-
2、機房同源策略,一個網絡請求可能涉及到幾個幾十個系統的協同,應當將這部分處理控制到同一個機房處理;
-
3、就近訪問策略,通過智能 DNS,路由到離用戶最近的機房機房。
轉載于:https://www.cnblogs.com/davidwang456/articles/10251560.html
總結
以上是生活随笔為你收集整理的库存系统难破题?京东到家来分享的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 商品中心设计
- 下一篇: Java 中15种锁的介绍:公平锁,可重