前言 參與過幾個中小型商城系統的開發,隨著時間的增長,以及對系統的深入研究和測試,發現確實有很多值得推敲和商榷的地方(總有很多重要細節存在缺陷)。基於商城系統,無論規模大小,或者本身是否分佈架構,個人覺得最核心的一環就是下單模塊,而這裡面更相關和棘手的一些設計和問題,大多時候都涉及庫存系統。想想之... ...
商城系統下單庫存管控系列雜記(一)(併發安全和性能基礎認識) 前言 參與過幾個中小型商城系統的開發,隨著時間的增長,以及對系統的深入研究和測試,發現確實有很多值得推敲和商榷的地方(總有很多重要細節存在缺陷)。基於商城系統,無論規模大小,或者本身是否分佈架構,個人覺得最核心的一環就是下單模塊,而這裡面更相關和棘手的一些設計和問題,大多時候都涉及庫存系統。想想之前跟某人的交流,他一句“庫存管控做得好,系統設計就成功了一半”,自己頗有認同。圍繞這個點,結合目前經驗和朋友間的交流(包括近來參閱其他文章提到的點),閑來做些整理記錄,也許不太完整,但總歸希望能有更多啟發,自己往後也會重新揣摩。當然,文中若有不妥,歡迎指正。 正文 談及”下單“,就立刻想起前年參與的一個基於微信的小型商城系統,裡面下單這塊本身談不上複雜,大概可以這樣描述提交過程:用戶提交商品訂單,系統核對用戶提交的訂單,校驗商品(商品價格、優惠折扣、積分等),檢測附屬信息(地址運費等),一切Pass,操作庫存(記錄/預扣),生成訂單及相關聯的明細數據。此時下單Ok,那麼後續則是等待用戶的及時付款了。 然而,看似如此簡單的一個流程,放在併發環境下,就暴露了足夠多的問題。深入進去,首當其衝的就是庫存管控。包括但不限於庫存的扣減方式,如何安全操作,以及減少性能損耗等等。 一、簡單提一提通常的庫存扣除時機選擇:“下單減庫存”和“付款減庫存” 首先表明個人觀點,在大多數業務場景下,個人相對傾向前者——“下單減庫存”。後續大部分解決方案的論述,也都是以這個為主要前提展開。當然,針對去年參與的某微信商城系統(之後用“AutumnBing”作為項目代號),兩種是同時實現的 ——— 商戶可以在管理後臺,指定某商品的庫存扣減方式。 兩者在應用上的一些細節區別: 1.1 下單減庫存: 用戶下單時,後臺進行預扣庫存,當前用戶體驗不錯。但當前用戶若遲遲未付款,這種“弔單”就造成庫存浪費,影響商戶利益,同時也影響了其他用戶的需求購物(除非,能做好一定風控和庫存回滾,後續會有闡述,也是我更傾向的)。 1.2 付款減庫存: 用戶下單後過了幾秒,進行線上支付,結果付款成功了,卻發現庫存已經不足,導致購物失敗,嚴重影響購物體驗。同時,還要考慮扣款的回退造成更多複雜性(除非,允許一定超賣,或者庫存數量“不計” ,另外倘若是秒殺場景,則依然是無法應付)。 二、描述下非併發情況下,針對庫存預扣的(其中)一種抽象流程 2.1 用戶選擇 商品P * 數量N,並提交訂單,系統後臺核心API介面 如SubmitOrder,進行接收處理。 2.1.1 在SubmitOrder中,假定商品P的庫存足夠,將對應商品規格的庫存 -N。 2.1.2 在SubmitOrder中,倘若商品P的庫存已經不足夠,告知下單失敗及原因。 2.2 訂單付款設置有一定的時效,為M分鐘。 2.2.1 在M分鐘內,可以正常付款並流轉後續服務。 2.2.2 超過M分鐘,則當前訂單自動處理為過期(或者直接Close),並將商品P的庫存 +N,從而恢復庫存。 2.3 用戶取消訂單,類似2.2.2,直接將當前訂單狀態改為取消(或者直接Close),並將商品P的庫存 +N,從而恢復庫存。 2.4 用戶申請退款 2.4.1 未發貨,可以直接申請退款,同時將商品P的庫存 +N,從而恢復庫存(Tips:某些極少數現存項目里,存在發貨後才減庫存的設計,那麼無需補足,但這裡不對比論述,否則本流程也會相應調整,也非重點)。 2.4.2 已發貨,可以直接申請退款,但需要發回商品,等待相關處理(手動重新上架,或者補充商品P庫存)。 三、額外說明,在不考慮併發情況,庫存風險管控上的一些附屬問題 商品P若被大量下單,這些訂單中又存在相當大比例的未支付,此時商品P的庫存會被瞬間清空,必定就需要針對這塊的風控檢測。(PS:其實這不是本文闡述的主要方向,但最近剛好在其他平臺看到幾篇相關的文章中有簡單提到以下幾點,既然有一定的關聯性,本人就結合目前的想法,就儘量先拋出來,但不做過多延伸)。 3.1 商品P被同一用戶刻意反覆下單(非併發造成): 可採取在SubmitOrder之前,設置用戶限購以及關聯商品P的訂單待支付核查。同時提供備用的手動黑名單機制。 3.2 商品P被同一IP的多個用戶刷單: 這種情況,首先就有系統的分級檢測風控,譬如第一級是設置驗證碼(針對用戶),第二級是已購買檢測(類似3.1),第三級是庫存閾值報警通知(針對商家),第三級是黑名單攔截(程式攔截和手動攔截)等。其實這在“AutumnBing”項目里並未用到,而截止目前,本人身邊也只有一位朋友提到了相關實際應用,並且是相對簡單粗糙的實現,畢竟這塊程式上能做的只是輔助。 3.3 商品P被不同IP的用戶惡意下單: 比如某些競爭對手非法發起的有網路組織進行團體性惡意拍單,這種類似“DDOS”的洪流(條件類似)已經上升到了另外的高度上去了。除了結合上面的一些輔助手段,目前沒有見過或者聽過有效的處理方式。但值得一提的是,和DDOS場景本身不同的地方,如果這些用戶賬號不停下單,卻未履約,那麼會受到一些凍結處罰(配合時效),這會使得惡意攻擊的成本更高 。 四、闡述關於併發環境中庫存管控的一些案例問題,以及涉及到的相關技術實現細節 庫存扣減,簡單來說,就是在對應的存儲器中(資料庫或者持久緩存)將對應商品的數量減少。 資料庫設計時,一般包含但不限於 商品主表,商品規格表,商品庫存表,商品庫存流水日誌表等等。但這裡為了方便後續闡述,將其簡化為一張表——商品表(PT),該表僅包含兩個欄位——商品主鍵(id)和商品庫存(qty )。 依然以商品P舉例,其主鍵為pid,那麼就是在下單時,將歷史庫存S修改為 S -N。具體到SQL里,原始操作大概是這樣(以SQL SERVER 舉例): update PT set qty = (S - N) where id = pid ; 這是以前的最原始的操作方式,單粒度的看,也沒什麼大礙。然而,放在一個併發環境中,則立馬暴露出諸多問題。 假定在同一時刻,有兩個用戶提交了訂單,一樣的操作,一樣的商品,一樣的數量。那麼最終商品P的庫存數量應該為 S - N - N。而執行上面的SQL,因為併發,導致兩次查詢到歷史庫存均是S(應該至少有一次qty為S - N),則更新完畢後,商品數量最終是 S - N。這種致命性的Bug,也屬於超賣(雖然不會扣為負數),如果放線上上,簡直是一個定時炸彈。 圍繞解決這樣的問題,考慮到併發安全以及併發性能,產生了各種解決方案。大體基於兩種機制:悲觀鎖和樂觀鎖。在諸多場景里,基於每種鎖,都有配套的輔助手段,以及各自不同的側重取捨和相關實現。 考慮到本篇更主要的是做一些基礎鋪墊,也可為對於電商系統不太瞭解的朋友做一些相關引導,第一篇 ,暫告一段落。第二篇 —— 商城系統下單庫存管控系列雜記(二) ,本人將找個專門的時間寫,可能會篇幅上長很多,內容上緊接第四點,將會談及較多的重要細節,以及相關應用實現,到時再進行具體延伸,以及其他擴展討論。 End.