第 7 / 22 篇

第 6 章:把“卡住了”还原成等待关系

应用看到 UPDATE 长时间没有返回,首先需要区分数据库正在执行、等待行锁、等待元数据锁,还是请求还没有获得连接。把这些都称为“SQL 慢”,会让优化方向偏离实际原因。

6.1 等待关系是一张有方向的图

事务 A 锁住账户 1,事务 B 也想修改账户 1。B 等待 A,是一条从等待者指向阻塞者的边。若 A 还在正常执行并很快提交,这只是等待;若各事务互相等待形成环,就可能构成死锁。

排查时至少关联事务、连接、当前语句、目标对象和持锁时长。只看最后一条正在等待的 SQL,不一定能找到阻塞者最初获得锁的语句;阻塞者当前可能已经空闲,却仍未结束事务。

6.2 直接观察实际等待

MySQL 8.4 的 Performance Schema 提供 data_locks 与 data_lock_waits。随书实验让一个事务明确持有账户记录的锁,再启动等待者,从独立连接查询实际等待边。

SELECT r.OBJECT_SCHEMA, r.OBJECT_NAME,
       w.REQUESTING_ENGINE_TRANSACTION_ID,
       w.BLOCKING_ENGINE_TRANSACTION_ID
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
WHERE r.OBJECT_SCHEMA=DATABASE();

这是诊断查询,需要相应观测权限。先选择本次实验数据库,DATABASE() 才能提供正确限定;尚未选库时它为 NULL,不会匹配。本版自动测试直接绑定随机库名作为参数,避免将实例上其他会话混入证据。生产环境还要结合访问权限和字段脱敏管理,不能把全实例事务信息无条件提供给所有用户。

6.3 索引、范围与隔离级别共同决定锁

InnoDB 的记录锁与索引记录相关。范围扫描还可能涉及间隙与 next-key 锁,行为随隔离级别、条件和访问路径变化。RC 减少某些间隙锁行为,并不表示所有竞争或死锁消失。

本书真实实验重点验证主键行竞争、反向锁序、等待超时与队列认领,没有完整枚举所有范围锁布局。遇到自己的范围查询时,应固定表结构、数据和隔离级别,从实际计划与锁表读取结果,再解释现象。

6.4 解除等待不等于修复根因

终止阻塞连接可能解除眼前等待,也可能导致昂贵回滚,并影响该连接承担的业务。操作前应确认业务归属、事务状态和恢复方式。不能只因为它出现在阻塞图里就直接执行终止命令。

根因可能是事务中调用了慢外部接口、连接池归还前没有回滚、批量更新范围过大或索引失效。应把“解除当前阻塞”和“避免下次发生”分别记录,前者是处置,后者需要代码或数据访问方式的改动。

练习。 阻塞者当前显示 Sleep,等待者是一条 UPDATE。为什么仍可能是前者持有锁?请写出你需要补充的事务信息,以及终止它之前需要确认的业务事项。