
故障排查
一次由 MySQL Gap 锁导致的阻塞排查实录
记录一次线上 INSERT 被长时间阻塞的定位过程,最终确认由 MySQL Gap/Next-Key 锁引起;文中说明锁的原理与触发场景,提供 MySQL 8.0 的 datalockwaits/INNODBTRX 诊断方法、KILL 应急,以及隔离级别、索引与语句优化等规避建议。
背景与症状
在一次常规操作中,一条 INSERT 语句(目标 id=664)被长时间阻塞,最后在 Go 应用层报错 invalid connection。
INSERT INTO `chip_info`(`info`,`display_order`,`id`)
VALUES ('{...}', 519, 664)
ON DUPLICATE KEY UPDATE `info`=VALUES(`info`), `display_order`=VALUES(`display_order`);
最终排查定位,阻塞的根源是 MySQL 的 Gap 锁(间隙锁)。通过终止持有该锁的悬挂事务,操作立即恢复正常。
Gap 锁与 Next-Key 锁的定义
- Gap 锁 (Gap Lock):这是一种锁机制,它锁定的不是具体的某一行记录,而是索引记录之间的"间隙"。其唯一目的是防止其他事务在这个间隙中执行
INSERT操作。 - Next-Key 锁 (Next-Key Lock):这是 InnoDB 在
REPEATABLE READ隔离级别下的默认锁策略。它本质上是行锁 (Record Lock) 与该行记录之前间隙的 Gap 锁的组合。Next-Key 锁是解决幻读问题的核心机制。
第一性原理:为什么需要 Gap 锁?
核心目标:实现可重复读 (Repeatable Read)
在 REPEATABLE READ 隔离级别下,数据库承诺在一个事务内,对同一条件的多次查询将返回完全相同的结果集。如果不存在 Gap 锁,并发的 INSERT 操作会破坏这一承诺,导致幻读 (Phantom Read)。
幻读场景示例 (无 Gap 锁的情况下)
- T1:
SELECT * FROM t WHERE id < 25;返回 5 条记录。 - T2:
INSERT INTO t(id) VALUES (15);并提交。 - T1: 再次执行
SELECT * FROM t WHERE id <25;,此时将返回 6 条记录。事务 T1 内的查询结果集发生了变化,违反了可重复读的原则。
Gap 锁的解决方案
为了防止幻读,InnoDB 引入了 Gap/Next-Key 锁。当事务 T1 执行范围查询时,InnoDB 不仅会锁住满足条件的已有行,还会锁住查询范围内的所有"间隙"。这样,事务 T2 的 INSERT 操作因无法在锁定的间隙中插入数据而被阻塞,直到 T1 提交或回滚,从而保证了 T1 的查询结果一致性。
理论与实践的平衡
Gap 锁可以看作是理论上谓词锁 (Predicate Lock) 的一种工程化、高性能的近似实现。它通过锁定索引区间来间接实现对查询谓词的保护。同时,这种机制也保证了基于语句的复制(SBR)在主从环境下执行结果的确定性。
Gap 锁的触发场景
Gap 锁的产生与隔离级别、索引使用和查询类型密切相关。
在 REPEATABLE READ 隔离级别下:
- 范围查询:执行
SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE、UPDATE、DELETE时,若WHERE条件是范围扫描(如>、<、BETWEEN),会锁定扫描过的索引区间。 - 唯一索引等值查询未命中:当使用唯一索引(包括主键)进行等值查询,但该记录不存在时,为防止并发插入该值,InnoDB 会在对应位置加上 Gap 锁。
在 READ COMMITTED 隔离级别下:
- 该级别下默认禁用 Gap 锁,因此大大减少了阻塞概率。
- 但在外键约束检查和唯一性检查这两种特殊场景下,为了保证数据一致性,仍然可能会产生 Gap 锁。
诊断与定位方法 (MySQL 8.0+)
当怀疑发生 Gap 锁阻塞时,可以通过以下视图进行诊断:
-
查询锁等待关系:
SELECT * FROM performance_schema.data_lock_waits;该表直接展示了哪个事务正在等待哪个事务所持有的锁。
-
查询活跃事务:
SELECT * FROM information_schema.INNODB_TRX;该表列出了所有当前正在运行的事务及其状态、执行的 SQL 等信息。
-
关联查询(推荐):
通过以下查询可以将事务信息与锁信息关联,快速定位持有锁的事务 ID (trx_id) 和其对应的数据库连接 ID (trx_mysql_thread_id)。
SELECT t.trx_id, t.trx_state, t.trx_started, t.trx_mysql_thread_id, t.trx_query FROM information_schema.INNODB_TRX t JOIN performance_schema.data_locks dl ON t.trx_id = dl.ENGINE_TRANSACTION_ID WHERE dl.LOCK_STATUS = 'GRANTED' -- 找到持有锁的事务 ORDER BY t.trx_started;
应急解决方案
定位到持有锁的事务后,最直接的解决方法是终止其数据库连接。
-
获取连接 ID (thread_id):
通过上述诊断查询,找到
trx_mysql_thread_id。 -
终止连接:
KILL [trx_mysql_thread_id]; -- 将 ID 替换为实际值执行
KILL命令后,该事务会立即回滚,释放其持有的所有锁,从而解决阻塞问题。
如何规避 Gap 锁问题
- 缩短事务生命周期:保持事务简短,尽快
COMMIT或ROLLBACK,减少锁的持有时间。 - 选择合适的隔离级别:如果业务逻辑允许,将隔离级别设置为
READ COMMITTED是最有效的规避方法。 - 优化查询,精准锁定:
- 尽量使用唯一索引进行等值查询和更新,避免范围扫描。
- 确保查询条件能够命中高效的索引,避免因索引不当导致锁范围扩大。
- 谨慎使用锁定读:仅在必要时使用
SELECT ... FOR UPDATE,并确保WHERE条件尽可能精确。 - 设置锁等待超时:合理配置
innodb_lock_wait_timeout,避免应用因长时间等待锁而无响应。
来自开放网络的回应
引用、喜欢、转发和站外回复会通过 Webmention 回到这里。
接收与展示代码已经就绪,注册正式域名后即可开始收集回应。
评论
评论由 GitHub Discussions 保存和管理。