总结:
间隙锁 (Gap Lock)
MySQL InnoDB 如何通过 MVCC 和间隙锁防止幻读
在 MySQL 的 InnoDB 存储引擎中,REPEATABLE READ
隔离级别通过 MVCC (多版本并发控制) 和间隙锁 (Gap Lock) 的组合机制确实能够防止幻读问题。下面详细解释这一机制的工作原理:
1. MVCC (多版本并发控制)
MVCC 是 InnoDB 实现非锁定读的关键技术:
版本链:每行记录包含隐藏字段(DB_TRX_ID、DB_ROLL_PTR),形成版本链
ReadView:事务开启时创建一致性视图,决定能看到哪些版本的数据
快照读:普通 SELECT 使用 MVCC,读取事务开始时的数据快照
MVCC 如何部分解决幻读
在 REPEATABLE READ
级别下:
事务中的普通 SELECT 看到的是事务开始时的数据快照
即使其他事务插入了新数据,当前事务也看不到这些"幻影行"
但仅靠 MVCC 不能完全防止幻读,因为:
当前事务如果执行更新操作,会看到其他事务已提交的变更
可能导致"看到"之前查询不存在的行
2. 间隙锁 (Gap Lock)
InnoDB 在 REPEATABLE READ
级别自动使用间隙锁来完全防止幻读:
记录锁:锁定索引中的具体记录
间隙锁:锁定索引记录之间的间隙
next-key 锁:记录锁 + 间隙锁的组合
间隙锁如何工作
当执行以下操作时会获取间隙锁:
sql
SELECT * FROM table WHERE id > 100 FOR UPDATE;
锁定范围包括:
id=100 的记录(如果存在)
id > 100 的所有间隙
正无穷大的"上确界"
这样其他事务无法在锁定范围内插入新记录,从而防止幻读。
3. 实际防止幻读的机制组合
场景示例
事务A:
sql
START TRANSACTION; SELECT * FROM users WHERE age > 20; -- 第一次查询(快照读) -- 此时事务B插入 age=25 的新记录并提交 SELECT * FROM users WHERE age > 20; -- 第二次查询(快照读,结果不变) UPDATE users SET name='test' WHERE age > 20; -- 当前读,会看到事务B插入的记录 SELECT * FROM users WHERE age > 20; -- 当前读,现在能看到新记录(幻读出现?)
InnoDB 的解决方案
第一次 SELECT:快照读,使用 MVCC 看到一致性视图
UPDATE 语句:会尝试获取 age>20 条件的 next-key 锁
锁定所有 age>20 的已有记录和间隙
阻塞其他事务在 age>20 范围内的插入
UPDATE 执行:采用"当前读"看到最新数据(包括事务B插入的记录)
后续 SELECT:在同一个事务中,所有读取都会基于最新更新后的视图
关键点:在 UPDATE 执行时获取的 next-key 锁会阻塞其他事务的插入操作,从而保证在事务提交前不会有新数据插入到查询范围内,从根本上防止了幻读。
4. 特殊情况说明
没有索引的列:
如果没有合适的索引,InnoDB 会锁全表
性能极差但也能防止幻读
唯一索引:
对唯一索引的等值查询只使用记录锁
范围查询仍使用 next-key 锁
半一致性读:
某些情况下 UPDATE 语句可能采用更宽松的锁定策略
可通过设置关闭
5. 验证方法
可以通过以下实验验证:
会话A:
sql
START TRANSACTION; SELECT * FROM users WHERE age > 20 FOR UPDATE; -- 获取next-key锁
会话B:
sql
INSERT INTO users (age) VALUES (25); -- 会被阻塞
只有当会话A提交后,会话B的插入才能继续。
总结
InnoDB 在 REPEATABLE READ
级别防止幻读的完整机制是:
普通 SELECT 使用 MVCC 实现快照读
加锁的 SELECT (FOR UPDATE) 和写操作使用 next-key 锁
next-key 锁(记录锁+间隙锁)阻塞其他事务在查询范围内的插入
保证事务期间查询范围的稳定性
这种组合既保证了读取性能(非阻塞读),又通过精细的锁定机制解决了幻读问题,是 InnoDB 设计的一大亮点。