根本原因是触发器隐式扩展主事务锁范围至多张表且锁生命周期与主事务绑定;须控制执行路径与锁粒度,禁用非索引dml、范围条件及逐行逻辑,优先用before赋值或集合操作。

批量更新时触发器引发严重锁表,根本原因不是触发器“太慢”,而是它把单条语句的锁范围隐式扩展到多张表、且锁生命周期与主事务完全绑定。停用触发器或降隔离级别基本无效,关键得控制它的执行路径和锁粒度。
触发器在UPDATE中不开启新事务,但会延长主事务锁生命周期
触发器内所有 UPDATE、INSERT、SELECT FOR UPDATE 都复用主事务 ID,锁直到 COMMIT 才释放。你执行一条 UPDATE orders,背后可能已对 user_points、audit_log 同时加了 X 锁——这些锁不会因触发器执行完就松开。
- 排查时注意错误堆栈里出现的
Deadlock found when trying to get lock,如果报错 SQL 是UPDATE audit_log SET ... WHERE order_id = NEW.id,但应用日志只记了INSERT INTO orders,说明锁源头被掩盖了 - 用
SHOW ENGINE INNODB STATUS\G查LATEST DETECTED DEADLOCK区块,若看到mysql tables in use 2, locked 2,基本可判定有触发器跨表操作 - 别指望把隔离级别从
REPEATABLE READ降到READ COMMITTED来缓解——触发器内 DML 的锁行为不受此影响
触发器内DML必须走唯一索引,禁用范围条件
触发器里一句 UPDATE config SET value = 'on' WHERE module = 'payment',如果 module 没索引,InnoDB 就全表扫描并给每行加 X 锁。哪怕只改一行,也等于锁住整张表的活跃数据段。
- 对触发器中每条 DML,用真实参数模拟执行
EXPLAIN FORMAT=TRADITIONAL,确认type不是ALL或index,key字段非NULL,rows估算值接近实际影响行数 - 禁用
WHERE status = 'pending'这类范围条件,它会触发间隙锁;统一改用主键或唯一索引更新,例如WHERE id = NEW.config_id - SQL Server 中尤其要避免把
inserted当单行变量处理:SELECT @id = id FROM inserted会丢弃其余行,导致 10 万行批量更新被拆成 10 万次单行触发,锁反复申请释放
用集合操作替代逐行逻辑,避免伪表误用
触发器内用循环或标量赋值处理多行插入/更新,本质是把集合操作退化为 RBAR(Row By Agonizing Row),极大放大锁竞争窗口。
- 正确做法是集合运算:例如 MySQL 触发器中写
UPDATE t2 JOIN inserted ON t2.order_id = inserted.id SET t2.status = 'done'(需适配语法),SQL Server 中写UPDATE t2 SET status = 'done' FROM t2 INNER JOIN inserted ON t2.order_id = inserted.id - 禁止在触发器里做子查询关联大表:
SELECT balance FROM accounts WHERE user_id = NEW.user_id若user_id无索引,会提前加 gap lock,而主事务又在更新同一张表其他行 → 等待环立刻形成 - 如果触发器必须更新本表(如维护统计字段),优先用
SET NEW.field = ...在BEFORE触发器中直接赋值,不走额外UPDATE语句
真正危险的是锁路径不可见,而非触发器本身存在
你很难从应用日志看出某次 INSERT 背后悄悄锁了三张表。优化重点不是删掉触发器,而是让它的锁行为可预测、可收敛:每条 DML 的索引路径必须确定,跨表顺序必须统一,且不能依赖运行时状态判断。
最易被忽略的一点:触发器内所有读操作都必须命中覆盖索引。哪怕只是 SELECT id FROM log_buffer WHERE order_id = ?,如果没建 INDEX(order_id, id),InnoDB 仍可能扫二级索引再回表,锁范围瞬间失控。










