正确做法是用主键加锁的UPDATE语句完成原子扣减:UPDATE inventory SET stock = stock - 1 WHERE inventory_id = 123 AND stock >= 1;触发器仅能做事后校验(如检查负库存并报错)、日志记录或更新只读统计字段,不可用于锁行或补救超卖。

为什么不能用 UPDATE ... SET stock = stock - 1 WHERE stock >= 1?
这条语句看似能防超卖,但高并发下会失效。多个事务同时读到 stock=1,都判断通过,接着都执行减法,最终 stock 变成 -1。根本原因是 WHERE 条件检查和 UPDATE 执行不是原子的,中间存在竞态窗口。
真正可靠的锁必须在数据行上加写锁(X lock),且锁要覆盖整个判断+修改过程。InnoDB 的行级锁只在满足索引条件时才生效,所以 WHERE 必须走主键或唯一索引——这意味着你不能靠商品 ID 加锁,而要靠「库存记录的主键」,比如每条库存单独一行(inventory_id),而不是一个全局 stock 字段。
触发器里不能做 SELECT FOR UPDATE,那怎么锁住库存行?
MySQL 触发器内禁止显式使用 SELECT ... FOR UPDATE 或 UPDATE 操作其他表(甚至同表),会报错 Can't update table 'inventory' in stored function/trigger。所以不能在触发器里“先查再锁再改”。
正确做法是:把库存扣减逻辑前置到业务 SQL 中,用一条带锁的 UPDATE 完成判断与扣减,再让触发器只做副作用(如记录日志、更新统计字段):
UPDATE inventory SET stock = stock - 1, updated_at = NOW() WHERE inventory_id = 123 AND stock >= 1;
这条语句在 inventory_id 是主键的前提下,会自动对对应行加 X 锁,且 WHERE 中的 stock >= 1 是在加锁后求值的,天然原子。执行后检查 ROW_COUNT() 是否为 1,为 0 就说明已售罄。
触发器唯一能安全做的库存防护动作是什么?
只能做「事后校验与阻断」,不能替代行锁。例如在订单插入前(BEFORE INSERT 触发器)中,尝试读取当前库存值,但注意:这个读是快照读(RR 隔离级别下不加锁),无法防止并发写。所以它只能用于辅助判断,比如:
- 检查
stock是否为负数(说明上游逻辑出 bug),抛出异常:SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Stock went negative'; - 记录本次下单的
order_id和inventory_id到审计表,便于对账 - 更新
sales_count等只读统计字段(不参与核心扣减逻辑)
任何试图在触发器里“补救”超卖的操作(比如回滚、重试、调用存储过程锁表)都会引入死锁或不可预测行为,得不偿失。
真正的防超卖必须靠应用层 + 数据库行锁协同
触发器不是锁机制的实现者,它只是链条末端的守门人。完整链路是:
- 业务代码生成订单前,执行带主键和库存条件的
UPDATE inventory SET stock = stock - 1 WHERE inventory_id = ? AND stock >= ? - 用
ROW_COUNT()判断是否成功扣减,失败则返回“库存不足” - 只有扣减成功后,才允许插入订单记录(可配合
INSERT ... SELECT或事务包裹) - 触发器仅用于日志、通知、缓存失效等非关键路径操作
漏掉主键条件、用错隔离级别(比如 RC 下 gap lock 行为不同)、或者库存表没建好主键索引——这些细节比触发器写得多重要得多。










