你剛結(jié)束一輪促銷,客服后臺(tái)涌來大量“已支付卻通知缺貨”的投訴——商品超賣了。庫存系統(tǒng)沒能攔住并發(fā)請(qǐng)求,導(dǎo)致實(shí)際發(fā)貨量遠(yuǎn)超可售數(shù)量。于是物流癱瘓、用戶憤怒、平臺(tái)賠付。
這背后不是一個(gè)簡單的減庫存錯(cuò)誤,而是庫存的扣減時(shí)機(jī)、并發(fā)控制、狀態(tài)流轉(zhuǎn)與多倉調(diào)配四個(gè)設(shè)計(jì)決策交織出的復(fù)雜問題。任何一處考慮不周,最終都會(huì)表現(xiàn)為超賣或負(fù)面體驗(yàn)。
決策一:在哪個(gè)業(yè)務(wù)動(dòng)作上扣庫存
庫存扣減時(shí)機(jī)直接決定了售賣體驗(yàn)與風(fēng)險(xiǎn)敞口。常見三種模式:
- 下單即扣減:用戶提交訂單時(shí)立即扣減可售庫存。能有效阻止超賣,但惡意下單會(huì)導(dǎo)致庫存占用,需配合未支付自動(dòng)取消與限購策略。
- 付款后扣減:支付回調(diào)再扣庫存。不會(huì)因占單浪費(fèi)庫存,但并發(fā)支付可能造成超賣——尤其在高流量下,多個(gè)支付請(qǐng)求同時(shí)通過庫存檢查。
- 發(fā)貨后扣減:實(shí)際出庫時(shí)才扣減。多見于貨到付款或虛擬商品,風(fēng)險(xiǎn)最大,通常需要額外保留緩沖庫存。
你必須根據(jù)業(yè)務(wù)模型選擇一種,但絕大部分電商會(huì)采用“下單預(yù)占、支付確認(rèn)”的組合模式:創(chuàng)建一個(gè)訂單時(shí)凍結(jié)庫存(預(yù)占),支付成功則轉(zhuǎn)為實(shí)際銷售,未支付取消后釋放凍結(jié)。這就引出了庫存狀態(tài)的分層設(shè)計(jì)。
決策二:庫存狀態(tài)機(jī)與表結(jié)構(gòu)設(shè)計(jì)
庫存不應(yīng)只有一個(gè)“數(shù)量”字段。你需要拆分出幾個(gè)核心狀態(tài)量,構(gòu)成庫存狀態(tài)機(jī):
- 可售庫存(available):前端展示“有貨”的依據(jù),等同于
總庫存 - 已凍結(jié) - 已售出。 - 凍結(jié)庫存(locked):由于待支付訂單暫時(shí)鎖定的數(shù)量,不允許其他訂單使用。
- 已售庫存(sold):支付完成或發(fā)貨完成后的累計(jì)售出量。
對(duì)應(yīng)的物理表至少包含以下兩表(最小實(shí)現(xiàn)):
-- 庫存總表(分 warehouse_id + sku_id 唯一)
CREATE TABLE inventory (
id BIGINT PRIMARY KEY,
sku_id BIGINT NOT NULL,
warehouse_id INT NOT NULL,
total_stock INT NOT NULL DEFAULT 0,
locked_stock INT NOT NULL DEFAULT 0,
sold_stock INT NOT NULL DEFAULT 0,
version INT NOT NULL DEFAULT 0, -- 樂觀鎖版本號(hào)
UNIQUE KEY (warehouse_id, sku_id)
);
-- 庫存流水表,用于對(duì)賬與追溯
CREATE TABLE inventory_flow (
id BIGINT PRIMARY KEY,
sku_id BIGINT NOT NULL,
warehouse_id INT NOT NULL,
order_id BIGINT NOT NULL,
change_type TINYINT NOT NULL, -- 1:凍結(jié) 2:解凍 3:已售扣減 4:補(bǔ)回
change_qty INT NOT NULL,
before_available INT NOT NULL,
after_available INT NOT NULL,
created_at DATETIME NOT NULL
);
狀態(tài)流轉(zhuǎn)必須嚴(yán)格按預(yù)定路徑進(jìn)行:可用 → 凍結(jié)(下單) → 已售(支付),取消則從凍結(jié)退回可用。不允許從已售回到凍結(jié),退貨退款屬于逆向流程,應(yīng)單獨(dú)記錄補(bǔ)償庫存。
決策三:并發(fā)下的防超賣實(shí)現(xiàn)
當(dāng)多個(gè)請(qǐng)求同時(shí)讀取到相同的 available 并進(jìn)行扣減時(shí),單純的 UPDATE SET available = available - ? 會(huì)導(dǎo)致臟寫。你需要引入并發(fā)控制:
數(shù)據(jù)庫層面
使用行鎖或條件更新。對(duì)于下單凍結(jié)操作,推薦的原子 SQL 是:
UPDATE inventory
SET locked_stock = locked_stock + ?,
version = version + 1
WHERE sku_id = ?
AND warehouse_id = ?
AND version = ?
AND (total_stock - locked_stock - sold_stock) >= ?;
通過校驗(yàn)可售庫存是否充足,以及樂觀鎖版本號(hào),能防止超賣。若影響行數(shù)為 0,則表示庫存不足或版本沖突,業(yè)務(wù)層回滾訂單創(chuàng)建。
Redis 緩存層
熱點(diǎn) SKU 的數(shù)據(jù)庫頻繁更新會(huì)成為瓶頸,通常將可售庫存緩存至 Redis。采用 Lua 腳本保證原子性扣減:
-- 凍結(jié)庫存:扣減可售并增加凍結(jié)
local available_key = KEYS[1] -- inventory:sku:123:available
local locked_key = KEYS[2] -- inventory:sku:123:locked
local deduck = tonumber(ARGV[1])
local current = tonumber(redis.call('GET', available_key) or 0)
if current >= deduck then
redis.call('DECRBY', available_key, deduck)
redis.call('INCRBY', locked_key, deduck)
return 1
else
return 0
end
庫存預(yù)熱時(shí)從數(shù)據(jù)庫加載真實(shí)可售量到 Redis。下單時(shí)優(yōu)先用 Lua 腳本操作 Redis,成功后再異步同步數(shù)據(jù)庫凍結(jié),并記錄流水。支付確認(rèn)時(shí)則需同步操作數(shù)據(jù)庫轉(zhuǎn)已售并更新 Redis 的凍結(jié)值。為避免緩存與數(shù)據(jù)庫不一致,你需要設(shè)計(jì)一個(gè)定時(shí)對(duì)賬任務(wù),掃描 inventory_flow 與當(dāng)前庫存快照進(jìn)行修復(fù)。
邊界情況與風(fēng)險(xiǎn)
- 訂單超時(shí)取消的庫存釋放:必須通過延遲任務(wù)或消息隊(duì)列可靠觸發(fā)。解凍操作同樣需要原子 SQL 或 Lua 腳本,防止重復(fù)釋放導(dǎo)致庫存虛增。
- 支付成功但數(shù)據(jù)庫已售扣減失敗:該場景下用戶已付錢但庫存未扣除,可能導(dǎo)致超賣。需要在該事務(wù)中重試,若最終失敗則記錄異常訂單并人工介入,或利用流水進(jìn)行補(bǔ)償。
- 多倉庫情形:前端顯示庫存時(shí)通常匯總所有分倉的可售量,但下單時(shí)需指定發(fā)貨倉。如果指定倉缺貨,可設(shè)計(jì)路由規(guī)則(缺貨時(shí)嘗試從其他倉發(fā)貨),但會(huì)增加拆單復(fù)雜度。最簡單的做法是:用戶選擇地區(qū)后,按優(yōu)先級(jí)選擇一個(gè)有貨倉進(jìn)行預(yù)占,若全無貨則展示無貨。
- 部分退款/退貨的庫存回補(bǔ):回到哪個(gè)狀態(tài)?應(yīng)新建一條補(bǔ)貨流水,直接增加
total_stock并同步增加available,不可回退sold_stock,以保證銷售統(tǒng)計(jì)不變。
行動(dòng)建議
- 確定扣減時(shí)機(jī):從業(yè)務(wù)指標(biāo)(超賣容忍度、轉(zhuǎn)化率、惡意下單風(fēng)險(xiǎn))出發(fā)選型,不要只追逐技術(shù)潮流。絕大多數(shù) B2C 商城使用“下單預(yù)占、支付確認(rèn)”。
- 設(shè)計(jì)庫存狀態(tài)機(jī)并落表:用
total、locked、sold三個(gè)字段表達(dá)狀態(tài),配合流水表讓每一筆變動(dòng)可追溯。 - 防超賣雙保險(xiǎn):數(shù)據(jù)庫層用條件 UPDATE + 樂觀鎖,熱點(diǎn)商品增加 Redis 原子扣減緩存層,并一定要有定時(shí)對(duì)賬任務(wù)做最后防線。
- 先走通主流程,再補(bǔ)異常:實(shí)現(xiàn)時(shí)優(yōu)先保證正常下單→支付→發(fā)貨的庫存流轉(zhuǎn)無 bug,然后再補(bǔ)充取消、超時(shí)、退款等逆向補(bǔ)償,避免憑空設(shè)計(jì)導(dǎo)致流程不可用。
不要迷信任何單一的解決方案——庫存系統(tǒng)最終考驗(yàn)的是你對(duì)一致性、可用性和業(yè)務(wù)容忍度之間的權(quán)衡。