BEFORE UPDATE 是唯一能阻止负库存的时机,因它可在写入前校验 NEW.stock;CHECK 约束在 MySQL 5.7 及更早版本无效,复杂校验仍需触发器或应用层,二者可共存。

BEFORE UPDATE 是唯一能拦住负库存的时机
库存字段写成负数,不是“业务逻辑没写好”,而是触发器没卡在正确位置。用 AFTER UPDATE 就算加了 SIGNAL SQLSTATE '45000' 也晚了——数据已经落盘。MySQL 的 BEFORE UPDATE 才是唯一能干预写入前值的钩子。
关键判断只看 NEW.stock:
- 正确写法:
IF NEW.stock - 错误写法:
IF OLD.stock - 10 ——硬编码扣减量,订单实际扣多少根本不确定 - 别在触发器里查其他表(比如预占库存),会拖慢主事务,且 MyISAM 下无法回滚
触发器不能替代事务边界,它只是最后一道防线
单条 UPDATE products SET stock = stock - 5 WHERE id = 123 能靠触发器兜底,但涉及多表操作(如扣库存 + 记订单 + 更新销量)时,触发器完全不参与事务控制。
必须由应用层或调用方显式开启事务:
- MySQL:先执行
START TRANSACTION,再发上述 UPDATE - SQL Server:用
BEGIN TRANSACTION包裹整套操作 - 如果其中某步失败(比如订单表插入失败),整个事务回滚,触发器对
products的修改自然撤销
漏掉这一步,哪怕触发器没报错,库存和订单也可能不匹配——因为它们根本不在同一个事务里。
并发场景下,仅靠触发器不够,得加 SELECT FOR UPDATE
两个请求同时读到 stock = 10,都判断“扣 8 没问题”,然后先后执行 UPDATE ... SET stock = stock - 8,结果变成 -6。触发器只校验最终值,不解决读-改-写竞争。
真正防超卖要组合使用:
- 在事务开头加
SELECT stock FROM products WHERE id = 123 FOR UPDATE,锁住该行 - 再做扣减计算和 UPDATE,确保串行执行
-
BEFORE UPDATE触发器保留,作为防止人为绕过应用层的兜底(比如直接 SQL 修改)
注意:FOR UPDATE 只在 InnoDB 有效,MyISAM 不支持行锁,这种场景必须换引擎。
CHECK 约束比触发器更轻量,但别指望它覆盖所有情况
MySQL 8.0.16+ 支持 stock INT CHECK (stock >= 0),性能更好、语法更简,但它有硬限制:
- MySQL 5.7 及更早版本忽略 CHECK,毫无作用
-
LOAD DATA INFILE ... IGNORE可能跳过约束校验 - 没法关联查另一张表(比如“扣减量不能超过预占库存”),复杂逻辑仍需触发器或应用层
生产环境建议:新项目优先建 CHECK,老版本或需跨表校验时,才补 BEFORE UPDATE 触发器。两者不互斥,可共存。

















