mysql写冲突时在等待持有锁的事务提交或回滚以释放行锁或间隙锁;该等待为内核级挂起,不耗cpu但占连接,超时由innodb_lock_wait_timeout控制,默认50秒。

MySQL 写冲突时到底在等什么
MySQL 在执行 UPDATE、DELETE 或带 FOR UPDATE 的 SELECT 时,如果遇到已被其他事务加锁的行,不会直接报错,而是进入“锁等待”状态——它在等那个持有锁的事务提交或回滚,释放 record lock 或 next-key lock。
这个等待不是轮询,是内核级挂起:线程被置为 SLEEP 状态,不消耗 CPU,但占用连接和事务上下文。一旦锁释放,InnoDB 唤醒等待队列头部的事务(FIFO),继续执行。
- 超时由
innodb_lock_wait_timeout控制,默认 50 秒;超时后抛出错误:Lock wait timeout exceeded; try restarting transaction - 只对行级锁(
REPEATABLE READ和READ COMMITTED)生效;READ UNCOMMITTED不加行锁,SERIALIZABLE会升级为间隙锁甚至表级意图锁 - 注意:唯一索引等值查询(如
WHERE id = ?)只锁匹配行;范围查询(如WHERE age > 25)会锁住间隙,可能阻塞更多操作
如何快速定位谁在锁谁
别猜,直接查 information_schema 三张表:当前锁、锁等待、事务状态。核心组合是:
SELECT r.trx_id waiting_trx_id,
r.trx_mysql_thread_id waiting_thread,
r.trx_query waiting_query,
b.trx_id blocking_trx_id,
b.trx_mysql_thread_id blocking_thread,
b.trx_query blocking_query
FROM information_schema.INNODB_LOCK_WAITS w
JOIN information_schema.INNODB_TRX b ON b.trx_id = w.blocking_trx_id
JOIN information_schema.INNODB_TRX r ON r.trx_id = w.requesting_trx_id;
- 如果结果为空,说明没有活跃的锁等待(但不代表没锁,只是没形成等待链)
-
blocking_query可能为NULL—— 持锁事务已提交但未清理元数据,或正在执行 DML 但尚未提交 - 配合
SHOW ENGINE INNODB STATUS\G看更细的 lock struct 和 heap size,但输出杂乱,优先用上面 SQL
INSERT ... ON DUPLICATE KEY UPDATE 会锁哪些行
这个语句看似“原子”,实际分两步:先按唯一键(主键或 UNIQUE 索引)查找,再决定 INSERT 还是 UPDATE。因此它会对**所有可能命中唯一约束的索引记录**加锁,包括:
- 若找到匹配行 → 加
record lock到该行(UPDATE 分支) - 若没找到但存在“间隙” → 加
next-key lock到对应间隙(防止并发 INSERT 冲突) - 若唯一索引是联合索引,且 WHERE 条件只用前缀(如
INDEX(a,b)但只查a=1),会锁整个范围,影响面更大
常见坑:INSERT ... ON DUPLICATE KEY UPDATE 在高并发下比纯 INSERT IGNORE 更容易引发锁等待,因为后者只在冲突时简单跳过,不加 UPDATE 锁。
避免死锁的关键不是重试,而是顺序
死锁本质是循环等待:事务 A 持有 X 锁等待 Y,事务 B 持有 Y 锁等待 X。InnoDB 能检测并杀掉其中一个(选 undo log 小的),但预防比处理重要。
- 所有业务逻辑中,对多行更新/删除,**强制按主键升序操作**(如
ORDER BY id ASC),让不同事务以相同顺序申请锁 - 避免在事务里混合使用不同索引条件更新同一张表(比如一个事务用
WHERE name=?,另一个用WHERE email=?),容易因索引扫描路径不同导致加锁顺序不一致 - 单条语句尽量覆盖全量条件,减少“先 SELECT 再 UPDATE”的模式——中间空窗期可能被其他事务插入/修改,引发后续锁升级
真正难处理的是跨表操作(比如先改用户表再改订单表),这时候锁顺序必须全局约定,否则靠重试解决不了根本问题。










