当前位置:主页 > 数据库 > Mysql >

MySQL多版本并发控制MVCC底层原理解析

时间:2023-02-20 09:45:47 | 栏目:Mysql | 点击:

1 事务并发中遇到的问题

1.1 脏读

当一个事务读取到了另外一个事务修改但未提交的数据,被称为脏读。

1.2 不可重复读

当事务内相同的记录被检索两次,且两次得到的结果不同时,此现象称为不可重复读。

1.3 幻读

当一个事务同样的查询条件查询两次(多次),查出的条数不一致称为幻读。

2 隔离级别

我们上边介绍了几种并发事务执行过程中可能遇到的一些问题,这些问题也有轻重缓急之分,我们给这些问题按照严重性来排一下序:

脏读 > 不可重复读 > 幻读

SQL 标准中规定,针对不同的隔离级别,并发事务可以发生不同严重程度的

问题,具体情况如下:

SQL 标准中规定,针对不同的隔离级别,并发事务可以发生不同严重程度的问题,具体情况如下:

3 版本链

我们知道,对于使用 InnoDB 存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列(row_id 并不是必要的,我们创建的表中有主键或者非 NULL的 UNIQUE 键时都不会包含 row_id 列):

trx_id: 每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务 id 赋值给 trx_id 隐藏列。

roll_pointer: 每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo 日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。

假设插入该记录的事务 id 为 80的记录,那么此刻该条记录的示意图如下所示:

假设之后两个事务 id 分别为 100、200 的事务对这条记录进行 UPDATE 操作,操作流程如下:

Trx 100:

UPDATE t_people SET name = '关羽' WHERE number = 1;
UPDATE t_people SET name = '张飞' WHERE number = 1;

Trx 200:

UPDATE t_people SET name = '赵云' WHERE number = 1;
UPDATE t_people SET name = '诸葛亮' WHERE number = 1;

每次对记录进行改动,都会记录一条 undo 日志,每条 undo 日志也都有一个 roll_pointer 属性(INSERT 操作对应的 undo 日志没有该属性,因为该记录并没有更早的版本),可以将这些 undo 日志都连起来,串成一个链表,所以现在的情况就像下图一样:

对该记录每次更新后,都会将旧值放到一条 undo 日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,我们把这个链表称之为版本链,版本链的头节点就是当前记录最新的值。另外,每个版本中还包含生成该版本时对应的事务 id。于是可以利用这个记录的版本链来控制并发事务访问相同记录的行为,那么这种机制就被称之为多版本并发控制(Mulit-Version Concurrency Control MVCC)

4 ReadView

4.1 ReadView 定义

InnoDB 提出了一个 ReadView 的概念,这个 ReadView 中主要包含 4个比较重要的内容:

4.2 访问控制

有了这个 ReadView,这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见:

4.3 再谈隔离

对于使用 READ UNCOMMITTED 隔离级别的事务来说,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好了。

对于使用 SERIALIZABLE 隔离级别的事务来说,InnoDB 使用加锁的方式来访问记录。

在 MySQL 中,READ COMMITTED 和 REPEATABLE READ 隔离级别的的一个非常大的区别就是它们生成 ReadView 的时机不同。

4.3.1 READ COMMITTED(读已提交)

读已提交,每次读取数据前都生成一个 ReadView。

假设现在有一个使用 READ COMMITTED 隔离级别的事务开始执行:

详解查询:

#使用 READ COMMITTED 隔离级别的事务
#Transaction 100、200未提交,得到的列 name 的值为 刘备
SELECT name FROM t_people WHERE number = 1;  

这个 SELECET 的执行过程如下:

不可重复读: 100事务、200事务开启读取到name都为刘备。当100事务提交时,由于是读已提交事务隔离级别,每次读取都会创建ReadView,200事务读取时,创建生成的ReadView m_ids 为 [200],这时根据读取规则读取到的name就为张飞。

# 使用 READ COMMITTED 隔离级别的事务
BEGIN;
# SELECE1:Transaction 100、200 均未提交,得到name值为刘备
SELECT name  FROM t_people WHERE number = 1; 

# SELECE2:Transaction 100 提交,Transaction 200 未提交
#Transaction 200 事务查询,得到name值为张飞,发生不可重复读。
SELECT name  FROM teacher WHERE number = 1; 

4.3.2 REPEATABLE READ(可重读)

可重读,在第一次读取数据时生成一个 ReadView。

解决不可重复读: 100事务、200事务开启,创建ReadView,m_ids 为[100,200],读取到name都为刘备。当100事务提交时,由于是可重读事务隔离级别,只创建一次ReadView,m_ids 仍然是[100,200],这时根据读取规则读取到的name仍然是刘备。

5 幻读

当一个事务同样的查询条件查询两次(多次),查出的条数不一致称为幻读。

在 REPEATABLE READ 隔离级别下的事务 T1 先根据某个搜索条件读取到多条记录,然后事务 T2 插入一条符合相应搜索条件的记录并提交,然后事务 T1 再根据相同搜索条件执行查询。结果会是什么?按照 ReadView 中的比较规则,不管事务 T2 比事务 T1 是否先开启,事务 T1 都是看不到 T2 的提交的。但是,在 REPEATABLE READ 隔离级别下 InnoDB 中的 MVCC 可以很大程度地避免幻读现象,而不是完全禁止幻读。

#SELECT:快照读。update:当前读。
REPEATABLE READ 可以解决快照读幻读问题。解决不了当前读幻读的问题。

案例:

1 执行begin,执行select *。

2 开启另一个窗口 执行insert、select、commit。

3 回到原窗口执行查询

4 执行 update 、提交

5 查找

6 总结

从上边的描述中我们可以看出来,所谓的 MVCC(Multi-Version ConcurrencyControl ,多版本并发控制)指的就是在使用 READ COMMITTD、REPEATABLE READ这两种隔离级别的事务在执行普通的 SELECT 操作时访问记录的版本链的过程,这样子可以使不同事务的读-写、写-读操作并发执行,从而提升系统性能。

READ COMMITTD、REPEATABLE READ 这两个隔离级别的一个很大不同就是,生成 ReadView 的时机不同,READ COMMITTD 在每一次进行普通 SELECT 操作前都会生成一个 ReadView,而 REPEATABLE READ 只在第一次进行普通 SELECT 操作前生成一个 ReadView,之后的查询操作都重复使用这个 ReadView 就好了,从而基本上可以避免幻读现象。

您可能感兴趣的文章:

相关文章