MVCC多版本并发控制的原理(Multi-Version Concurrency Control)

245 阅读5分钟

MVCC是多版本并发控制Multi-Version Concurrency Control的简称,是MySQL提供的一种机制或者说思想,并发就是指多个事务同时读写相同的数据,那么如果没有这种机制的话,像这种多个事务并发读写的情况会因为读写的冲突导致相率很低,而多版本就是为了解决这种冲突,通过给同一个数据提供不同的版本,然后不同的事务去读不同的版本减少这种冲突,提高并发效率。

多版本如何实现: InnoDB 里面每个事务有一个唯一的事务 ID,叫作 transaction id。它是在事务开始的时候向 InnoDB 的事务系统申请的,是按申请顺序严格递增的。每次事务更新数据的时候,都会生成一个新的数据版本,并且把 transaction id 赋值给这个数据版本的事务 ID,记为 row trx_id。同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它。也就是说,数据表中的一行记录,其实可能有多个版本 (row),每个版本有自己的 row trx_id。

下图是一个数据被多次更新后的状态,数据的row trx_id会在每次数据被更新后记录为更新这个数据的事务ID,并且每次更新之后都会保留更新前的那个版本,当然这里所说的保留并不一定是真的同时保留了多份数据,而是指我可以通过当前版本的数据以某种方式重新拿到以前版本的数据。

68d08d277a6f7926a41cc5541d3dfced.webp

从上图可以看到数据在被更新的过程其实就是在不断生成新的版本的过程。

如何找到之前版本的数据: 其实MySQL在数据被更新的时候会记录一种叫undo log,那么基于这种日志我们就可以找到数据的历史版本。上图中的U1,U2,U3就可以理解为undo log。

现在我们已经知道多版本的实现了,并且也知道如何找到不同版本的数据了,那么我们的事务怎么知道到底要读哪些版本的数据呢。其实MySQL底层已经做了规定。

事务要怎么知道读哪个版本的数据 简单来说就是事务会去记录当前活跃的事务ID,这里的活跃其实际就是指事务已经创建了,但是还没有提交。 然后当我们的事务去读取数据的时候,会将最新版本的数据的row trx_id和这些活跃ID进行对比,如果发现不能读取这个版本的数据,就在找到前一个版本的数据,再拿前一个版本的row trx_id和活跃ID对比,如此往复直到找到能够读取的数据版本为止。

那么对比的规则是怎么样的?

首先这些活跃事务ID会放到一个数组里面,数组里面事务 ID 的最小值记为低水位,当前系统里面已经创建过的事务 ID 的最大值加 1 记为高水位。 882114aaf55861832b4270d44507695e.webp 如果当前版本数据的row trx_id等于当前事务的ID,则说明这个版本是当前事务生成的,那么我们就可以读取这个版本的数据。

如果当前版本数据的row trx_id小于低水位,则说明在我们记录活跃事务ID时,这个事务已经提交了,那么我们就可以读取这个版本的数据。

如果当前版本数据的row trx_id处于低水位到高水位之间,有两种情况,如果在活跃的事务ID中,则说明在我们记录活跃事务ID时,这个事务并没有提交,那么就不能读取此版本的数据。如果不在活跃的事务ID中,则说明在我们记录活跃事务ID时,这个事务已经被提交了,就可以读取此版本的数据。

最后如果事务ID大于等于高水位,则说明在我们记录活跃事务ID时,这个事务还没开启,则不能读取这个版本的数据。

读已提交和可重复读 你会发现这怎么有点像事务隔离级别里面的读已提交或者可重复读啊,其实确实是这样的,基于这种机制,如果事务只在一开始记录一次活跃的事务ID,那么这个事务的隔离级别就是可重复读,如果一个事务在每次执行sql的时候会记录一次活跃的事务ID,那么这个事务的隔离级别其实就是读已提交。其实我们可以发现这个活跃的事务ID结合多版本的数据其实就是事务的一致性视图,就是我们说的读已提交或者可重复读隔离级别下事务开启会生成视图,然后事务接下来的读取都是基于此视图读取的,我们视图就是说的这个。因为读未提交读的都是最新的数据版本,串行化事务又是串行执行的,都没怎么用到多版本,所以这里也不讨论了。

本文参考:林晓斌老师的《MySQL实战45讲》