mysql触发器中禁止使用start transaction、commit、rollback;异常需用signal中断执行以触发外部事务回滚,否则如库存负值等逻辑错误将被忽略。

触发器里不能用事务控制语句
MySQL触发器运行在父语句的事务上下文中,BEGIN...END块内不允许出现START TRANSACTION、COMMIT或ROLLBACK。一旦触发器内部抛出异常(比如SIGNAL SQLSTATE '45000'),整个外部语句会回滚——这是唯一可控的“回滚”方式。
常见错误现象:ERROR 1305 (42000): SAVEPOINT does not exist 或触发器执行后库存错乱但没报错。
- 扣减库存时发现余额为负,却没阻止插入/更新:说明没用
SIGNAL中断执行,只写了IF stock 这类“静默修复” - 在触发器里调用存储过程,而该过程含
COMMIT:直接报错ERROR 1307 (HY000): Function or expression '<code>xxx' cannot be used in a trigger - 想“先查再改”做校验?注意:
SELECT ... FOR UPDATE在触发器中不生效,行锁由外部语句持有
BEFORE INSERT/UPDATE 才能安全拦截库存不足
库存扣减必须放在BEFORE触发器里,且仅限INSERT(下单)和UPDATE(改订单状态如“已支付”)场景。用AFTER触发器补减库存等于事后救火——订单已提交,用户看到“下单成功”,但库存实际不够。
典型使用场景:用户提交订单时插入orders表,触发器检查对应商品在products表中的stock是否足够。
-
BEFORE INSERT ON orders:读取NEW.product_id,查products.stock,不足则SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Insufficient stock' -
BEFORE UPDATE ON orders:仅当OLD.status != 'paid' AND NEW.status = 'paid'时才扣减,避免重复扣除 - 别在
UPDATE products上建触发器来响应订单——业务逻辑逆向,耦合重,且无法感知订单数量
UPDATE 触发器要严格区分新旧值
电商场景中,订单可能多次修改(如地址、备注),但库存只应在支付确认那一刻扣减。如果触发器无条件响应所有UPDATE,容易导致库存被多扣或漏扣。
关键在判断字段变化:必须对比OLD.column和NEW.column,而不是只看NEW.column值。
- 错误写法:
IF NEW.status = 'paid' THEN ...—— 只要新值是'paid'就扣,不管之前是不是 - 正确写法:
IF OLD.status != 'paid' AND NEW.status = 'paid' THEN ... - 涉及数量变更(如用户修改订单商品件数):需计算差值
NEW.quantity - OLD.quantity,再增减对应库存,而非直接设为NEW.quantity - 注意NULL比较:用
IS NULL/IS NOT NULL,别用= NULL
触发器性能隐患比想象中更早出现
单条订单插入触发一次查询+一次更新,看似无害。但高并发下单时,所有触发器操作都串行争抢同一行的products记录锁——这不是应用层能绕开的瓶颈。
真实压测中,QPS超过300后,orders表插入延迟陡增,错误率上升,根源常在触发器里的SELECT ... FROM products WHERE id = NEW.product_id这句。
- 加索引没用:主键查询本身很快,瓶颈在行锁等待
- 替代方案不是“优化触发器”,而是放弃用触发器做核心库存扣减——改用应用层+Redis预减+MySQL最终一致性校验
- 如果坚持用触发器,至少把
products.stock字段单独拆到一张轻量表(如product_stock),减少锁表范围 - 测试时别只看功能对不对,一定要跑
sysbench或mysqlslap模拟并发INSERT,观察Innodb_row_lock_waits指标
触发器看着省事,但它把库存校验绑死在MySQL单点上,连连接池、分库分表都难介入。真正上线前,得先问自己:这个“自动”是不是正在悄悄拖垮整个下单链路。











