MVCC到底是什么?这一篇博客就够啦
MVCC简单理解
需要改写的部分
MVCC全称Multi-Version Concurrency Control 即多版本并发控制 MVCC是一种并发控制方法 一般在数据库管理系统中实现对数据库的并发访问 在编程语言中实现事务内存 这一内容源自百度百科的标准解释。
转化成自己的语言:
交易一致性级别
MVCC的好处\作用
- MVCC在MySQL InnoDB中的主要实现目的是提升数据库的并发处理能力,并采用更加高效的方式解决读写冲突问题。
- 我们都知道,并发访问数据库会带来四类典型的并发问题(具体表现为脏写、脏读、不可重复读及幻读),而MVCC则通过最大限度地减少锁机制的应用来有效地规避这些潜在问题。
数据库的四种隔离级别
| 隔离界别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED:未提交读 | 可能发生 | 可能发生 | 可能发生 |
| READ COMMITTED:已提交读 | 解决 | 可能发生 | 可能发生 |
| REPEATABLE READ:可重复读 | 解决 | 解决 | 可能发生 |
| SERIALIZABLE:可串行化 | 解决 | 解决 | 解决 |
为何没有缺失干净的书写(clean write)?
四种问题按严重程度排序:干净的书写(clean write)> 脸红心跳(flush read)> 未被污染的阅读(unreadable read)> 幻读(幻读)。
干净的书写这一问题极为关键,在任何隔离级别下都不允许出现这种情况。
好!那下面就开始进入正题……
MVCC的实现原理
一、基于被隐藏的两个字段
在之前的博客文章[()中提到过,在InnoDB数据库中的Compacted行模式下存在三个被隐式的字段
| 列名 | 是否必须 | 描述 |
|---|---|---|
| row_id | 否 | 行ID,唯一标识一条记录(如果定义主键,它就没有啦) |
| transaction_id | 是 | 事务ID |
| roll_pointer | 是 | DB_ROLL_PTR是一个回滚指针,用于配合undo日志,指向上一个旧版本 |
二、版本链
假设初始添加一个数据。如图:

有两个事务同时进行更新信息,事务执行流程:

为什么会存在两个事务执行的顺序差异?
其实很简单,在某些情况下如果能够同时对同一个数据进行交叉修改
那不就是"脏写"不一致性问题吗?因为mysql在处理每个操作时都会为其加锁
所以当一个事务完成操作后
就会导致其他事务暂时无法进行
经过如此多轮次的数据更新后,并不会是一条数据 anymore吗?但实际情况并非如此

对该记录每次更新后, 都会将其旧值记录于一条undo日志中, 即成为该记录的一个旧版本. 随着更新次数的增多, 通过roll_pointer属性将这些旧版本连接成一个链表, 我们称这个链表为版本链接. 其头节点对应当前记录的最新值. 此外, 每个版本中还包含生成此版本时所涉及的事务id.
三、ReadView
那么什么是Read View?
简单来说,在数据库系统中进行快照读操作时会生成一种特殊的视图——读视图(Read View)。在该时刻(即该事务执行快照读操作的那个瞬间),数据库系统会生成一个当前快照,并记录所有活跃事务的ID(当每个事务开启时都会被分配一个唯一递增的ID),从而维护系统的最新活跃事务状态。
这种机制确保了在任何时刻都能快速获取数据库系统的最新数据状态。
其主要功能在于能够确定当前事务在版本链中所处的位置及其可见性。具体而言,该机制通过分析当前事务与各版本之间的关系来实现这一判定
其最重要的四个部分:
1、m_ids: 表示生成ReadView时当前系统中活跃读写的事务ID集合。
2、min_trx_id: 表示生成ReadView时当前系统活跃读写的事务ID中的最小值。
3、max_trx_id: 表示为分配下一个读写的事务ID预留的空间。
4、creator_trx_id: 表示创建该ReadView所使用的交易ID。
用ReadView判断哪个版本的数据可读的过程:
如果该版本的trx_id等于ReadView中的creator_trx_id,则说明当前事务可访问自身修改过的历史记录。
如果该版本的trx_id小于或等于ReadView中的min_trx_id,则表明生成该版本的事务已先于当前事务生成了ReadView。
如果该版本的trx_id大于ReadView中的max_trx_id,则表示生成该版本的事务是在当前事务生成ReadView之后提交的。
如果该版本的trx_id落在ReadView的min_trx_id与max_trx_id之间,则需进一步判断:当前事务生成时是否有活跃状态:
如果有活跃状态,则该版本不可被访问;
若无活跃状态,则该版本可被访问。
在READ COMMITTED(读取已提交) 和REPEATABLE READ(可重复读)两种隔离级别下生成ReadView的行为存在差异。接下来我们将详细探讨这两个隔离级别的特点及其区别。具体来说,在以下两种隔离级别下:一是READ COMMITTED(读取已提交),这种情况下会产生一种特殊的ReadView;二是REPEATABLE READ(可重复读),这种情况下则会产生另一种形式的ReadView。因此,在选择合适的隔离级别时需要根据具体的应用需求进行权衡。
READ COMMITTED(读取已提交)— 每次读取数据前都生成一个ReadView
为了全面掌握这一流程的关键环节(我在初步了解时往往匆匆掠过文字,在整体上并没有形成完整的认识;经过反复研读几遍后才逐渐弄清楚了每个步骤的作用和意义;现在终于能够清晰地理解整个流程的工作逻辑与协作关系!)
1、比方说现在系统里有两个事务id分别为100、200的事务在执行:
Transaction 100
BEGIN;UPDATE hero SET name = ‘关羽’ WHERE number = 1;
UPDATE hero SET name = ‘张飞’ WHERE number = 1;
Transaction 200
BEGIN;更新了一些别的表的记录 …
请注意此时尚未提交的操作。
当前时刻,“表hero中number为1的记录生成了版本历史信息列表。”

3、假设现在有一个使用READ COMMITTED隔离级别的事务开始执行:
BEGIN;
SELECT1:Transaction 100、200未提交
SELECT * FROM hero WHERE number = 1;
得到的列name的值为’刘备’
那这个select的语句能都读取到的数据就是我们最关心的啦!
过程:
在执行SELECT语句时会先生成一个ReadView,ReadView的m_ids列表的内容就是[100,
200],min_trx_id为100,max_trx_id为201,creator_trx_id为0。
然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列name的内容是’张飞’,该版本的trx_id值为100,在m_ids列表内,所以不符合可见性要求,根据roll_pointer跳到下一个版本。
下一个版本的列name的内容是’关羽’,该版本的trx_id值也为100,也在m_ids列表内,所以也不符合要求,继续跳到下一个版本。
下一个版本的列name的内容是’刘备’,该版本的trx_id值为80,小于ReadView中的min_trx_id值100,所以这个版本是符合要求的,最后返回给用户的版本就是这条列name为’刘备’的记录。
4、随后我们将事务ID为100的任务提交至系统中。
5、接着在完成所有任务后,在数据库表hero中将number字段设置为1的操作将在事务ID 200的任务范围内进行。
该系统已提交了第200笔交易,并已执行了更新操作。
该系统已成功地对其他表格中的数据项进行了相应的设置。
该系统已对角色信息字段(hero)中的名称字段进行设置为‘赵云’。
该系统已对角色信息字段(hero)中的名称字段进行设置为‘诸葛亮’。
此刻版本链是这样的:

接着采用reads committed的隔离级别进行查询(我们之前已经读取过一次数据,在这次操作和上一次操作之间属于同一事务的不同操作),如上。
BEGIN;
SELECT1:Transaction 100、200均未提交
SELECT * FROM hero WHERE number = 1;
得到的列name的值为’刘备’SELECT2:Transaction 100提交,Transaction 200未提交
SELECT * FROM hero WHERE
number = 1; # 得到的列name的值为’张飞’
7、这个SELECT2的执行过程如下:
- 执行SELECT语句时会自动创建一个ReadView对象。
- 该ReadView对象的m_ids列表内容即为[200](事务id为100的任务已提交完成),其min_trx_id值设定为200,默认max_trx_id值设为201,并将creator_trx_id字段赋值为空。
- 从版本链中选择可见记录时发现当前列名'诸葛亮'对应的trx_id值虽为200但仍在m_ids列表范围内因此不符合可见性要求需继续跳转至下一个版本。
- 在下一个版本中发现列名'O赵云'对应的trx_id值同样满足条件但仍需继续跳转至下一个版本。
- 最终当发现列名为'张飞'且其对应的trx_id值小于读视图中的min_trx_id值(即1号事务)时该条目满足条件并被选中返回给用户。
请仔细阅读以下内容:若理解了流程即可掌握要领,在以后遇到类似情况就不会有问题:
_每当发起一次查询时,在使用READ COMMITTED隔离级别进行操作时会创建独立的ReadView对象。
REPEATABLE READ —— 在第一次读取数据时生成一个ReadView
我们还用上面的一样的场景看一下,进行对比,区别就显而易见啦
在提交事务100后操作流程开始进行中(由于前面的操作与当前相同),同样会触发读取'刘备'这条数据(值得注意的是,在此之前已经创建了一个ReadView)。
接着在表hero中number字段值为1的位置进行一次更新操作,在事务ID 200的操作中完成。当前版本链条的情况如下:

然后接着,在刚才使用的REPEATABLE READ隔离级别的事务中继续查找该事务中的number为1的记录
使用REPEATABLE READ隔离级别的事务
BEGIN;SELECT1:Transaction 100、200均未提交
SELECT * FROM hero WHERE number = 1;
得到的列name的值为’刘备’SELECT2:Transaction 100提交,Transaction 200未提交
SELECT * FROM hero WHEREnumber = 1;
得到的列name的值仍为’刘备’
过程:
由于当前事务采用的是REPEATABLE READ隔离级别,在之前执行SELECT1操作时已成功创建了一个ReadView。因此可以直接使用前一次生成的ReadView,并查看其m_ids列表的内容为[100, 200]。
首先从版本链中筛选出可见记录,在图中可以看出最新版本的列name字段值为’诸葛亮’,其对应的trx_id值为200且存在于m_ids列表中。
不符合可见性要求的版本会根据roll_pointer跳转到下一个可用版本。
下一个版本的列name字段值是’赵云’且其trx_id值同样是200,在m_ids列表中存在该值。
同样不符合条件继续跳转至下一个版本。
接下来的列name字段值是’张飞’对应trx_id是100且也在m_ids列表内。
继续不满足条件直至下一个版本。
当处理到列name字段值是’关羽’时发现对应trx_id仅为88这一数值低于原先创建ReadView时所确定的min_trx_id(即100)。
最终符合条件并被返回给用户的记录来自trxAid为88这一版本中的列c=’刘备’的数据。
也就是说两次SELECT查询得到的结果是重复的
下面个人的理解:
ReadView即快照读取方式,在每一次执行快照读取操作时相当于对数据库进行了一次快照捕捉。捕捉到的数据内容可以直接获取,并且如果试图在快照中添加数据项(如'小狗'),则该'小狗'也会被包含在内;但如果其他人随后插入了新的数据记录(如往杯子加水),则这些新增内容将无法通过当前快照反映出来。为了区分不同版本的数据状态,在ReadView中我们设置了事务ID列表记录了你在操作前处于待命状态的所有会话实例。因此可以通过事务ID的时间戳来确定是否可读取特定版本的数据。
这段内容参考了《MySQL 是怎样运行的:从根儿上理解 MySQL》中的相关资料与图形分析。
