触发器应仅用于单表字段简单计算,避免跨表操作、复杂SQL、存储过程调用及复制不一致风险;复杂逻辑须移至应用层或异步队列。

触发器里执行复杂 SQL 会锁表甚至死锁
MySQL 触发器在 AFTER INSERT/UPDATE/DELETE 中调用多表 JOIN、子查询或写入其他大表,极易引发锁等待升级。尤其是对主键或索引字段频繁更新的场景,UPDATE t1 SET x=(SELECT y FROM t2 WHERE t2.id=t1.ref_id) 这类语句在触发器中执行,会让 t2 的相关行被加 SHARE MODE 或更重的锁,阻塞其他事务。
- 触发器内所有 SQL 都运行在原事务上下文中,无法单独提交或回滚
-
BEFORE触发器中修改NEW字段是安全的;但一旦涉及SELECT ... FOR UPDATE或跨表写操作,风险陡增 - MySQL 8.0+ 对触发器嵌套深度限制默认为
max_sp_recursion_depth=0(禁用),但手动开启后,复杂逻辑极易触发ER_STACK_OVERRUN
触发器调用存储过程时参数传递不透明
如果把复杂逻辑封装进存储过程再从触发器调用,看似解耦,实则掩盖了隐式依赖。例如:CALL sync_user_profile(NEW.user_id) 内部若包含 INSERT INTO log_table ...,而 log_table 恰好也有触发器,就可能形成隐式递归链——MySQL 不报错,但会 silently 截断(取决于 max_sp_recursion_depth 设置)。
- 触发器调用的存储过程无法捕获
SQLEXCEPTION并优雅降级;错误直接中断当前事务 - 传入
NEW或OLD字段时,若字段为NULL或类型不匹配(如DECIMAL传给INT参数),会静默转为 0 或截断,难定位 - 存储过程内用
SELECT ... INTO赋值时,若查询无结果,变量保持旧值——在触发器高频写入场景下,这会导致脏数据累积
复制环境下触发器行为不一致
基于语句的复制(binlog_format=STATEMENT)下,触发器在从库不会重放;而基于行的复制(ROW)下,触发器默认不执行(slave_skip_errors=OFF 且 replicate_events_marked_for_skip=FILTERED)。这意味着主库改了 3 张表,从库只同步了原始 DML 对应的那张表,其余由触发器写的表数据缺失。
- MySQL 5.7+ 默认
binlog_format=ROW,但触发器仍需显式设置log_bin_trust_function_creators=1才能创建,否则报ERROR 1418 - 使用
MIXED模式也无法保证触发器逻辑被正确传播——MySQL 只对“确定性函数”做语句级记录,触发器一律视为不确定 - GTID 复制中,若触发器内有非事务引擎(如 MyISAM 表操作),会导致
GTID_NEXT不一致,整个复制中断
替代方案比硬塞触发器更可控
真正需要强一致性时,应该把逻辑提到应用层或用异步队列兜底,而不是依赖触发器。比如订单状态变更后要更新库存和积分,与其在 orders 表上写触发器,不如让应用发一条 UPDATE inventory SET stock=stock-1 WHERE sku_id=? + INSERT INTO points_log ... 到同一个事务里;若怕应用崩,就用可靠消息(如 Kafka + 本地事务表)确保最终一致。
- 审计类需求(如记录谁何时改了哪条)可用
GENERATED COLUMN+DEFAULT CURRENT_USER+DEFAULT CURRENT_TIMESTAMP替代简单日志触发器 - 实时统计类(如计数器)优先用
INSERT ... ON DUPLICATE KEY UPDATE或REPLACE INTO,避免触发器中SELECT COUNT(*) - 必须用触发器时,只做单表字段计算(如
NEW.total = NEW.price * NEW.qty),绝不碰其他表、不调用函数、不写日志表
DELIMITER $$
CREATE TRIGGER orders_calculate_total
BEFORE INSERT ON orders
FOR EACH ROW
BEGIN
IF NEW.price IS NOT NULL AND NEW.qty IS NOT NULL THEN
SET NEW.total = NEW.price * NEW.qty;
END IF;
END$$
DELIMITER ;复杂度一旦突破单表达式计算,就该怀疑是不是设计错了——触发器不是胶带,粘不住架构漏洞。










