MySQL InnoDB的间隙锁在范围查询中如何阻塞幻读
在并发的数据库事务中,“幻读”指一个事务在前后两次按相同条件查询时,第二次查询看到了第一次查询未返回的其他事务插入的新行,仿佛出现了“幻影”。MySQL InnoDB 存储引擎在 REPEATABLE READ(可重复读)隔离级别下,通过一种名为间隙锁的机制来防止幻读。本文将通过一个可复现的实验,手把手演示间隙锁在范围查询中是如何阻塞幻读的。
阶段一:理解基本概念与准备实验环境
幻读:事务A查询ID大于10的记录,得到2条。此时事务B插入一条ID为11的记录并提交。事务A再次执行相同查询,看到了3条记录,多出来的这条就是“幻影行”。
间隙锁:InnoDB 不仅锁住查询匹配的现有记录(记录锁),还会锁住这些记录之间的“间隙”,防止其他事务在间隙中插入新数据。这是实现可重复读的关键。
准备实验环境:我们需要一个运行中的 MySQL 服务器。建议使用两个独立的客户端连接(例如两个终端窗口),分别模拟两个并发事务。
- 创建测试数据库和表。在一个客户端中执行以下SQL。
CREATE DATABASE gap_lock_demo;
USE gap_lock_demo;
CREATE TABLE test (
id INT PRIMARY KEY,
value VARCHAR(20)
);
INSERT INTO test (id, value) VALUES (5, 'Five'), (15, 'Fifteen'), (25, 'Twenty-Five');
现在,表中存在 id 为 5, 15, 25 的三条记录。根据这些记录,InnoDB 会创建如下“间隙”:(负无穷, 5), (5, 15), (15, 25), (25, 正无穷)。
- 确认事务隔离级别。InnoDB 默认的隔离级别是
REPEATABLE READ,这正是我们需要的。可以在任一客户端执行SELECT @@transaction_isolation;确认。
阶段二:演示没有间隙锁(或锁失效)时如何发生幻读
为了理解间隙锁的作用,我们先看一个错误的假设:如果 InnoDB 只锁记录,不锁间隙,会发生什么?
客户端A(事务1)操作:
- 开始一个事务:
BEGIN; - 查询所有
id > 10的记录:SELECT * FROM test WHERE id > 10;- 此时,客户端A看到的结果是
id=15和id=25两条记录。它假设只要自己的事务不结束,这个结果集就是稳定的。
- 此时,客户端A看到的结果是
客户端B(事务2)操作:
- 开始一个事务:
BEGIN; - 插入一条新记录:
INSERT INTO test (id, value) VALUES (11, 'Eleven'); - 提交事务:
COMMIT;- 此时,新记录
(11, 'Eleven')已被持久化到表中,位于(5,15)的间隙内。
- 此时,新记录
客户端A(事务1)再次查询:
- 再次执行相同的查询:
SELECT * FROM test WHERE id > 10; - 观察结果:现在,客户端A看到了三条记录:
id=11, 15, 25。id=11就是出现的“幻读”行。
如果发生上述过程,就说明数据库的隔离机制失效了。InnoDB 通过间隙锁来阻止客户端B在第2步的插入操作。
阶段三:演示间隙锁如何阻塞幻读
现在,我们重新进行实验,让 InnoDB 的间隙锁机制生效。
客户端A(事务1)操作:
- 开始一个事务:
BEGIN; - 使用带有锁的查询。为了观察间隙锁,我们使用一个会主动请求锁的查询语句。执行:
SELECT * FROM test WHERE id > 10 FOR UPDATE;FOR UPDATE语句除了对匹配的记录 (id=15, 25) 加上记录锁,还会根据查询条件(id > 10)对满足条件的记录之间的所有间隙加上间隙锁。- 具体来说,它会锁住
(5, 正无穷)这个大间隙,因为id=15和id=25都大于10,且id=5小于10是边界。这个锁覆盖了(5,15)和(15,25)以及(25,正无穷)三个间隙。本质上,它防止了任何id > 10的新记录被插入。
客户端B(事务2)尝试插入:
- 开始一个事务:
BEGIN; - 尝试插入位于间隙中的记录:
INSERT INTO test (id, value) VALUES (11, 'Eleven'); - 观察现象:这条
INSERT语句不会立即执行成功,它会被阻塞(挂起)。客户端B的窗口会看起来像卡住了。- 这是因为客户端A持有的间隙锁
(5, 正无穷)阻止了任何在id > 10范围内的插入操作,而id=11正好在这个范围内。
- 这是因为客户端A持有的间隙锁
验证锁的状态(可选步骤):
- 打开第三个MySQL客户端。
- 查询当前的锁信息:
SELECT * FROM performance_schema.data_locks; - 在结果中寻找。你应该能看到客户端A的事务持有一个或多个
LOCK_GAP类型的锁,范围覆盖了我们关心的区间。同时,能看到客户端B的事务在等待一个LOCK_INSERT_INTENTION类型的锁(插入意向锁),它正在等待间隙锁释放。
清理与完成实验:
- 在客户端A中,提交事务以释放锁:
COMMIT; - 此时,客户端B之前被阻塞的
INSERT语句会立即执行成功。因为间隙锁已释放,插入操作得以进行。 - 你可以在客户端B中看到
INSERT完成,然后提交:COMMIT;
阶段四:核心原理总结
通过上述实验,我们可以清晰地看到间隙锁的工作机制:
- 锁定范围:当一个事务执行范围查询(如
WHERE id > 10)并请求锁(通过SELECT ... FOR UPDATE,LOCK IN SHARE MODE,或者在当前读操作下)时,InnoDB 不仅锁住符合条件的现有记录(记录锁),还会锁住这些记录之间的间隙。 - 阻塞插入:任何其他事务尝试向这些被锁定的间隙中插入新记录时,其操作会被阻塞,直到第一个事务提交或回滚释放锁。
- 防止幻读:因为插入被阻止,所以在第一个事务的两次相同查询之间,不可能有满足条件的新行“突然出现”,从而避免了幻读。
重要注意事项:
- 间隙锁只在
REPEATABLE READ及更高的隔离级别下生效。 READ COMMITTED隔离级别下,没有间隙锁,因此可能发生幻读。- 并非所有查询都会触发间隙锁。普通的
SELECT ...(快照读) 不会触发任何锁。间隙锁主要由当前读操作触发。 - 间隙锁的设计理念是以牺牲一定的并发插入性能为代价,来换取更高的数据一致性保障。在索引上,间隙锁通常发生在唯一索引的范围查询或非唯一索引的等值查询中。
理解间隙锁是掌握MySQL事务并发控制的关键一步。它像一个“防护栏”,守护着查询结果在事务期间的稳定性。

暂无评论,快来抢沙发吧!