触发器中不能显式使用 begin transaction,因mysql会忽略、sql server会报错;其dml共享父事务的锁与回滚点,正确做法是在主事务开头用select ... for update预加锁。

触发器里不能显式用 BEGIN TRANSACTION
MySQL 和 SQL Server 的 DML 触发器都运行在**父事务上下文内**,你写 BEGIN TRANSACTION 不会开启新事务,反而会报错(SQL Server)或被忽略(MySQL)。触发器语句直接复用主事务的 ID、锁生命周期和回滚点——这意味着你在触发器里执行的 UPDATE 或 INSERT,和主 SQL 共享同一组锁,也一起提交或回滚。
常见错误现象:
- 在触发器里写
START TRANSACTION→ MySQL 报Can't execute the given command because you have active locked tables或静默失败 - 试图在触发器中
COMMIT→ 直接报错:Cannot execute statement in a READ ONLY transaction(MySQL 8.0+)或The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION(SQL Server)
SELECT ... FOR UPDATE 必须在主事务开头预加锁
如果你的触发器要更新日志表或校验余额,而主 SQL 又在操作订单表,两个路径锁序不一致就极易死锁。正确做法不是让触发器自己去锁,而是**在主事务最开始就把触发器要碰的行提前锁住**。
例如主 SQL 是 INSERT INTO orders (...) VALUES (...),触发器要更新 log_table SET status = 'done' WHERE order_id = NEW.order_id:
- ✅ 正确:在
START TRANSACTION后第一句就执行SELECT id FROM log_table WHERE order_id = ? FOR UPDATE - ❌ 错误:等触发器自动执行时再
UPDATE log_table—— 此时锁顺序由优化器决定,可能走非唯一索引,引发 gap lock - ⚠️ 注意:
FOR UPDATE必须作用于明确主键或唯一索引列,否则可能锁整范围;且该语句本身必须在事务内,不能单独执行
触发器中所有 DML 必须走唯一索引路径
触发器里的 UPDATE 或 DELETE 如果用范围条件(如 WHERE status = 'pending'),InnoDB 会加 gap lock,扩大锁范围,且无法被主事务预判。这是死锁高频源头。
- 禁止写
UPDATE audit_log SET processed = 1 WHERE created_at - 必须改写为基于主键或唯一键的精确匹配:
UPDATE audit_log SET processed = 1 WHERE id = ?或WHERE order_id = ? AND tenant_id = ?(复合唯一索引) - 检查执行计划:
EXPLAIN确认触发器内所有语句都命中type=const或type=eq_ref,避免range或index扫描
跨表操作宁可异步,也不要塞进触发器事务
触发器调用存储过程、写入多个非核心表(如统计表、通知表、外部映射表),等于把锁范围不可控地放大。一旦其中一张表响应慢或锁冲突,整个主事务就被拖住。
- 资金类、库存类等强一致性场景:触发器只做轻量校验(如
CHECK约束级逻辑),写日志/发消息全部移出事务边界 - 审计类需求:改用
INSERT IGNORE INTO log_buffer (order_id, action) VALUES (?, 'created'),再由定时任务批量 flush 到正式表 - 若必须同步写,用应用层单事务完成全部操作(如 ORM 中的
select_for_update+save()),而非依赖数据库层触发
真正难处理的不是语法怎么写,而是锁的“时间窗口”和“作用域”是否可控——触发器把这两者全藏起来了。你看到的是一条 INSERT,背后可能是三张表、四个索引、两秒锁等待。











