数据库面试中,有一个高频却容易答错的问题:
InnoDB 的加锁到底是锁“行”还是锁“索引”?
很多同学会回答:锁的是行。
但实际上——
InnoDB 的锁是加在索引上的,不是行上的!如果表没有索引,加锁范围会变得不可控甚至锁全表。
为什么?
InnoDB 到底锁了什么?
为什么范围查询会锁一大片?
为什么没索引会死锁频发?
一、核心结论:InnoDB 的锁加在索引上
你必须先记住一句话:
InnoDB 的所有行锁,本质上都是“索引项锁”。
如果没有可用索引,InnoDB 会进行全表扫描,并对所有扫描到的数据加锁。
也就是说:
- 查询命中了索引 → 锁的是索引项
- 查询没命中索引 → 会锁住全表对应所有索引项(相当于锁全表)
所以 InnoDB 并不存在“按行锁数据页”这种操作,而是通过“索引结构”来定位数据行并加锁。这也是为什么 数据库/中间件 优化中,索引设计至关重要。
二、底层原理:为什么锁是加在索引上的?
核心原因:InnoDB 是索引组织表 (Index Organized Table, IOT)。
InnoDB 的数据本身就是按照主键索引(B+Tree)存储的。
这意味着:
- 主键索引的叶子节点保存了完整的数据行。
- 其他二级索引的叶子节点保存的是对应记录的主键值。
因此,要锁住一行数据,必须先访问索引。
因为无论是查询、更新还是删除记录,都必须通过索引来定位。所以,所有的锁操作最终都以“索引项”为单位进行。
三、各类行锁的本质:都是索引锁
InnoDB 的行锁(本质是索引锁)主要包括:
- 记录锁 (Record Lock):锁定某一条具体的索引记录。
- 间隙锁 (Gap Lock):锁定两个索引值之间的空隙,防止插入。
- 临键锁 (Next-Key Lock):记录锁与间隙锁的组合,也是InnoDB默认的锁算法。
- 插入意向锁 (Insert Intention Lock):一种特殊的间隙锁,表示插入意图。
无论哪种锁,其作用对象都是索引项或索引间的间隙。
四、实例解析:不同索引下的加锁行为
表结构:
CREATE TABLE user (
id INT PRIMARY KEY,
age INT,
name VARCHAR(20),
KEY idx_age(age)
) ENGINE=InnoDB;
场景1:使用主键索引查询
SELECT * FROM user WHERE id = 10 FOR UPDATE;
锁的位置: 锁的是主键索引 B+Tree 上 id = 10 这一条索引项。它不会去锁name字段所在的“行”,加锁过程也无需访问二级索引。
场景2:使用二级索引查询
SELECT * FROM user WHERE age = 18 FOR UPDATE;
锁的位置:
- 首先,锁住二级索引
idx_age 中所有 age = 18 的索引项。
- 然后,根据这些索引项存储的主键值“回表”,到主键索引上锁住对应的记录。
这是一个“二级索引加锁 → 回表 → 主键索引加锁”的过程。
场景3:最危险的未命中索引查询
SELECT * FROM user WHERE name = 'Tom' FOR UPDATE;
如果 name 字段没有索引,会发生什么?
InnoDB 只能进行全表扫描。而全表扫描是沿着主键索引进行的,每扫描到一行,都会尝试对其主键索引项加锁,最终导致锁住全表主键索引。这正是索引缺失导致死锁频发、性能骤降的根本原因。
五、范围查询与Gap Lock原理
SQL:
SELECT * FROM user WHERE age BETWEEN 10 AND 20 FOR UPDATE;
假设索引 idx_age 中现有值:8, 12, 16, 25。
其加锁范围(Next-Key Lock)将是:
(8, 12], (12, 16], (16, 20]
解析:
- 记录锁:锁住索引值为12和16的记录。
- 间隙锁:锁住(8,12)、(12,16)、(16,20)这些索引区间。
- 临键锁:上述锁的组合,即锁住
(8,12]、(12,16]、(16,20]这几个“左开右闭”的区间。
这清晰地展示了锁是加在索引项及其间隙上的。当 MySQL 执行范围更新时,锁的范围可能远超你的预期。
六、DELETE/UPDATE为何锁更多?
以更新为例:
UPDATE user SET age = age + 1 WHERE age = 18;
其加锁流程比单纯SELECT更复杂:
- 在二级索引
idx_age 上,找到 age=18 的记录并加记录锁。
- 回表,对主键索引上的对应记录加锁。
- 因为修改了索引列的值,需要在旧的索引值前后加间隙锁,以防止其他事务插入新的
age=18的记录。
因此,写操作的加锁范围通常比读操作更大。
七、从“索引锁”原理出发的性能优化实践
理解锁在索引上这一本质后,可以指导我们进行有效优化:
-
务必为查询条件添加合适索引
这是根本。没有索引会导致全表扫描加锁,锁范围巨大,并发度极低,死锁概率飙升。
-
精确查询优于范围查询
在业务允许的情况下,使用 WHERE id = ? 比 WHERE age BETWEEN ? AND ? 更安全,因为它可能只需要一把记录锁,而无需间隙锁。
-
规范事务内的数据访问顺序
在高并发场景下,让所有事务都按相同的顺序(例如,按主键ID升序)访问记录,可以大幅降低死锁概率。
-
避免在高并发下执行无索引的DML操作
像 UPDATE user SET status = 1 WHERE name='Tom'; 这样的语句,在name无索引时是性能灾难,应坚决避免。
八、总结
InnoDB的行锁实质是索引锁,而非物理数据行锁。这源于其索引组织表的存储结构:数据存于主键索引,二级索引引用主键。所有行锁操作,包括记录锁、间隙锁、临键锁,最终都作用在索引项上。若SQL无法命中索引,引擎将退而对全表主键索引加锁,从而引发严重的锁竞争与性能问题。深刻理解 “索引锁” 这一核心概念,是进行高效 数据库优化 和并发程序设计的关键前提。