不能直接用触发器做库存动态计算,因其破坏事务一致性且并发下必然超卖;正确做法是用带UPDLOCK+HOLDLOCK的存储过程原子化校验与更新,并配独立日志表记录变更。

不能直接用触发器做库存动态计算——它会破坏事务一致性,且在并发场景下必然出错。
为什么 INSTEAD OF 或 AFTER 触发器不适合库存扣减
库存本质是聚合状态,必须与业务操作(如订单创建、出库单审核)严格绑定在同一个事务中。触发器无法感知上层业务意图,也无法控制执行顺序:
-
INSERT到OrderDetail表时,触发器若去改ProductStock,但此时订单主表OrderHeader可能还未提交,事务回滚后库存已错 - 两个并发请求同时扣同一商品,触发器读取的
CurrentStock是旧值,导致超卖 - 如果业务逻辑还涉及赠品、拆分单、部分发货,触发器无法适配这些分支判断
正确做法:用存储过程封装「校验 + 更新」原子操作
把库存变更收口到带事务控制的存储过程中,强制业务调用它而非直写明细表:
- 先
SELECT CurrentStock WITH (UPDLOCK, HOLDLOCK)锁住目标行(防止并发读旧值) - 检查是否足够:
IF @CurrentStock < @RequiredQty RAISERROR(...) - 再
UPDATE ProductStock SET CurrentStock = CurrentStock - @RequiredQty - 最后插入业务单据(如
OrderDetail),全部在同一个BEGIN TRAN内
示例关键片段:
CREATE PROCEDURE usp_StockDeduct
@ProductID INT,
@Qty INT
AS
BEGIN
DECLARE @Stock INT;
SELECT @Stock = CurrentStock
FROM ProductStock WITH (UPDLOCK, HOLDLOCK)
WHERE ProductID = @ProductID;
<pre class='brush:php;toolbar:false;'>IF @Stock < @Qty
RAISERROR('库存不足', 16, 1);
UPDATE ProductStock
SET CurrentStock = CurrentStock - @Qty
WHERE ProductID = @ProductID;END
哪些地方绝对不能省略锁提示
只用 SELECT ... UPDATE 两步不加锁,就是并发漏洞的根源:
-
WITH (NOLOCK)→ 读到脏数据或幻读,直接导致超卖 - 不加任何提示 → 默认
READ COMMITTED,两次查询间可能被其他事务修改 - 必须用
UPDLOCK(避免升级为表锁)+HOLDLOCK(等价于SERIALIZABLE,防止新行插入干扰范围查询) - 如果按仓库+商品联合查库存,锁粒度要落在
(WarehouseID, ProductID)索引键上,确保索引覆盖
历史库存追溯必须靠独立日志表,不是触发器
真正需要“动态记录每次变动”的场景,应建专用日志表(如 StockChangeLog),由业务存储过程在更新完主表后显式插入:
- 字段至少含:
ProductID,ChangeQty,ReasonCode(如 'ORDER'/'ADJUST'/'RETURN'),RefID(关联单据号) - 不要在触发器里写日志——它可能因主操作失败而未执行,造成日志缺失
- 日志表可加
INSERT触发器做轻量校验(如ReasonCode枚举检查),但绝不参与库存计算
最常被忽略的一点:库存字段本身必须设为 NOT NULL 且有默认值,否则 UPDATE SET Stock = Stock - @Qty 遇到 NULL 会整个变 NULL,后续所有判断都失效。

















