触发器不能使用 select ... for update 或事务控制语句,因其会报错 error 1442;仅允许基于 new/old 直接计算(如 new.quantity),禁止查询触发行所在表,且不可调用存储过程、远程表或非确定性函数。

触发器里不能用 SELECT ... FOR UPDATE 或事务控制语句
MySQL 的 BEFORE INSERT 或 AFTER UPDATE 触发器中,不允许显式开启事务、执行 COMMIT、ROLLBACK,也不能对当前正在被触发的表做 SELECT ... FOR UPDATE —— 这会直接报错 ERROR 1442 (HY000): Can't update table 'inventory' in stored function/trigger because it is already used by statement which invoked this stored function/trigger.
- 库存余额必须基于“本次操作的行数据”直接计算,比如:
NEW.quantity和OLD.quantity,而不是查原表再加减 - 如果业务需要校验可用库存(如出库前判断是否够用),得放在应用层或存储过程中做,触发器只负责“确定性更新”
- 对
inventory表本身做UPDATE是允许的,但只能更新其他行(非触发行),且需确保不形成递归调用
入库用 AFTER INSERT,出库用 BEFORE UPDATE 配合状态字段
典型订单表 orders 包含 status('pending'/'shipped'/'canceled')、item_id、quantity。库存同步不能等所有状态流转完才更新,否则中间状态会导致余额不一致。
- 入库(采购单/调拨单完成):在
orders表的AFTER INSERT触发器里,检查NEW.type = 'purchase'且NEW.status = 'completed',然后UPDATE inventory SET balance = balance + NEW.quantity WHERE item_id = NEW.item_id - 出库(销售单发货):用
BEFORE UPDATE,当OLD.status = 'pending'且NEW.status = 'shipped'时,先查inventory确认balance >= NEW.quantity;如果不满足,用SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Insufficient inventory'中断更新 - 注意:出库校验必须在
BEFORE UPDATE做,否则AFTER里无法阻止订单状态变更
balance 字段必须设为 NOT NULL DEFAULT 0,且建唯一索引
触发器不处理并发冲突,全靠数据库约束兜底。如果两个出库请求同时读到同一 balance 值并各自扣减,就会超卖。
-
balance列必须声明为NOT NULL DEFAULT 0,避免 NULL 参与运算导致结果为 NULL - 在
inventory(item_id)上建唯一索引(或主键),确保每种商品只有一条记录,防止重复插入导致多行余额 - 真正防超卖要靠
UPDATE ... SET balance = balance - ? WHERE item_id = ? AND balance >= ?的影响行数判断——这个逻辑不能塞进触发器,得由应用执行并检查ROW_COUNT()
不要在触发器里调用存储过程或访问远程表
触发器生命周期绑定于单条 DML 语句,任何外部依赖都会放大不确定性。例如调用一个含 SLEEP(0.1) 的存储过程,会让所有订单插入变慢;访问另一个数据库的表可能因网络抖动失败,导致整个事务回滚。
- 所有计算必须是纯逻辑:加减乘除、
CASE WHEN、IF(),不涉及 I/O 或函数副作用 - 避免使用
NOW()、UUID()、RAND()等非确定性函数,MySQL 8.0+ 对此有严格限制 - 日志类需求(如记录每次余额变更)应写入另一张表,但必须用
INSERT INTO audit_log (...) VALUES (...),不能用SELECT ... INTO OUTFILE或调用系统命令
触发器只是库存最终一致性的辅助手段,真正的强一致性得靠应用层的乐观锁或分布式事务协调。很多人以为加了触发器就高枕无忧,其实它连最基本的并发扣减都扛不住。










