商城系統下單庫存管控系列雜記(一)(並發安全和性能基礎認識)
前言
參與過幾個中小型商城系統的開發,隨着時間的增長,以及對系統的深入研究和測試,發現確實有很多值得推敲和商榷的地方(總有很多重要細節存在缺陷)。基於商城系統,無論規模大小,或者本身是否分布架構,個人覺得最核心的一環就是下單模塊,而這里面更相關和棘手的一些設計和問題,大多時候都涉及庫存系統。想想之前跟某人的交流,他一句“庫存管控做得好,系統設計就成功了一半”,自己頗有認同。圍繞這個點,結合目前經驗和朋友間的交流(包括近來參閱其他文章提到的點),閑來做些整理記錄,也許不太完整,但總歸希望能有更多啟發,自己往后也會重新揣摩。當然,文中若有不妥,歡迎指正。
正文
談及”下單“,就立刻想起前年參與的一個基於微信的小型商城系統,里面下單這塊本身談不上復雜,大概可以這樣描述提交過程:用戶提交商品訂單,系統核對用戶提交的訂單,校驗商品(商品價格、優惠折扣、積分等),檢測附屬信息(地址運費等),一切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.