InnoDB通过挂起线程并加入锁等待队列处理行锁冲突:T2创建is_waiting=true的锁结构插入FIFO队列末尾,调用os_event_wait()休眠;由T1提交/回滚时扫描队列头部唤醒T2重试加锁,超时则报Lock wait timeout exceeded。

行锁冲突时,InnoDB如何挂起事务线程
当事务 T2 尝试更新一条已被 T1 加了 X 锁的记录时,InnoDB 会先检查该记录上是否存在活跃锁结构(lock_t)。若存在且与当前事务不兼容(比如 T1 持有 X 锁,T2 也申请 X 锁),InnoDB 就不会立即报错,而是为 T2 创建一个新的锁结构,并把其 is_waiting 属性设为 true。
这个锁结构会被插入到该记录的锁等待队列(FIFO)末尾。此时 T2 对应的内核线程会调用 os_event_wait() 进入休眠状态,等待被唤醒——它不再占用 CPU,但线程上下文(包括事务状态、SQL 执行位置等)完整保留在内存中。
常见错误现象包括:
-
SHOW PROCESSLIST中看到状态为updating或Locked,但 SQL 停住不动 -
SELECT * FROM information_schema.INNODB_TRX显示trx_state = 'LOCK WAIT',且trx_wait_started时间持续增长
谁来唤醒?唤醒条件是什么
唤醒动作由持有锁的事务(T1)释放锁时触发。T1 提交或回滚后,InnoDB 会:
- 清理它在该记录上创建的锁结构
- 扫描该记录的锁等待队列,取出队首(最老)的等待锁结构(即 T2 的)
- 将其
is_waiting改为false - 调用
os_event_set()唤醒对应线程
注意:唤醒不等于加锁成功。T2 被唤醒后会重试获取锁——重新检查锁兼容性。如果此时又有 T3 插入并抢先拿到了锁,T2 可能再次进入等待。这就是为什么死锁检测必须在“等待前”或“等待中”介入,而不是等唤醒之后。
关键细节:
安装、修复、更新或打包一个基于 Python 和 PyMySQL 实现的 Windows `mysql` 命令。适用于用户希望在 PowerShell/CMD 中输入 `mysql` 时重定向到 Python 实现、需要交互式输入 MySQL 连接信息、需要同步到当前用户本地 bin 目录,或需要将该能力封装成可公开复用的 Codex skill。
- 唤醒只针对队列头部,不跳过中间事务(无优先级调度)
- 若等待超时(由
innodb_lock_wait_timeout控制,默认 50 秒),T2 会收到Lock wait timeout exceeded错误并回滚 - 唤醒是单向信号,不传递锁所有权;T2 必须自己重新竞争
为什么有时看到“唤醒后立刻又等”
这通常源于两个现实约束:
- 行锁不是孤立存在的:T2 在获取目标记录锁前,可能还需获取二级索引上的意向锁、或聚簇索引上的记录锁(回表场景),任一环节冲突都会导致二次等待
- 锁粒度升级:当更新涉及大量行,InnoDB 可能主动将多个行锁升级为表锁(尤其在
autocommit=0+ 大范围UPDATE时),此时 T2 等待的是表级IX或X锁,而非单条记录
典型诱因包括:
- SQL 没走索引,触发全表扫描 + 行锁堆积,最终触发锁升级
- 事务中混合执行 DML 和 DDL(如
ALTER TABLE),隐式持有元数据锁(MDL),阻塞后续所有操作 - 隔离级别为
REPEATABLE READ且使用范围条件(如WHERE age > 25),InnoDB 加的是Next-Key Lock,锁定间隙,扩大了冲突面
调试时别漏掉锁结构链路本身
真正容易被忽略的,是锁结构并非直接绑定“SQL 语句”,而是绑定“索引记录”。同一个 UPDATE t SET x=1 WHERE id=100:
- 如果
id是主键 → 锁在聚簇索引叶子节点 - 如果
id是二级唯一索引 → 先锁二级索引项,再锁主键项(回表) - 如果
id字段无索引 → 全表扫描,每行都尝试加锁,极易触发锁升级或长时间等待
所以看 INNODB_LOCK_WAITS 和 INNODB_LOCKS 时,重点比对 lock_trx_id、lock_rec_id(即 space_id + page_no + heap_no)和 lock_index,而不是只盯 SQL 文本。锁等待的本质,永远是“某事务正在等某索引页上某槽位的某种锁”。










