mysql中InnoDB的行锁机制与性能分析

9次阅读

InnoDB 行锁仅作用于索引列,无索引或无法走索引时会退化为全表扫描锁;联合索引需最左匹配才生效;临键锁(记录锁 + 间隙锁)是 RR 级别默认机制,易引发死锁与阻塞。

mysql 中 InnoDB 的行锁机制与性能分析

InnoDB 行锁只在索引列上生效

InnoDB 的行级锁(RECORD LOCK)本质是加在索引记录上的,不是加在数据行本身。如果 WHERE 条件中用的列没有索引,或者用了无法走索引的表达式(比如 WHERE name LIKE '%abc'),InnoDB 会退化为锁全表(实际是锁所有扫描到的索引记录,可能等效于表锁)。

常见错误现象:UPDATE users SET status = 1 WHERE phone = '138……' 执行很慢、并发更新阻塞严重——先查 SHOW INDEX FROM users,确认 phone 是否有单列索引或作为联合索引最左前缀。

  • 联合索引 (a, b, c) 只能支持 WHERE a = ?WHERE a = ? AND b = ? 等最左匹配的行锁
  • WHERE b = ? 即使 b 在联合索引中,也不会走索引,大概率触发间隙锁 + 临键锁的全范围扫描锁定
  • SELECT …… FOR UPDATEUPDATE/DELETE 语句若未命中任何记录,仍可能加 GAP LOCK(间隙锁),阻塞插入

临键锁(Next-Key Lock)是默认行为,也是死锁主因

InnoDB 默认事务隔离级别 REPEATABLE READ 下,行锁 = 记录锁 + 间隙锁 → 合称临键锁。它锁住的是「索引记录 + 该记录前的间隙」,目的是防止幻读。但这也意味着:即使你只更新一行,也可能锁住一大段索引区间。

典型死锁场景:

SESSION A: UPDATE t SET x = 1 WHERE id = 5; SESSION B: UPDATE t SET x = 2 WHERE id = 10; SESSION A: UPDATE t SET x = 3 WHERE id = 10;  -- 等待 B SESSION B: UPDATE t SET x = 4 WHERE id = 5;   -- 等待 A → 死锁

根本原因不是 id=5 和 id=10 冲突,而是两个事务在相同索引范围(比如主键 B+ 树的同一页面)内申请了重叠的临键锁。

  • SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX 查当前运行事务
  • SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS 查谁在等谁
  • 临时关闭临键锁:改为 READ COMMITTED 隔离级别(但会丢失可重复读语义,且仍可能有间隙锁)

隐式主键与二级索引锁的双重开销

即使表显式定义了 PRIMARY KEY,InnoDB 也会用它做聚簇索引;但如果查询走的是二级索引(比如 INDEX idx_email(email)),InnoDB 会先锁二级索引记录,再回表去聚簇索引找主键,此时会额外加一把对应主键记录的锁。

这意味着:一个 UPDATE …… WHERE email = ? 可能同时持有两把锁 —— 二级索引页上的记录锁 + 聚簇索引页上的记录锁。并发高时,锁竞争放大。

  • 避免在高频更新场景中,对非主键字段建过多二级索引
  • 如必须按 email 更新,考虑将 email 设为联合主键一部分(谨慎评估业务主键语义)
  • EXPLAIN FORMAT=tree 可看是否走了索引、是否发生回表

锁监控与诊断必须依赖 INFORMATION_SCHEMA 表

InnoDB 不提供类似 Oracle 的 v$lock 视图,所有实时锁信息都藏在 INFORMATION_SCHEMA 下的几张表里。靠 SHOW ENGINE INNODB STATUS 只能看到最近一次死锁,无法定位长期阻塞。

关键组合查询:

SELECT    trx_id, trx_state, trx_started, trx_query,   lock_trx_id, lock_mode, lock_type, lock_table, lock_index,   lock_data FROM INFORMATION_SCHEMA.INNODB_TRX t JOIN INFORMATION_SCHEMA.INNODB_LOCKS l ON t.trx_id = l.lock_trx_id JOIN INFORMATION_SCHEMA.INNODB_LOCK_WAITS w ON w.blocking_lock_id = l.lock_id;

注意:lock_data 显示被锁的具体索引值(如 5'alice@example.com'),这是判断锁粒度是否合理的直接依据。很多线上慢锁问题,就卡在没查这列,只看到“有锁”却不知道锁了哪几条记录。

真正难处理的,永远不是“有没有锁”,而是“为什么 这行被锁了却不在你的 WHERE 条件里”——往往是因为临键锁扩大了范围,或者唯一索引查找失败后退化为范围扫描。

text=ZqhQzanResources