MVCC机制和间隙锁
MVCC机制:多版本并发控制,通过保留数据的多个版本来提高并发性能。隔离级别:可重复读
核心实现原理:
- 隐藏字段:给每行数据加了2个隐藏字段【最后一次修改该行的事务id和指向改行历史版本的回滚指针】
- undo log(回滚日志):记录了修改之前的数据内容【注意与RedoLog区别,一个是记录修改前,一个是记录修改后。回滚和持久化】
- Read View(读视图):ReadView是MVCC中用于定义事务可见性的机制。每个事务在开始时会生成一个ReadView,ReadView决定了哪些版本的数据对该事务可见。ReadView包含以下关键信息:
- m_ids:当前活跃(未提交)的事务ID集合。
- min_trx_id:活跃事务中的最小事务ID。
- max_trx_id:下一个将要分配的事务ID。
- creator_trx_id:创建该ReadView的事务ID。
- 如果数据版本的事务ID小于min_trx_id,则该版本对当前事务可见。
- 如果数据版本的事务ID在m_ids中,则该版本对当前事务不可见。
- 如果数据版本的事务ID大于max_trx_id,则该版本对当前事务不可见。
通过ReadView,事务可以判断某行数据的版本是否可见:
在Repeatable Read
隔离级别下,MVCC可以避免快照读中的幻读问题。然而,如果事务中存在当前读操作(如SELECT ... FOR UPDATE
),仅靠MVCC是不够的,还需要锁机制来防止其他事务插入新数据。
(1) 间隙锁(Gap Lock)
间隙锁锁定的是索引记录之间的“间隙”,防止其他事务在范围内插入新数据。例如,表中现有记录的age
值为[10, 20, 30]
,执行SELECT * FROM users WHERE age > 20 FOR UPDATE
时,InnoDB会锁定(20, +∞)
的区间。
(2) 临键锁(Next-Key Lock)
临键锁是**记录锁(Record Lock) + 间隙锁(Gap Lock)**的组合,锁定索引记录及其之前的间隙。例如,索引值为20
的记录,临键锁会锁定区间(-∞, 20]
。
SELECT * FROM Users WHERE age < 20 FOR UPDATE;
- 记录锁:锁定age = 10的记录。【有记录为10的行】
- 间隙锁:锁定age = 10和age = 20之间的间隙。
- 临键锁:锁定(-∞, 20)范围内的所有记录和间隙。
3. 如何解决幻读
当执行范围查询并请求共享或排他锁时(如SELECT ... FOR UPDATE
),InnoDB会给符合条件的已有数据记录的索引项加锁,同时也会对键值在条件范围内但并不存在的记录(即间隙)加锁。这样,其他事务就无法在这个范围内插入新的数据,从而避免了幻读问题。
例如:
- 事务A执行:SELECT * FROM users WHERE age > 20 FOR UPDATE,InnoDB会通过临键锁锁定age > 20的范围。
- 事务B尝试插入:INSERT INTO users (age) VALUES (25),该操作会被阻塞,直到事务A提交或回滚。
- 因此,事务A的两次查询结果一致,避免了幻读。
4. 注意事项
- 仅对当前读有效:MVCC的快照读可以避免幻读,但若事务中混合快照读和当前读,仍需显式加锁。
- 索引依赖:间隙锁和临键锁依赖于索引。若查询未使用索引,InnoDB会退化为表锁,严重影响性能。
- 隔离级别限制:在Read Committed隔离级别下,间隙锁会被禁用,无法完全避免幻读。
总结
InnoDB通过MVCC的快照读和临键锁的当前读双重机制,在Repeatable Read
隔离级别下解决了幻读问题:
- MVCC:保证快照读的一致性视图。
- 临键锁:通过锁定索引范围,阻止其他事务插入新数据。
理解这些机制有助于在实际开发中合理设计事务和查询逻辑,确保数据一致性并提升并发性能。