99%的“死循环”实为ERROR 1442拦截,根本原因是恢复语句触发原表触发器,而触发器试图操作同一张表;需通过SET SESSION sql_log_bin = 0与会话变量守卫临时禁用,而非删除或依赖max_sp_recursion_depth。

恢复数据时触发器引发的“死循环”,99%不是真循环,而是触发器在还原过程中反复报 ERROR 1442 或隐式失败——根本原因是恢复语句(如 INSERT、UPDATE)触发了原表上的触发器,而该触发器又试图操作同一张表。
为什么 mysqldump 恢复会卡在触发器上
mysqldump 默认导出的 SQL 中包含 CREATE TRIGGER 语句,且会在建表后、插入数据前执行。但如果你用的是带 --triggers 的完整备份,还原时每条 INSERT 都会激活触发器;若触发器里有 UPDATE users SET updated_at = NOW() WHERE id = NEW.id 这类逻辑,就会立刻撞上 MySQL 的语法级拦截:ERROR 1442 (HY000): Can't update table 'users' in stored function/trigger...。
- 这不是性能问题,是 MySQL 主动拒绝执行,事务直接回滚
- 部分客户端(如某些 Python pymysql 连接池)可能静默吞掉这个错误,只看到数据没写进去,却查不到报错
- 即使你加了
IF OLD.col != NEW.col判断,只要触发器体里存在任何对本表的 DML,就仍会触发拦截
恢复前必须禁用触发器(不是删除)
MySQL 没有 DISABLE TRIGGER 语法,但有等效且安全的绕过方式:临时关闭当前会话的触发器执行权限。最可靠的做法是,在执行恢复 SQL 前,先运行:
SET SESSION sql_log_bin = 0; SET @TRIGGER_DISABLED = 1;
然后在所有触发器定义中统一加守卫逻辑(需提前修改好备份文件或线上定义):
BEGIN
IF @TRIGGER_DISABLED IS NOT NULL THEN
LEAVE proc_label;
END IF;
-- 原有业务逻辑
END- 不要用
DROP TRIGGER再重建——误删无法回退,且恢复中途出错会导致状态不一致 -
sql_log_bin = 0可防止 binlog 记录触发器副作用,避免从库同步异常 - 变量
@TRIGGER_DISABLED是会话级的,不影响其他连接,也不污染全局状态
批量 INSERT 时触发器被逐行执行,性能雪崩
MySQL 触发器只有 FOR EACH ROW,没有语句级触发。这意味着一条 INSERT INTO t VALUES (...),(...),(...) 会调用触发器 3 次——如果触发器里还带 SELECT ... FROM other_table WHERE x = NEW.id,I/O 和锁开销会指数级放大。
- 恢复大表时,这种设计会让导入速度下降 5–10 倍,甚至因锁等待超时中断
- 别指望
max_sp_recursion_depth = 1能缓解——它管的是嵌套调用层级,不是行数 - 真正有效的做法:在恢复前临时改写触发器,把所有 DML 替换为
SET NEW.xxx = ...(仅适用于 BEFORE 触发器),或干脆注释掉整个触发体
误删触发器后恢复,千万别直接跑 CREATE TRIGGER
从备份或 binlog 里捞出的 CREATE TRIGGER 语句,常含 DEFINER = 'user'@'host'。如果目标库没这个用户,或权限不足,语句会静默失败(尤其在低版本 MySQL 中)。
- 先检查并创建对应用户:
CREATE USER IF NOT EXISTS 'trigger_admin'@'localhost' IDENTIFIED BY 'pwd'; - 再手动去掉或重写
DEFINER子句,改为当前登录用户,或用ALTER DEFINER(MySQL 5.7+) - 执行前确认
SQL_MODE一致,否则STRICT_TRANS_TABLES缺失可能导致空值插入失败却不报错 - 最关键的一步:恢复后立刻在测试环境用真实流量压测,因为触发器逻辑是否生效,往往只在特定字段变更组合下才暴露



















