跳到主要内容

技术教程

MySQL 行锁等待:用 data_lock_waits 找到真正的阻塞事务

验证与风险信息

适用环境
MySQL 8.4.10 / InnoDB / Docker 24.0.7
验证耗时
约 12 分钟
风险与回滚
诊断查询为低风险;KILL CONNECTION 和生产索引变更必须先确认事务所有者、回滚规模与变更窗口
最后复核
2026-08-01
验证证据
隔离实验观察到 PRIMARY 索引记录锁的等待与阻塞事务映射;阻塞事务提交后等待事务完成,余额为 1025.00,等待数归零。

MySQL 请求突然变慢、应用连接池堆积时,“有锁”等于没有结论。InnoDB 每时每刻都在使用锁;真正要回答的是:哪个事务正在等待、哪个事务阻塞它、双方执行了什么、锁落在哪张表和哪个索引,以及持锁事务能否安全结束。只看 SHOW PROCESSLIST 经常只能看到一个会话处于 updating,却看不到完整的阻塞关系。

适用范围与结论

本文适用于 MySQL 8.0/8.4、InnoDB 表和行锁等待场景。V2CE 复现实验使用 MySQL 8.4.10,通过两个独立连接更新同一条主键记录,使用 Performance Schema 的 data_locksdata_lock_waits 建立等待者到阻塞者的映射。

锁等待和死锁不是同一件事:

  • 锁等待表示一个事务暂时无法获得锁,可能在持锁事务提交后继续执行,也可能达到 innodb_lock_wait_timeout 后报错;
  • 死锁表示事务之间形成循环依赖,InnoDB 会检测并回滚其中一个事务以打破循环。

诊断时先保存证据,再决定提交、回滚、终止会话或优化 SQL。直接杀掉 processlist 中运行时间最长的连接,可能中断正常批任务并触发长时间回滚。

第一步:确认当前确实存在锁等待

使用具有 Performance Schema 查询权限的只读诊断账号执行:

SELECT COUNT(*) AS current_lock_waits
FROM performance_schema.data_lock_waits;

返回大于 0 表示采样时存在锁等待关系。该表反映当前状态,等待解除后记录会消失,因此生产环境应保存查询时间、结果和相关监控窗口。不要把一次返回 0 理解为“过去没有发生过”。

第二步:把等待锁和阻塞锁连接起来

下面的查询把 data_lock_waits 中的请求锁、阻塞锁分别连接到 data_locks,得到事务 ID、对象、索引、模式和锁数据:

SELECT
  w.REQUESTING_ENGINE_TRANSACTION_ID AS waiting_trx_id,
  w.BLOCKING_ENGINE_TRANSACTION_ID AS blocking_trx_id,
  r.OBJECT_SCHEMA,
  r.OBJECT_NAME,
  r.INDEX_NAME,
  r.LOCK_TYPE,
  r.LOCK_MODE AS waiting_lock_mode,
  r.LOCK_STATUS AS waiting_lock_status,
  b.LOCK_MODE AS blocking_lock_mode,
  b.LOCK_STATUS AS blocking_lock_status,
  r.LOCK_DATA
FROM performance_schema.data_lock_waits AS w
JOIN performance_schema.data_locks AS r
  ON r.ENGINE = w.ENGINE
 AND r.ENGINE_LOCK_ID = w.REQUESTING_ENGINE_LOCK_ID
JOIN performance_schema.data_locks AS b
  ON b.ENGINE = w.ENGINE
 AND b.ENGINE_LOCK_ID = w.BLOCKING_ENGINE_LOCK_ID;

LOCK_DATA 可帮助识别记录,但不应把它当作完整业务数据导出。生产证据中应按数据分类规则脱敏。若查询涉及非唯一索引或范围条件,还可能出现间隙锁、next-key lock 或多条锁记录,不能只截取结果第一行。

第三步:定位连接、事务与正在执行的 SQL

