MVCC通过维护数据多版本和读视图机制,在InnoDB中实现非阻塞读,提升并发性能。它利用undo log存储历史版本,结合事务ID和回滚指针判断数据可见性,避免脏读与不可重复读。该机制在READ COMMITTED和REPEATABLE READ隔离级别下发挥作用,减少读写冲突,而在READ UNCOMMITTED中被绕过,SERIALIZABLE中被锁机制替代。
MVCC在MySQL中,尤其是在InnoDB存储引擎里,本质上是一种非阻塞的读操作机制。它通过维护同一行数据的多个版本,让读操作可以读取到事务开始时的数据快照,而写操作则可以同时进行,互不干扰。这样一来,就极大地提升了数据库的并发性能,减少了读写之间的锁竞争,让用户体验到更流畅的数据访问。说白了,它让数据库在处理大量并发请求时,能像一个高效的多任务处理者,而不是一个排队等候的单线程程序。
要理解MVCC是如何在MySQL中工作的,我们需要深入到InnoDB的一些核心机制。在我看来,这套设计精妙而实用。
当一个事务对某行数据进行修改(UPDATE或DELETE)时,InnoDB并不会直接覆盖或删除原始数据。相反,它会做几件事:
DB_TRX_ID),记录了是哪个事务创建或最后修改了它。
DB_ROLL_PTR)。这个指针指向了
undo log中的一个记录,该记录存储了当前行版本修改前的旧数据。通过这个指针,我们可以沿着链条追溯到这行数据的所有历史版本。
undo log不仅用于事务回滚,更是MVCC实现多版本并发控制的核心。每当数据被修改,旧的数据值就会被写入
undo log。这样,即使数据被更新了,其他事务仍然可以通过
undo log找到并读取到它们需要看到的旧版本数据。

