扣庫存

牽涉到三個核心風險:
- 超賣(Overselling)
- 鎖競爭(Lock Contention)
- 效能瓶頸(Performance Bottleneck)
庫存扣減要即時又要安全,但越嚴格的隔離會越慢,要怎麼平衡?
在扣庫存的交易中,最怕的不是「讀到舊資料」,而是「兩個人同時看到同一個庫存,然後都扣成功」。這其實不是 read 的問題,而是 concurrent update 的問題
關鍵是交易期間必須鎖定庫存那一筆資料,確保扣減是序列化的(serial)
通常在電商系統,我們會選擇交易層級用 READ COMMITTED 或 REPEATABLE READ,配合「明確加鎖查詢」
| 模式 | 隔離級別 | 特性 | 是否會鎖住那筆庫存 | 備註 |
|---|---|---|---|---|
(1) READ COMMITTED + UPDLOCK |
預設 | 防髒讀 | ✅ 是 | 最常用、安全 |
| (2) SERIALIZABLE | 最嚴格 | 防髒讀 + 幻讀 | ✅ 是 (範圍鎖) | 太重,僅用於財務對帳 |
| (3) SNAPSHOT / RCSI | 基於版本 | 不會 block,但需要行版本控制 | ❌ 否 | 不適合庫存扣減(可能超賣) |
範例一:扣庫存交易(單筆商品)
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN TRANSACTION;
– 1️⃣ 讀取並鎖定這筆庫存資料
SELECT stock
FROM inventory WITH (UPDLOCK, ROWLOCK, HOLDLOCK)
WHERE product_id = 1001;
– 2️⃣ 檢查庫存足夠再扣減
UPDATE inventory
SET stock = stock - 1
WHERE product_id = 1001;
COMMIT TRANSACTION;
- UPDLOCK:取得更新鎖,避免其他人也拿到讀鎖後再轉更新鎖(防止死鎖)
- ROWLOCK:明確要求行鎖(避免升級成頁鎖或表鎖)
- HOLDLOCK:等同於 SERIALIZABLE 在這筆資料的效果(範圍鎖),直到交易結束才釋放
範例二:檢查庫存不足時回滾
BEGIN TRANSACTION;
DECLARE @stock INT;
SELECT @stock = stock
FROM inventory WITH (UPDLOCK, ROWLOCK, HOLDLOCK)
WHERE product_id = 1001;
IF @stock < 1
BEGIN
ROLLBACK TRANSACTION;
PRINT ‘Insufficient stock’;
RETURN;
END
UPDATE inventory
SET stock = stock - 1
WHERE product_id = 1001;
COMMIT TRANSACTION;
多人同時下單 → 只有第一個交易能鎖住那筆資料
其他交易必須等該交易 COMMIT 或 ROLLBACK
不會發生超賣