事务 ID 解释了锁关系,连接信息则帮助确认责任服务和操作上下文。先查看 InnoDB 事务:

SELECT
  trx_id,
  trx_mysql_thread_id,
  trx_started,
  trx_state,
  trx_wait_started,
  trx_rows_locked,
  trx_rows_modified,
  LEFT(trx_query, 200) AS current_query
FROM information_schema.innodb_trx
ORDER BY trx_started;

再把 trx_mysql_thread_id 与进程列表的 ID 对照:

SELECT ID, USER, HOST, DB, COMMAND, TIME, STATE, LEFT(INFO, 200) AS sql_text
FROM information_schema.PROCESSLIST
WHERE COMMAND <> 'Sleep' OR TIME > 30
ORDER BY TIME DESC;

阻塞者可能显示为 SleepINFO 为空,因为它已经执行完 SQL,但事务尚未提交。此时不能凭 processlist 判断“空闲连接无害”;应以 innodb_trx 和锁表为准,并结合应用 trace、连接来源和事务开始时间查明代码路径。

第四步:判断是等待、超时还是死锁

锁等待达到会话的 innodb_lock_wait_timeout 时,等待的语句通常返回 ERROR 1205 (HY000): Lock wait timeout exceeded。这不等于整个事务已经自动回滚;应用应明确执行回滚,并根据业务幂等性决定是否重试。

死锁常见错误为 ERROR 1213 (40001)。可以读取最近一次 InnoDB 死锁信息:

SHOW ENGINE INNODB STATUS\G

如果需要持续保留死锁记录,应使用受控的数据库日志配置和集中日志系统,而不是人工高频轮询。锁等待只是一条单向边;死锁需要形成环,处理策略不能混用。

现场处置:先找事务所有者,再终止连接

优先让持锁事务按业务逻辑提交或回滚。若已经确认它是异常遗留事务、继续阻塞造成的损失更大,并且有权限与变更记录,才考虑:

KILL CONNECTION 12345;

终止连接后 InnoDB 仍可能需要回滚大量修改,锁不一定瞬间释放。持续检查 innodb_trxdata_lock_waits 和受影响请求的延迟。不要在未知事务内容、回滚规模和主从拓扑影响时批量 KILL。

根因修复

  1. 缩短事务:不要在事务中等待远程 API、用户输入或无关计算;尽早提交或回滚。
  2. 统一加锁顺序:多个业务流程更新相同资源时,以一致顺序访问记录,降低死锁概率。
  3. 让条件命中合适索引:EXPLAIN 验证访问路径。缺少索引可能扫描并锁定比预期更多的记录,但不要在事故现场未经评估直接创建大索引。
  4. 限制批量大小:把超大批次拆成可控事务,同时保留幂等键和失败恢复机制。
  5. 修正连接管理:确保所有异常路径都会回滚并归还连接,避免“SQL 已完成但事务未结束”。

修改后使用相同并发路径复测,确认等待数量、P95/P99 延迟、事务时长和超时错误都回到基线。单独看到 data_lock_waits=0 只能证明采样瞬间没有等待,不能替代完整负载验证。

V2CE 复现实验记录

实验建立 accounts InnoDB 表和主键 id=1。事务 A 更新余额后保持事务 12 秒;事务 B 在另一个连接中更新同一主键。采样时,MySQL 8.4.10 返回一条等待关系:等待事务 1808、阻塞事务 1807、对象 v2ce_lab.accounts、索引 PRIMARY、记录锁模式 X,REC_NOT_GAP,请求状态为 WAITING,阻塞锁状态为 GRANTED

同期 processlist 显示阻塞连接正在 SLEEP(12),等待连接状态为 updating。事务 A 提交后,事务 B 取得锁并提交;最终余额从 1000.00 变为 1025.00,data_lock_waits 返回 0。实验随后删除了独立容器、网络和数据卷。

参考资料