当一个
SELECT语句(在特定的隔离级别下)被执行时,它会获得一个“读视图”(Read View),这可以理解为数据库在那个时间点的一个“快照”。这个读视图包含了当前所有活跃的事务ID列表。然后,InnoDB会根据以下规则来判断哪个行版本对当前的
SELECT事务是可见的:
DB_TRX_ID小于读视图中最早的活跃事务ID,这意味着这个版本是在读视图创建之前就已经提交的,所以它是可见的。
DB_TRX_ID大于读视图中最大的活跃事务ID,那么这个版本是在读视图创建之后才启动的事务修改的,所以它是不可见的。
DB_TRX_ID在读视图的活跃事务ID列表中,那么这个版本是由一个尚未提交的事务修改的,同样不可见(除非是当前事务自己修改的)。
DB_ROLL_PTR指向的
undo log链条,不断回溯,直到找到一个对当前读视图可见的旧版本。
通过这种机制,读事务总能看到一个一致的数据快照,而写事务则可以自由地修改数据,两者之间几乎没有阻塞,极大地提升了数据库的并发处理能力。
MVCC的引入,在我看来,是数据库并发控制领域的一个里程碑,它巧妙地解决了传统锁机制下的一些痛点。
MVCC解决的并发问题主要包括:
READ COMMITTED和
REPEATABLE READ隔离级别下,MVCC确保读事务不会看到其他未提交事务的修改。它总是提供已提交的数据版本,或者在
REPEATABLE READ下提供事务开始时的那个一致快照。
REPEATABLE READ隔离级别下,MVCC通过固定事务的读视图,保证了在一个事务的生命周期内,多次读取同一行数据会得到相同的结果,即便这期间有其他事务修改并提交了该行数据。
REPEATABLE READ级别下,MySQL结合了间隙锁(Gap Locks)和Next-Key Locks来进一步解决幻读问题。不过,单就MVCC而言,它主要关注的是已有行的版本一致性。
与传统锁机制的不同:
在我看来,MVCC的出现,让数据库的并发控制从“排队等候”的模式,转向了“各取所需”的模式,这无疑是其魅力所在。
在MVCC的舞台上,
undo log绝对是一个不可或缺的幕后英雄,它的作用远不止我们字面上理解的“撤销”那么简单。我个人认为,没有
undo log,MVCC的诸多精妙设计都将无从谈起。
Undo Log在MVCC中的核心角色包括:
undo log最直接也是最重要的功能。每当InnoDB中的一行数据被修改(
UPDATE或
DELETE),原始数据(修改前的值)并不会立即被丢弃,而是会被记录到
undo log中。这些被记录下来的旧数据,就构成了行数据的“历史版本”。当一个事务需要读取某个行数据,但当前最新的版本对它不可见时(比如,最新版本是由一个尚未提交的事务修改的,或者是在当前事务启动之后才提交的),数据库就会沿着
DB_ROLL_PTR(回滚指针)指向的
undo log链条,回溯到
undo log中存储的旧版本,直到找到一个对当前事务的读视图可见的版本。
undo log的本职工作——提供事务回滚的能力——在MVCC中也同样重要。如果一个事务执行到一半需要撤销所有操作(
ROLLBACK),数据库就会利用
undo log中记录的信息,将所有修改过的数据恢复到事务开始前的状态。这保证了事务的原子性。
undo log是实现MVCC“一致性读”的关键。一致性读意味着一个事务在读取数据时,会看到一个在它事务开始时就已经存在的数据快照,即使其他事务在这期间修改了数据并提交了。如果当前数据版本不符合这个快照要求,
undo log就提供了追溯历史版本的能力,让读事务总能看到符合其隔离级别和读视图要求的数据。
undo log记录的数据并不会永久保存。当一个
undo log记录不再被任何活跃事务引用(即,没有事务需要读取这个旧版本,也没有事务需要回滚到这个状态),它就可以被清除以回收空间。这个清理过程通常由一个后台的
Purge线程完成。
undo log的存在,为数据库判断何时可以安全地删除旧版本数据提供了明确的依据。
可以说,
undo log就像一个时光机,它存储了数据的“过去”,让不同的事务可以根据自己的时间线去查看相应的数据版本。这使得读写操作能够和谐共存,是InnoDB高并发性能的秘密武器之一。
隔离级别和MVCC的关系,在我看来,是理解MySQL并发控制不可或缺的一环。MVCC并不是对所有隔离级别都“一视同仁”的,它的作用和效果会因隔离级别的不同而有显著差异。
主要受益于MVCC的隔离级别:
READ COMMITTED级别下,一个事务的每次
SELECT查询都会创建一个新的读视图(snapshot)。这意味着,如果其他事务在两次
SELECT查询之间提交了数据修改,当前事务的第二次
SELECT可能会看到这些新的提交。MVCC在这里的作用是,它确保了任何
SELECT查询都只能看到已提交的数据,避免了“脏读”。但由于每次
SELECT都有新的读视图,它无法避免“不可重复读”。
REPEATABLE READ级别下,一个事务的读视图是在事务开始时(第一次
SELECT语句执行时)创建的,并且在整个事务的生命周期内保持不变。这意味着,即使其他事务在这期间修改并提交了数据,当前事务的后续
SELECT查询仍然会看到事务开始时的那个一致快照。MVCC在这里彻底解决了“不可重复读”的问题。结合InnoDB的Next-Key Locks,它还能有效地防止“幻读”。
不适用或较少依赖MVCC来实现读一致性的隔离级别:
undo log来查找旧版本以提供一致性视图——就显得不那么重要了,或者说,它被刻意绕过了。数据库会直接读取最新的、可能尚未提交的数据版本,而不会去判断其可见性。显然,这牺牲了数据的一致性来换取最高的并发度(因为几乎不需要等待)。
SERIALIZABLE隔离级别,MySQL通常会通过对所有
SELECT语句自动添加共享锁(
LOCK IN SHARE MODE),或者在
SELECT语句中使用
FOR UPDATE或
FOR SHARE显式加锁。这意味着,读操作也会像写操作一样,通过加锁来阻塞其他事务的写操作,从而保证数据的一致性。在这种情况下,MVCC的多版本机制在提供读一致性方面的作用就被锁机制所取代了。虽然MVCC的底层机制可能仍然存在,但其核心的非阻塞读特性在这里并没有被用于实现隔离。它本质上回到了传统的锁机制来确保串行化。
所以,我们可以看到,MVCC是InnoDB在
READ COMMITTED和
REPEATABLE READ这两个隔离级别下实现高并发和数据一致性的核心技术。而在
READ UNCOMMITTED,它被“忽略”;在
SERIALIZABLE,它被更严格的锁机制所“替代”,以达到更高的隔离要求。理解这一点,对于我们选择合适的隔离级别,优化数据库性能至关重要。