mysql中的间隙锁与唯一键锁的使用

5次阅读

间隙锁在 REPEATABLE READ 级别下,由非唯一索引或范围查询触发,锁定索引间隙而非记录本身;唯一索引等值查询(=、非 NULL、索引有效)时通常不加间隙锁。

mysql 中的间隙锁与唯一键锁的使用

间隙锁(Gap Lock)在什么情况下会被触发

MySQL 的间隙锁只在 REPEATABLE READ 隔离级别下由 InnoDB 主动加,且仅作用于非唯一索引或范围查询场景。它不会锁住记录本身,而是锁定索引项之间的“空隙”,防止其他事务在这个范围内插入新行。

常见触发条件包括:

  • SELECT …… FOR UPDATESELECT …… LOCK IN SHARE MODE 配合范围条件(如 WHERE age > 25),且 age 列无唯一索引
  • UPDATE/DELETE 使用非唯一索引做范围扫描(如 UPDATE users SET name='x' WHERE status=1status 是普通索引)
  • 即使查询结果为空,只要走的是范围扫描 + 非唯一索引,InnoDB 仍会加间隙锁(例如 SELECT * FROM t WHERE id > 100 FOR UPDATE,而表中最大 id 是 99)

唯一键锁(Record Lock + Gap Lock 组合)为什么 有时不生效

当查询条件命中唯一索引(含主键)的 ** 等值匹配 ** 时,InnoDB 通常只加记录锁(Record Lock),不加间隙锁——这是优化,不是 bug。但这个“只锁记录”的行为有严格前提:

  • 必须是 = 查询,不能是 >=BETWEEN
  • 唯一索引列不能为 NULL(否则可能退化为间隙 + 记录组合)
  • 查询必须能通过索引直接定位到单条记录(即执行计划显示 type=consttype=eq_ref
  • 如果使用了 OR、函数、隐式类型转换(如 WHERE id = '123'id 是整型),可能导致索引失效,进而触发间隙锁

典型反例:

SELECT * FROM orders WHERE order_no = 'ORD-001' FOR UPDATE;

order_no 是唯一索引但定义为 VARCHAR,而传入参数是数字类型(如 123),MySQL 会做 隐式转换,导致无法使用索引,最终锁住整个范围。

如何验证当前语句加了哪种锁

最直接的方式是查 INFORMATION_SCHEMA.INNODB_TRXINFORMATION_SCHEMA.INNODB_LOCKS(MySQL 5.7)或 performance_schema.data_locks(MySQL 8.0+)。但更实用的是结合 SHOW ENGINE INNODB STATUSG 中的 TRANSACTIONS 部分观察锁信息。

关键识别点:

  • 输出中出现 gap before rec 表示间隙锁
  • 出现 record lock 且带具体主键值(如 n_fields=2; compact format; info bits 0; 0: len 4; hex 80000001; asc ;;)表示记录锁
  • 出现 supremum pseudo-record 表示锁住了索引末尾间隙(常被忽略,但极易引发死锁)
  • 若看到 lock_mode X locks gap before rec insert intention,说明另一个事务正尝试在该间隙插入——这是典型的插入冲突信号

唯一索引等值查询却发生死锁?重点检查这些地方

即使用了唯一索引等值查询,仍可能因以下原因卷入死锁:

  • 事务中先查后更新,但两次查询使用不同索引(如第一次用 email 唯一索引查用户,第二次用 user_id 更新订单),导致锁顺序不一致
  • 唯一索引字段存在重复 NULL 值(MySQL 中 NULL != NULL,唯一约束允许多个 NULL,此时 InnoDB 会将它们视作“相同间隙”处理)
  • 使用了 INSERT …… ON DUPLICATE KEY UPDATE:它先尝试插入,失败时转为更新,过程中可能先加插入意向锁(insert intention lock),再加记录锁,形成锁升级路径
  • 应用层重试逻辑未设超时或未捕获 Deadlock found when trying to get lock 错误码(1213),导致重试雪崩

真正难调试的,往往是那些看似“只锁一行”的语句,在并发插入场景下,因为间隙锁与插入意向锁的交互规则被绕开,实际锁住了比预期更大的范围。

text=ZqhQzanResources