昨天晚上了解了关于数据库事务的知识,现在我们讲MVCC机制。
简介
MVCC(Multi-Version Concurrency Control)是多版本并发控制,以乐观锁为理论基础。通过对数据行的多个版本管理来实现数据库的并发控制。这样我们就可以通过比较版本号决定数据是否显示出来,读取数据的时候不需要加锁也可以保证事务的隔离效果,以此提高数据库并发性能。
MVCC 没有固定的实现规范,不同数据库一般会有不同的实现方式。
MySQL 中 InnoDB 采用了 MVCC 来实现“读已提交“和“可重复读”两个隔离级别。其他两个隔离级别和 MVCC 不兼容,因为“读未提交”总是读取最新的数据行,不需要进行版本控制,而“串行化”则会对所有读取的行加锁。
上面是一般能查到的官方解释,太干了,我们下面会举一些例子对这个mvcc里面最重要的两个部分:版本链和读视图,进行更好的了解mvcc
原理
MVCC 的核心实现主要基于两部分:版本链和读视图。
为了方便描述,首先我们创建一个表 goods(商品表),就三个字段,分别是主键 goods_id(商品id),名称 goods_name (商品名称)和stock(库存)。然后向表中插入一些数据:
INSERT INTO book VALUES(1, '辣条', 100);
INSERT INTO book VALUES(2, '可乐', 100);
INSERT INTO book VALUES(3, '薯片', 100);
版本链
对于使用 InnoDB 存储引擎的表,其聚簇索引记录包含了两个重要的隐藏列:
事务ID(DB_TRX_ID):每当事务对聚簇索引中的记录进行修改时,都会把当前事务的 id 记录到 DB_TRX_ID 中。
回滚指针(DB_ROLL_PTR):每当事务修改聚簇索引中的记录时,都会把该记录的旧版本写到 undo 日志,通过 DB_ROLL_PTR 指针可以获取该记录的旧版本。
如果一个事务多次修改记录,则每次修改都会生成 undo 日志,并且这些 undo 日志通过 DB_ROLL_PTR 指针串联成一个版本链。版本链的头结点是该记录的最新值,尾结点是事务开始时的初始值。
也就是一个是事务id,另一个是指向过去版本的指针
例如,我们在表 goods中做以下修改:
BEGIN;
UPDATE goods SET stock = 200 WHERE id = 1;
UPDATE goods SET stock = 300 WHERE id = 1;
COMMIT;
我们简单画一下目前的一个版本连:
读视图
接下来了解下读视图,读视图(Read View)是实现 MVCC 的关键部分,用于管理事务的可见性,以确保每个事务在读取数据时只看到在事务开始之前已经提交的数据版本。
「读已提交」和「可重复读」的区别就在于它们生成 Read View 的策略不同。
其实就相当于数据库的一个快照,它保存了数据库在某个时刻的数据信息,并根据事务的隔离级别决定在某个事务开始时,该事务能看到什么信息。“读已提交"是在每个语句执行前都会重新生成一个 Read View,而“可重复读“是启动事务时生成一个 Read View,然后整个事务期间都在用这个 Read View。
这里需要了解四个重要的read-view的参数
- creator_trx_id :创建读视图的事务 id。
- m_ids :创建读视图时,当前数据库中「活跃事务」的事务 id 列表。注意是一个列表,活跃事务指启动了但还没提交的事务。
- min_trx_id :创建读视图时,当前数据库中「活跃事务」中事务 id 最小的事务,也就是 m_ids 的最小值。
- max_trx_id :这个并不是 m_ids 的最大值,而是创建读视图时当前数据库应该分配给下一个事务的 id 值,也就是全局事务中最大事务 id 值 + 1。
怎么说呢,在这里试着画个图进行理解,在创建读视图后,我们可以将记录中的 trx_id 划分为三种情况
一个事务去访问记录的时候,除了自己的更新记录总是可见之外,还有这几种情况:
(1)如果记录的 trx_id 小于 min_trx_id,表示这个版本的记录是在创建 Read View 由已经提交的事务生成,所以该版本的记录对当前事务可见。
(2)如果记录的 trx_id 大于等于 max_trx_id,表示这个版本的记录是在创建 Read View 后才启动的事务生成的,所以该版本的记录对当前事务不可见。
(3)如果记录的 trx_id 在 Read View 的 min_trx_id 和 max_trx_id 之间,需要判断 trx_id 是否在 m_ids 列表中:
如果记录的 trx_id 在 m_ids 列表中,表示生成该版本记录的事务依然活跃着(还没提交事务)。
如果记录的 trx_id 不在 m_ids 列表中,表示生成该版本记录的事务已经被提交。
这种通过「版本链」来「读视图」控制事务并发访问同一个记录的行为就叫 MVCC(多版本并发控制)。
我们还是继续举个例子往下继续说
假设我们有一个数据库表,其中包含了多行数据,并且有两个事务T1和T2正在并发执行。
- 事务T1开始:T1需要读取表中的某些数据。当T1开始时,数据库会为其生成一个读视图。这个读视图会记录当前活跃的事务列表(包括T1本身),以及系统当前的最大事务ID(假设为trx_id_max)。
- 事务T2修改数据:在T1读取数据的同时,T2修改了表中的某些行。T2的修改操作会为这些行创建新的版本,并将旧版本保存在undo log中。这些新版本的数据行会带有T2的事务ID。
- 事务T1再次读取数据:T1需要再次读取之前读取过的数据。此时,T1会使用之前生成的读视图来判断哪些数据版本是可见的。
读视图的判断逻辑:
对于任何数据行,如果它的版本的事务ID小于读视图中的最小活跃事务ID,那么这个版本对T1是可见的,因为生成这个版本的事务在T1开始之前已经提交。
如果数据行的版本的事务ID等于读视图中的任何一个活跃事务ID,那么这个版本对T1是不可见的,因为这个事务还在活跃状态,可能还未提交。
如果数据行的版本的事务ID大于读视图中的最大事务ID(trx_id_max),那么这个版本也不可见,因为它是在T1开始之后生成的。
通过读视图的判断,T1能够确定哪些数据版本对它来说是“可见”的,并据此读取数据。这样,即使T2在T1读取数据的过程中修改了数据,T1仍然能够读取到它开始事务时一致的数据版本,从而保证了事务的隔离性。
然后「读已提交」隔离级别是在每次读取数据时,都会生成一个新的 Read View。「可重复读」隔离级别是启动事务时生成一个 Read View,然后整个事务期间都在用这个 Read View。