文章目录

MySQL InnoDB的间隙锁在范围查询中如何阻塞幻读

发布于 2026-06-03 18:48:25 · 浏览 21 次 · 评论 0 条

MySQL InnoDB的间隙锁在范围查询中如何阻塞幻读

在并发的数据库事务中,“幻读”指一个事务在前后两次按相同条件查询时,第二次查询看到了第一次查询未返回的其他事务插入的新行,仿佛出现了“幻影”。MySQL InnoDB 存储引擎在 REPEATABLE READ(可重复读)隔离级别下,通过一种名为间隙锁的机制来防止幻读。本文将通过一个可复现的实验,手把手演示间隙锁在范围查询中是如何阻塞幻读的。


阶段一:理解基本概念与准备实验环境

幻读:事务A查询ID大于10的记录,得到2条。此时事务B插入一条ID为11的记录并提交。事务A再次执行相同查询,看到了3条记录,多出来的这条就是“幻影行”。

间隙锁:InnoDB 不仅锁住查询匹配的现有记录(记录锁),还会锁住这些记录之间的“间隙”,防止其他事务在间隙中插入新数据。这是实现可重复读的关键。

准备实验环境:我们需要一个运行中的 MySQL 服务器。建议使用两个独立的客户端连接(例如两个终端窗口),分别模拟两个并发事务。

  1. 创建测试数据库和表。在一个客户端中执行以下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, 正无穷)

  1. 确认事务隔离级别。InnoDB 默认的隔离级别是 REPEATABLE READ,这正是我们需要的。可以在任一客户端执行 SELECT @@transaction_isolation; 确认。

阶段二:演示没有间隙锁(或锁失效)时如何发生幻读

为了理解间隙锁的作用,我们先看一个错误的假设:如果 InnoDB 只锁记录,不锁间隙,会发生什么?

客户端A(事务1)操作

  1. 开始一个事务:BEGIN;
  2. 查询所有 id > 10 的记录:SELECT * FROM test WHERE id > 10;
    • 此时,客户端A看到的结果是 id=15id=25 两条记录。它假设只要自己的事务不结束,这个结果集就是稳定的。

客户端B(事务2)操作

  1. 开始一个事务:BEGIN;
  2. 插入一条新记录:INSERT INTO test (id, value) VALUES (11, 'Eleven');
  3. 提交事务:COMMIT;
    • 此时,新记录 (11, 'Eleven') 已被持久化到表中,位于 (5,15) 的间隙内。

客户端A(事务1)再次查询

  1. 再次执行相同的查询:SELECT * FROM test WHERE id > 10;
  2. 观察结果:现在,客户端A看到了三条记录:id=11, 15, 25id=11 就是出现的“幻读”行。

如果发生上述过程,就说明数据库的隔离机制失效了。InnoDB 通过间隙锁来阻止客户端B在第2步的插入操作。


阶段三:演示间隙锁如何阻塞幻读

现在,我们重新进行实验,让 InnoDB 的间隙锁机制生效。

客户端A(事务1)操作

  1. 开始一个事务:BEGIN;
  2. 使用带有锁的查询。为了观察间隙锁,我们使用一个会主动请求锁的查询语句。执行:
    SELECT * FROM test WHERE id > 10 FOR UPDATE;
    • FOR UPDATE 语句除了对匹配的记录 (id=15, 25) 加上记录锁,还会根据查询条件 (id > 10) 对满足条件的记录之间的所有间隙加上间隙锁
    • 具体来说,它会锁住 (5, 正无穷) 这个大间隙,因为 id=15id=25 都大于10,且 id=5 小于10是边界。这个锁覆盖了 (5,15)(15,25) 以及 (25,正无穷) 三个间隙。本质上,它防止了任何 id > 10 的新记录被插入。

客户端B(事务2)尝试插入

  1. 开始一个事务:BEGIN;
  2. 尝试插入位于间隙中的记录:INSERT INTO test (id, value) VALUES (11, 'Eleven');
  3. 观察现象:这条 INSERT 语句不会立即执行成功,它会被阻塞(挂起)。客户端B的窗口会看起来像卡住了。
    • 这是因为客户端A持有的间隙锁 (5, 正无穷) 阻止了任何在 id > 10 范围内的插入操作,而 id=11 正好在这个范围内。

验证锁的状态(可选步骤)

  1. 打开第三个MySQL客户端。
  2. 查询当前的锁信息:
    SELECT * FROM performance_schema.data_locks;
  3. 在结果中寻找。你应该能看到客户端A的事务持有一个或多个 LOCK_GAP 类型的锁,范围覆盖了我们关心的区间。同时,能看到客户端B的事务在等待一个 LOCK_INSERT_INTENTION 类型的锁(插入意向锁),它正在等待间隙锁释放。

清理与完成实验

  1. 客户端A中,提交事务以释放锁:COMMIT;
  2. 此时,客户端B之前被阻塞的 INSERT 语句会立即执行成功。因为间隙锁已释放,插入操作得以进行。
  3. 你可以在客户端B中看到 INSERT 完成,然后提交COMMIT;

阶段四:核心原理总结

通过上述实验,我们可以清晰地看到间隙锁的工作机制:

  1. 锁定范围:当一个事务执行范围查询(如 WHERE id > 10)并请求锁(通过 SELECT ... FOR UPDATE, LOCK IN SHARE MODE,或者在当前读操作下)时,InnoDB 不仅锁住符合条件的现有记录(记录锁),还会锁住这些记录之间的间隙
  2. 阻塞插入:任何其他事务尝试向这些被锁定的间隙中插入新记录时,其操作会被阻塞,直到第一个事务提交或回滚释放锁。
  3. 防止幻读:因为插入被阻止,所以在第一个事务的两次相同查询之间,不可能有满足条件的新行“突然出现”,从而避免了幻读

重要注意事项

  • 间隙锁只在 REPEATABLE READ 及更高的隔离级别下生效。
  • READ COMMITTED 隔离级别下,没有间隙锁,因此可能发生幻读。
  • 并非所有查询都会触发间隙锁。普通的 SELECT ... (快照读) 不会触发任何锁。间隙锁主要由当前读操作触发。
  • 间隙锁的设计理念是以牺牲一定的并发插入性能为代价,来换取更高的数据一致性保障。在索引上,间隙锁通常发生在唯一索引的范围查询或非唯一索引的等值查询中。

理解间隙锁是掌握MySQL事务并发控制的关键一步。它像一个“防护栏”,守护着查询结果在事务期间的稳定性。

评论 (0)

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

扫一扫,手机查看

扫描上方二维码,在手机上查看本文