MySQL 中的 行锁(Record Lock) 和 间隙锁(Gap Lock)
1. 行锁(Record Lock)
定义
- Record Lock 是 InnoDB 在事务中对索引记录加的锁,用于保护某一行数据不被其他事务修改。
- 它是基于索引的锁,如果没有索引,InnoDB 会退化为表锁。
作用
- 防止其他事务修改或删除当前事务正在处理的行。
- 保证事务的隔离性(尤其是 REPEATABLE READ和SERIALIZABLE隔离级别)。
触发场景
- 常见于 SELECT ... FOR UPDATE或UPDATE、DELETE操作。
- 必须通过索引定位行,否则会锁住更多数据(甚至全表)。
例子
假设有表:
CREATE TABLE user ( id INT PRIMARY KEY, name VARCHAR(50), age INT ) ENGINE=InnoDB;事务 A:
BEGIN; SELECT * FROM user WHERE id=5 FOR UPDATE;- InnoDB 会在 id=5这一行的索引记录上加 Record Lock。
- 事务 B 如果执行:
UPDATE user SET age=30 WHERE id=5;会被阻塞,直到事务 A 提交或回滚。
2. 间隙锁(Gap Lock)
定义
- Gap Lock 是 InnoDB 在事务中对索引记录之间的间隙加的锁。
- 它锁住的是索引之间的空隙,而不是具体的行。
- 主要用于防止幻读(Phantom Read)。
作用
- 阻止其他事务在某个索引范围内插入新记录。
- 常见于范围查询(BETWEEN、>、<)或REPEATABLE READ隔离级别下的SELECT ... FOR UPDATE。
触发场景
- 范围查询并加锁时(FOR UPDATE或LOCK IN SHARE MODE)。
- 在 REPEATABLE READ下,InnoDB 会自动加 Gap Lock 来防止幻读。
例子
假设表中有数据:
id: 1, 5, 10
事务 A:
BEGIN; SELECT * FROM user WHERE id BETWEEN 5 AND 10 FOR UPDATE;- InnoDB 会对 id=5和id=10加 Record Lock。
- 同时会对 (5,10)之间的空隙加 Gap Lock。
- 事务 B 如果执行:
INSERT INTO user VALUES (7, 'Tom', 25);会被阻塞,因为 (5,10) 的间隙被锁住。
3. 区别总结
| 锁类型 | 锁住对象 | 作用 | 常见场景 | 
|---|---|---|---|
| Record Lock | 某一行的索引记录 | 防止该行被修改或删除 | 精确匹配索引的 UPDATE、DELETE、SELECT ... FOR UPDATE | 
| Gap Lock | 索引记录之间的空隙 | 防止在范围内插入新记录 | 范围查询加锁,防止幻读 | 
✅ 总结:
- Record Lock:锁住已有的行,防止修改或删除。
- Gap Lock:锁住索引间的空隙,防止插入新行。
- 两者经常一起出现,比如范围查询时会同时锁住已有行和间隙,这种组合锁叫 Next-Key Lock。
BEGIN;SELECT * FROM user WHERE id BETWEEN a AND b FOR UPDATE;如果a和b距离很大是否直接锁住a和b中间所有行
是的,
在 InnoDB 的 REPEATABLE READ 隔离级别下,执行
BEGIN; SELECT * FROM user WHERE id BETWEEN a AND b FOR UPDATE;会触发 Next-Key Lock(Record Lock + Gap Lock 的组合),它会锁住范围内所有匹配的行,并且锁住这些行之间的间隙,防止其他事务在这个范围内插入新数据。
1. 锁定范围的规则
假设 id 是主键索引,表中数据如下:
id: 1, 5, 10, 20, 50, 100
如果执行:
SELECT * FROM user WHERE id BETWEEN 5 AND 50 FOR UPDATE;InnoDB 会:
- 对 id=5、id=10、id=20、id=50加 Record Lock(行锁)
- 对 (5,10)、(10,20)、(20,50)这些间隙加 Gap Lock
- 还会对 (50, 下一条记录)的间隙加锁(防止插入 51、60 等)
2. 如果 a 和 b 很远
如果 a=5,b=1000000,而表中有很多行:
- 是的,InnoDB 会锁住 id=5到id=1000000范围内所有匹配的行和它们之间的间隙。
- 这意味着这个范围内的更新、删除、插入都会被阻塞,直到事务提交或回滚。
- 如果范围特别大,可能会造成大范围锁定,严重影响并发性能。
3. 注意事项
- 基于索引:行锁和间隙锁都是基于索引的,如果 id没有索引,InnoDB 会退化成表锁,锁住整张表。
- 范围越大,锁的行和间隙越多,并发冲突风险越高。
- 如果只想锁住已存在的行而不锁间隙,可以使用 READ COMMITTED隔离级别(Gap Lock 不会生效)。
4. 直观示意
假设:
id: 1, 5, 10, 20, 50, 100
执行:
SELECT * FROM user WHERE id BETWEEN 5 AND 50 FOR UPDATE;锁定范围:
[5]====(gap)====[10]====(gap)====[20]====(gap)====[50]====(gap)====
- [x] 表示 Record Lock(行锁)
- (gap) 表示 Gap Lock(间隙锁)
我建议,如果你的 a 和 b 之间范围特别大,可以考虑:
- 拆分查询,减少一次性锁定的范围
- 使用更精确的条件,避免锁住无关数据
- 降低隔离级别(如 READ COMMITTED)减少 Gap Lock
为什么5前面没有被锁住?
在执行
BEGIN; SELECT * FROM user WHERE id BETWEEN 5 AND 50 FOR UPDATE;的时候,为什么 id=5 前面的间隙(比如 (1,5))没有被锁住?
核心原因:锁定范围是由查询条件决定的
InnoDB 的 Next-Key Lock(行锁 + 间隙锁)只会锁住满足查询条件的记录以及这些记录之间的间隙。
- 查询条件是 id BETWEEN 5 AND 50
- 范围的起点是 id=5
- 所以它不会去锁 (1,5)这个间隙,因为这个间隙里的值(比如 2、3、4)不满足条件,不会影响当前事务的结果集。
锁定范围的精确规则
在 REPEATABLE READ 隔离级别下:
- Record Lock:锁住满足条件的记录(这里是 id=5, 10, 20, 50)。
- Gap Lock:锁住这些记录之间的间隙,以及最后一条记录到下一条记录之间的间隙。
- 不锁条件范围外的间隙,因为它们不会导致幻读。
例子对比
假设表数据:
id: 1, 5, 10, 20, 50, 100
查询:
SELECT * FROM user WHERE id BETWEEN 5 AND 50 FOR UPDATE;锁定范围:
[5]====(gap)====[10]====(gap)====[20]====(gap)====[50]====(gap)====
- [x]= Record Lock
- (gap)= Gap Lock
- (1,5)不锁,因为不在条件范围内。
如果是:
SELECT * FROM user WHERE id >= 5 FOR UPDATE;锁定范围:
复制代码
[5]====(gap)====[10]====(gap)====[20]====(gap)====[50]====(gap)====[100]====(gap)====
- 依然不会锁 (1,5),因为条件是>=5。
如果是:
SELECT * FROM user WHERE id > 1 FOR UPDATE;锁定范围:
(1,5)====[5]====(gap)====[10]====(gap)====[20]====(gap)====[50]====(gap)====[100]====(gap)====
- 这时 (1,5)会被锁,因为它在条件范围内(>1)。
✅ 总结:
- InnoDB 只锁条件范围内的记录和间隙,不会锁条件范围外的间隙。
- BETWEEN 5 AND 50的起点是 5,所以 5 前面的间隙不会被锁。
- 如果条件包含更小的值(比如 >1),那么 5 前面的间隙就会被锁。
