背景与症状
在一次常规操作中,一条 INSERT
语句(目标
id=664
)被长时间阻塞,最后在 Go 应用层报错
invalid connection
。
1 | INSERT INTO `chip_info`(`info`,`display_order`,`id`) |
最终排查定位,阻塞的根源是 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 锁阻塞时,可以通过以下视图进行诊断:
查询锁等待关系:
1
SELECT * FROM performance_schema.data_lock_waits;
该表直接展示了哪个事务正在等待哪个事务所持有的锁。
查询活跃事务:
1
SELECT * FROM information_schema.INNODB_TRX;
该表列出了所有当前正在运行的事务及其状态、执行的 SQL 等信息。
关联查询(推荐):
通过以下查询可以将事务信息与锁信息关联,快速定位持有锁的事务 ID (trx_id) 和其对应的数据库连接 ID (trx_mysql_thread_id)。
1
2
3
4
5
6
7SELECT
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
。终止连接:
1
KILL [trx_mysql_thread_id]; -- 将 ID 替换为实际值
执行
KILL
命令后,该事务会立即回滚,释放其持有的所有锁,从而解决阻塞问题。
如何规避 Gap 锁问题
- 缩短事务生命周期:保持事务简短,尽快
COMMIT
或ROLLBACK
,减少锁的持有时间。 - 选择合适的隔离级别:如果业务逻辑允许,将隔离级别设置为
READ COMMITTED
是最有效的规避方法。 - 优化查询,精准锁定:
- 尽量使用唯一索引进行等值查询和更新,避免范围扫描。
- 确保查询条件能够命中高效的索引,避免因索引不当导致锁范围扩大。
- 谨慎使用锁定读:仅在必要时使用
SELECT ... FOR UPDATE
,并确保WHERE
条件尽可能精确。 - 设置锁等待超时:合理配置
innodb_lock_wait_timeout
,避免应用因长时间等待锁而无响应。