触发器里执行复杂 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 ;复杂度一旦突破单表达式计算,就该怀疑是不是设计错了——触发器不是胶带,粘不住架构漏洞。
