一、MVCC解决了什么问题?
MVCC 解决了数据库高并发场景下的两大核心问题:
-
读写阻塞:在传统的锁机制下,读操作可能会阻塞写操作,写操作也一定会阻塞读操作。当有大量读写操作并发时,数据库性能会急剧下降。
-
事务隔离的代价:为了实现高级别的事务隔离(如可重复读),需要对数据加锁,这严重影响了并发性能。
MVCC的核心理念是:通过创建数据快照,让读操作和写操作互不阻塞。读操作访问事务开始时的快照版本,写操作则创建新版本。 这使得在 REPEATABLE READ
隔离级别下,读操作无需加锁,极大地提升了数据库的并发处理能力。
二、MVCC的原理是什么?
MVCC的实现依赖于三个核心要素:
-
隐式字段:
-
DB_TRX_ID
:一个6字节的字段,表示最后一个对本行数据做修改的事务ID。 -
DB_ROLL_PTR
:一个7字节的字段,指向回滚段中的Undo Log记录,也就是指向该行数据的上一个历史版本。 -
DB_ROW_ID
:一个6字节的隐藏自增ID,如果表没有主键,InnoDB会基于此生成聚簇索引。
-
-
Undo Log:
-
当事务修改数据时,会将数据的旧版本拷贝到Undo Log中。
-
这个Undo Log通过
DB_ROLL_PTR
指针形成一个数据版本链。如上图所示,最新的数据V2指向旧版本V1。读请求根据规则遍历这个链,找到适合自己的数据版本。
-
-
Read View:
-
这是事务进行快照读操作时产生的读视图。在事务执行快照读的那一刻,会生成数据库系统当前的一个快照。
-
Read View主要用于做可见性判断,即决定当前事务能看到哪个版本的数据。
-
它主要包含:
-
m_ids
:生成Read View时,系统中活跃的(未提交的)事务ID列表。 -
min_trx_id
:活跃事务列表中的最小事务ID。 -
max_trx_id
:生成Read View时,系统应该分配给下一个事务的ID。 -
creator_trx_id
:创建该Read View的事务ID。
-
-
可见性判断规则:
沿着数据版本链,从最新版本开始检查,如果数据行的DB_TRX_ID
:
-
等于
creator_trx_id
? -> 可见。说明是本事务自己修改的。 -
小于
min_trx_id
? -> 可见。说明该版本在本次Read View创建前已提交。 -
大于等于
max_trx_id
? -> 不可见。说明该版本在本次Read View创建后才生成。 -
在
min_trx_id
和max_trx_id
之间,但不在m_ids
中? -> 可见。说明该版本对应的事务已提交。 -
在
min_trx_id
和max_trx_id
之间,且在m_ids
中? -> 不可见。说明该版本对应的事务仍活跃,未提交。
如果不可见,就顺着DB_ROLL_PTR
指针找到上一个版本,重复上述判断,直到找到可见的数据版本。
三、MySQL幻读解决了吗?
这是一个非常容易混淆的问题。结论是:
-
在MySQL的InnoDB引擎的
REPEATABLE READ
隔离级别下,通过MVCC + Next-Key Lock
的机制,已经解决了幻读问题。
什么是幻读?
一个事务在重新执行同一个查询时,返回了更多的行或者发现了之前不存在的行(由于其他事务的插入或删除操作)。
如何解决的?
-
对于快照读:普通的
SELECT ...
语句使用MVCC。因为事务始终读取的是它开始时的快照,所以其他事务的插入/删除操作对该事务是不可见的,从而防止了幻读。 -
对于当前读:像
SELECT ... FOR UPDATE
/SELECT ... LOCK IN SHARE MODE
/UPDATE
/DELETE
这类操作,它们读取的是数据的最新版本,并且需要加锁。这时,MVCC无法防止幻读。InnoDB通过引入 Next-Key Lock 来给扫描到的索引范围加锁,阻止其他事务在锁定范围内插入新数据,从而解决了当前读下的幻读问题。
所以,标准的MySQL InnoDB在 REPEATABLE READ
级别下,已经可以完全防止幻读。
四、间隙锁和临键锁解决了什么问题?原理是什么?
它们共同解决了 "当前读" 场景下的幻读问题。
1. 间隙锁
-
解决的问题:防止在某个索引记录的间隙中插入新数据,从而解决幻读。
-
锁定的目标:锁定一个索引记录之间的开区间。例如,表中存在id为1和5的记录,那么间隙锁可能锁定区间
(1, 5)
。 -
原理:当一个事务执行
SELECT * FROM table WHERE id BETWEEN 1 AND 10 FOR UPDATE
时,InnoDB不仅会锁住id=1和10的记录(如果存在),还会锁住(1,10)这个区间内的所有"间隙"。这阻止了任何其他事务在(1,10)的范围内插入新的id值,比如插入id=3或id=7,从而防止了幻读。
2. 临键锁
-
本质:临键锁是 记录锁 和 间隙锁 的结合。
-
锁定的目标:它会锁定索引记录本身以及该记录之前的间隙。
-
例如,如果数据库中有索引记录10, 11, 13。
-
那么临键锁可能锁定的区间是:
(-∞, 10]
,(10, 11]
,(11, 13]
,(13, +∞]
。
-
-
原理:当执行
SELECT * FROM table WHERE id > 10 FOR UPDATE
时,InnoDB会从找到的第一个大于10的记录(比如11)开始,对其加临键锁(锁住(10, 11]),然后继续向后加锁,直到最后一个记录,最后还会对最后一个记录之后的间隙(比如(13, +∞))加一个间隙锁。这样,任何想在锁定范围内插入新数据的操作都会被阻塞。
💎 总结
机制 | 核心解决问题 | 原理 |
---|---|---|
MVCC | 快照读的读写阻塞与不可重复读 | 通过Undo Log版本链和Read View实现多版本数据快照,读操作无需加锁。 |
间隙锁 | 当前读的幻读(防止插入) | 锁定索引记录之间的空隙,阻止其他事务在范围内插入新数据。 |
临键锁 | 当前读的幻读(防止插入和修改) | 记录锁 + 间隙锁,锁定记录本身及之前的间隙,是InnoDB默认的行锁算法。 |
它们协同工作,使得MySQL InnoDB在 REPEATABLE READ
隔离级别下,既保证了高并发性能(通过MVCC),又实现了严格的事务隔离,解决了脏读、不可重复读和幻读所有问题。