应根据业务动作类型选择:新增订单用before insert,状态变更(如pending→confirmed)用after update,退货入库用before insert;严禁在触发器中update被触发的同一张表。

触发器里写 UPDATE 还是 INSERT?先看业务动作类型
库存变动通常来自订单创建(减库存)或退货入库(加库存),对应的是 INSERT 或 UPDATE 操作。别一上来就写 BEFORE UPDATE —— 如果你只处理新订单插入,那该用 BEFORE INSERT;如果要响应发货状态变更(比如订单表 status 从 ‘pending’ 改为 ‘shipped’),才考虑 BEFORE UPDATE。关键不是语法多酷,而是触发时机是否匹配真实业务流。
常见错误:在订单表上建 BEFORE UPDATE 触发器,但更新字段是 updated_at,结果每次时间戳刷新都去查库存,白白触发校验、锁行、甚至报错中断。
- 新增订单 → 用
BEFORE INSERT在订单插入前校验并扣减库存 - 订单取消 → 用
BEFORE UPDATE,但限定只响应status = 'cancelled'的更新 - 入库单确认 → 用
BEFORE INSERT到warehouse_in表,而非在商品表上监听
库存不足时怎么阻止操作?别只靠 RAISE ERROR
MySQL 5.7+ 支持 SIGNAL 抛出异常,这是最直接的拦截方式。但要注意:它会让整个事务回滚,且客户端必须能正确捕获 SQLSTATE 错误码。如果应用层没做异常处理,可能只看到“Unknown error”,排查困难。
示例中常漏掉 SQLSTATE 值,导致错误分类混乱:SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Insufficient stock for product_id=123'; —— 这个 '45000' 是自定义错误通用码,比直接用 'HY000' 更利于区分业务逻辑错误。
- 不要用
SELECT ... INTO后再IF判断然后INSERT,这有竞态风险 - 避免在触发器里调用存储过程做校验,增加不可控延迟和嵌套深度
- 若需记录拒绝原因,建议在触发器里写入日志表(如
stock_reject_log),而不是依赖应用层补日志
并发场景下库存扣减为什么还会超卖?
触发器本身不解决并发问题。即使你在 BEFORE INSERT 里查了当前库存 > 所需数量,两个并发事务仍可能同时通过校验,然后一起扣减,最终库存变负。根本解法是加行锁,而最稳妥的方式是在触发器内执行带 FOR UPDATE 的 SELECT。
例如,在触发器开头写:SELECT stock FROM products WHERE id = NEW.product_id FOR UPDATE;。这会锁定该行,后续同 ID 的并发请求会被阻塞,直到前一个事务提交或回滚。注意:必须确保这个 SELECT 和后面的 UPDATE 在同一事务上下文(即触发器属于主事务),否则锁会立刻释放。
-
FOR UPDATE必须搭配事务使用,单独执行无效 - 别在触发器里用
START TRANSACTION—— MySQL 不允许在触发器中显式启事务 - 如果库存表用了 MyISAM 引擎,
FOR UPDATE直接报错,必须切到 InnoDB
触发器修改其他表时要注意什么?
触发器里更新 products 表本身没问题,但若还要同步更新 product_stats 或调用 UPDATE inventory_summary...,就得小心“递归触发”和“最大嵌套层级”限制(默认 25)。MySQL 会把触发器内的 UPDATE 当作新事件,可能再次激活同一触发器,造成死循环。
典型陷阱:在 orders 表的触发器里扣减 products.stock,而 products 表又有一个 AFTER UPDATE 触发器去更新统计表 —— 这时只要 products 被改,就会触发第二层,容易超限报错 ERROR 1420 (HY000): Recursive stored function or trigger call limit exceeded。
- 用
DISABLE TRIGGER不现实(MySQL 不支持运行时禁用) - 更可靠的做法是:在触发器里用
INSERT INTO ... SELECT写日志,或用应用层异步补偿,而非在触发器里层层联动 - 如果真要跨表更新,确保目标表没有对同一字段的触发器,或用标记字段(如
skip_trigger = 1)绕过











