触发器本身不直接锁表,真正锁表的是其内部未走索引的sql语句(如where sku无索引导致全表扫描并加next-key lock),或在repeatable read下因范围条件触发间隙锁;before/after时机影响锁持有逻辑,且锁持续时间等于父事务生命周期。

触发器本身不会主动锁整张表,真正锁表的是它内部执行的 SQL 语句——尤其是那些没走索引、扫描范围过大或在高隔离级别下触发间隙锁的操作。
触发器里 UPDATE 没走索引,直接扫全表
这是最常见原因。比如触发器中写了 UPDATE inventory SET stock = stock - 1 WHERE sku = @sku,但 sku 字段没建索引,InnoDB 只能全表扫描。在 REPEATABLE READ 隔离级别下,这会为所有扫描到的行 + 间隙加 Next-Key Lock,效果等同于锁表。
- 用
EXPLAIN查触发器内这条语句,看type是不是ALL - 确认
SHOW CREATE TABLE inventory中sku是否在某个索引的最左列 - 函数包装(如
WHERE UPPER(sku) = UPPER(@sku))会让索引失效 - 字符集不匹配(字段是
utf8mb4_0900_as_cs,参数却是utf8mb4_general_ci)也会跳过索引
BEFORE/AFTER 触发时机影响锁持有逻辑
BEFORE 触发器运行时,主语句还没落盘,但已持有了目标行的 X 锁;AFTER 则一定在变更之后执行,若它再对其他表做 UPDATE,等于在同一事务里多加一次锁,容易形成锁等待环路。
-
BEFORE可安全修改NEW.column,值会写入最终结果 -
AFTER中改NEW无效,且 MySQL 5.7+ 明确禁止更新触发它的表,否则报错Can't update table 't1' in stored function/trigger - 多个
AFTER INSERT同时往同一张统计表写数据,可能因主键争抢触发死锁
触发器调用存储过程或 UDF 放大锁范围
一个看似干净的触发器,可能通过 CALL update_stock_log @sku, @qty 调用另一个过程,而该过程内部又做了 INSERT INTO stock_log_history SELECT * FROM inserted —— 如果这个 SELECT 没限定主键或没加 WITH (NOLOCK),就可能触发对 stock_log_history 的全表扫描加锁,反过来阻塞主表。
- 所有嵌套调用共享同一个事务上下文,锁不会自动释放
- SQL Server 中 UDF 若含
SELECT,每次调用都会申请共享锁 - MySQL 触发器内严禁使用
START TRANSACTION、COMMIT或ROLLBACK,会直接报错或引发隐式锁异常
锁持续时间不等于触发器执行时间
触发器锁表时间 = 主事务生命周期,不是触发器跑完就释放。哪怕触发器只耗时 200ms,只要事务后面还有 SLEEP(30) 或没 COMMIT,锁就挂满 30 秒。
-
SHOW PROCESSLIST看到状态是Updating或Waiting for table metadata lock,大概率是事务没结束 - ORM(如 Django 的
@transaction.atomic)可能自动包了一层事务,你根本没写BEGIN,锁早挂上了 - 连接池复用时,上个请求忘了
COMMIT,下个请求接着用同一个连接,锁直接继承
真正要定位问题,得查 performance_schema.data_locks(MySQL 8.0+)或 INFORMATION_SCHEMA.INNODB_TRX + INNODB_LOCK_WAITS,而不是盯着触发器代码本身——它只是事务链路上的一环,锁行为藏在它执行的每一条语句里。











