触发器中禁止使用 SELECT ... FOR UPDATE,因其会使主事务提前持有非目标表行锁,导致锁序混乱和死锁;安全做法是仅用主键或唯一索引单行查找且命中覆盖索引。

为什么触发器里不能写 SELECT FOR UPDATE
触发器运行在主事务中,所有锁都归属同一事务上下文。你在触发器里写 SELECT ... FOR UPDATE,实际是让主事务提前持有了另一张表的行锁——而此时主事务还没走到自己的 DML 阶段,锁序完全不可控。死锁日志里一旦出现 TABLE: `inventory` 和 TABLE: `orders` 交叉等待,基本就是这个原因。
真正安全的做法只有一条:所有读操作必须基于主键或唯一索引单行查找,且必须命中覆盖索引。例如:SELECT qty FROM stock WHERE sku = NEW.sku,前提是 sku 字段有唯一索引;用 EXPLAIN 确认 type 是 const 或 ref,绝不能是 ALL 或 index。
- 禁用
SELECT ... LOCK IN SHARE MODE和任何带范围条件的SELECT ... FOR UPDATE - 别在触发器里调用存储过程,除非该过程内部已做明确的行级锁控制
- 如果只是更新当前行字段,直接用
SET NEW.status = 'done',不发额外 SQL
并发更新时如何避免死锁报错 1213
触发器本身不支持重试,Deadlock found when trying to get lock(错误码 1213)一报,整个事务立刻回滚,触发器逻辑也跟着失效。你没法在触发器里捕获它、也不能 COMMIT 或 ROLLBACK 自身——MySQL 明确禁止 START TRANSACTION、GET DIAGNOSTICS 在触发器中使用。
有效方案只能放在应用层:
- 捕获错误码
1213,最多重试 3 次,间隔按 10ms → 30ms → 100ms 指数退避 - 每次重试前重新生成业务数据(如订单号、时间戳),防止重复插入
- 删掉触发器里所有跨表操作、子查询统计、
SELECT ... FOR UPDATE - 确认
innodb_print_all_deadlocks = 1已开启,否则你连死锁长什么样都不知道
BEFORE UPDATE 中怎么安全改本表字段
AFTER UPDATE 里再对原表执行 UPDATE 会直接报错 ERROR 1442;BEFORE UPDATE 才是唯一能安全赋值的地方。但必须逐字段比对,否则冗余字段会被无差别覆盖。
比如同步用户昵称到订单表:
- 用
IF NOT (OLD.nickname NEW.nickname)判断变更,能正确处理NULL - 写成
SET NEW.buyer_nickname = NEW.nickname,不是UPDATE orders SET buyer_nickname = ... - 别在
BEFORE里查大表——哪怕只查一次,没索引就拖慢整条写入链路
高并发下编号生成为什么不能用 MAX(id)+1
这不是“概率低”,而是确定性失败:SELECT MAX(id) FROM orders 在 RR 隔离级别下加间隙锁,多个事务同时读到相同结果(比如都是 100),接着全设 NEW.id = 101,必然触发 ERROR 1062 (23000): Duplicate entry '101' for key 'PRIMARY'。
原子递增才是正解:
- MySQL:用
INSERT INTO seq_counter VALUES (CURDATE(), 1) ON DUPLICATE KEY UPDATE counter = LAST_INSERT_ID(counter + 1) - PostgreSQL:直接调
NEXTVAL('seq_so') - SQL Server:用
MERGE或UPDATE ... WITH (HOLDLOCK)更新计数行 - 日期部分统一用
CURDATE()(MySQL)或CURRENT_DATE(PG/SQL Server),别在触发器里多次调NOW()
真正难的不是写几行 SQL,而是把业务维度(日期/类型/租户)拆进计数表主键,而不是堆在一个字段里——这点漏掉,再好的原子操作也白搭。

















