在電商、秒殺等高并發場景中,“超賣”問題指庫存被過量扣減,導致實際庫存不足。以下是使用 分布式鎖 和 樂觀鎖 解決超賣問題的原理與實現方案:
一、超賣問題的核心原因
多個并發請求同時讀取庫存余量,并在本地計算后發起寫操作,導致實際扣減后的庫存為負數。
二、解決方案 1:分布式鎖
核心思想
通過 強制串行化 扣減庫存操作,同一時間僅一個請求能處理庫存。
實現步驟(以 Redis 為例):
-
獲取鎖
使用SET key uuid NX EX timeout
命令,確保原子性加鎖。import redis r = redis.Redis()def acquire_lock(product_id, uuid, expire=10):key = f"lock:{product_id}"return r.set(key, uuid, nx=True, ex=expire)
-
扣減庫存
在鎖的保護下執行庫存操作:UPDATE inventory SET stock = stock - 1 WHERE product_id = 1 AND stock > 0;
-
釋放鎖
使用 Lua 腳本保證原子性釋放(避免誤刪其他請求的鎖):if redis.call("get", KEYS[1]) == ARGV[1] thenreturn redis.call("del", KEYS[1]) elsereturn 0 end
優缺點
- 優點:強一致性,邏輯簡單。
- 缺點:性能瓶頸(串行化)、鎖失效風險(需合理設置超時時間)。
三、解決方案 2:樂觀鎖
核心思想
基于 版本號 或 條件判斷,在更新時校驗數據未被修改,若沖突則重試或失敗。
實現步驟(以 MySQL 為例):
-
查詢庫存與版本號
SELECT stock, version FROM inventory WHERE product_id = 1;
-
更新庫存(帶條件)
通過版本號或庫存量確保原子性:-- 版本號方式 UPDATE inventory SET stock = stock - 1, version = version + 1 WHERE product_id = 1 AND version = {old_version} AND stock > 0;-- 條件判斷方式(直接校驗庫存) UPDATE inventory SET stock = stock - 1 WHERE product_id = 1 AND stock = {queried_stock} AND stock > 0;
-
檢查更新結果
若影響行數為 0,說明沖突,需重試或返回錯誤。
代碼示例(偽代碼):
def deduct_stock():retries = 3for _ in range(retries):# 查詢庫存和版本stock, version = db.query("SELECT stock, version FROM inventory WHERE product_id=1")if stock <= 0:return "庫存不足"# 嘗試更新rows = db.execute("UPDATE inventory SET stock=stock-1, version=version+1 ""WHERE product_id=1 AND version=%s AND stock>0",version)if rows > 0:return "成功"return "請重試"
優缺點
- 優點:高并發性能好,無鎖競爭。
- 缺點:需處理重試邏輯,沖突頻繁時性能下降。
四、方案對比與選型
方案 | 適用場景 | 性能 | 復雜度 | 一致性 |
---|---|---|---|---|
分布式鎖 | 強一致性、低并發沖突 | 低 | 中 | 強一致性 |
樂觀鎖 | 高并發、沖突較少 | 高 | 低 | 最終一致性 |
五、增強方案
-
結合緩存優化
- 使用 Redis 預扣庫存,異步同步到數據庫。
- 例如:先扣減 Redis 中的庫存,再通過消息隊列更新數據庫。
-
庫存分段
- 將庫存拆分為多個段(如 100 個庫存拆為 10 段),每段獨立加鎖,提升并發度。
-
限流與降級
- 使用令牌桶或漏桶算法限制請求流量,防止系統過載。
六、注意事項
- 分布式鎖的可靠性
- 推薦 Redlock 算法(多節點 Redis)或 ZooKeeper 實現高可用鎖。
- 樂觀鎖的重試策略
- 限制最大重試次數,避免無限循環。
- 事務隔離級別
- 確保數據庫隔離級別為 Read Committed 或以上,避免臟讀。
通過合理選擇鎖機制并結合業務場景優化,可有效解決超賣問題。