MySQL(四)—MVCC实现可重复读的原理

文章目录

一、MVCC概况

MVCC是什么?MVCC即多版本控制协议,InnoDB实现了MVCC作版本控制,防止不该被当前事务看到的数据看到。
举个例子,下面就是在T4时刻,事务A和事务C看到的数据不一致,也就是说有多个版本。

事务时刻 事务A 事务B 事务C
T1 begin; begin; begin;
T2 select * from transation_test;

结果:id salary
1 1 | | |
| T3 | | 插入一行数据(2,2)
commit; | |
| T4 | select * from transation_test;
结果:id salary
1 1 ** ** | | select * from transation_test;
结果:id salary
1 1
2 2 |

引入MVCC的原因就是在读写事务时读的情况不加锁,提高并发性能。

二、MVCC实现原理

本质理解
在InnoDB中,主要是通过使用readview的技术来判断数据是否能当前事务读到。如果可以,则输出,否则就利用undolog来构建历史版本,再进行判断,直到记录构建到最老的版本或者可见性条件满足。

上面的工作需要两或三个隐藏字段、undo log、一个数组、ReadView完成。

1.两或三个隐藏字段。

InnoDB表数据组织方式是聚簇索引。在聚簇索引上还有一些额外信息会存储,就是两或三个隐藏字段。
DB_TRX_ID:事务ID,表示最近一次插入或者更新该记录的事务ID。
DB_ROLL_PTR:回滚指针,指向该记录的之前的undo log记录
DB_ROW_ID:当表上没有用户主键的时候,InnoDB会自动创建(这也是为什么两或三个隐藏字段)

假设一个表中有两个字段(ID,Name),一个事务id为1插入一条记录(1,事务1)。该条记录还没有上一版本,回滚指针为null。那么现在这张表就变成了下面的样子。
MySQL(四)—MVCC实现可重复读的原理

2.undo log

前面我们也介绍过undo log在innodb中有两个作用,MVCC、事务回滚。undo log主要存放一条sql语句执行前的记录及与sql语句相反的操作。

现在来一个事务要修改ID为1这一行记录,那么它的过程如下。

首先向InnoDB申请一个事务ID,注意事务ID是严格递增的,假如申请的ID为3,简称事务3。
事务3要对ID为1的记录做update修改操作,数据库为这行记录加上行锁。
将该行数据拷贝进undo log中
拷贝完成后,事务3将ID为1记录修改为(1,事务3),事务ID也会变成3,回滚指针指向undo log中该条记录事务1版本
事务3提交,释放锁。

MySQL(四)—MVCC实现可重复读的原理

现在又来一个事务,修改ID为1这行记录
该事务会重新走一遍事务3的流程,假如该事务ID为5,则现在的图变为
MySQL(四)—MVCC实现可重复读的原理

可以看出此时事务对同一条记录的修改,会使这条记录的undo log变为一个链表的形式。链首是最新的旧记录,链尾是最早的旧记录。

注意点
undo log分为insert undo和update undo,insert undo即执行insert 操作留下的历史记录,insert undo会在事务提交/回滚后直接删除,而update undo会保留下来做历史版本链表。上面为了能够讲述明白所以没有删除事务1的insert undo

3.一个数组

在InnoDB内部维护着一个数组,该数组(trx_sys->descriptors数组)会记录当前还未提交的事务id,id会从小到大排序。也就是说事务执行时会向InnoDB申请一个事务id,该数组会记录此id,如果该事务提交了,则从该数组中删除。

这个数组有什么用呢?为下面的ReadView做铺垫,创建ReadView时会复制一份该数组到ReadView中,ReadView会依据数组中未提交的id值进行判断事务是否可见一条记录。

4.ReadView

什么是ReadView?
ReadView,读视图。ReadView从代码层面其实是一个结构体(C语言名词),名叫read_view_t。事务其实也是一个结构体,trx_t。每个数据库连接持有一个trx_t(事务),每个trx_t(事务)持有一个read_view_t(读视图);事务进行快照读操作产生的一个ReadView。

ReadView有什么用?
前面说到ReadView主要做事务可见性判断,即某个事务执行快照读时,对该记录创建一个ReadView读视图,根据ReadView去判断当前事务能够看到哪个版本数据,有可能是最新的数据,也有可能是该行记录undo log里面某个版本的数据。

ReadView如何做可见性判断?
回答这个问题要从ReadView结构体中的属性入手了。
read_view_t

  • descriptors数组(readview数组):拷贝记录当前活跃事务id的trx_sys->descriptors到该数组中
  • up_limit_id:记录该数组中的最小值(min_trx_id有点反人类,up对应min)
  • low_limit_id:记录系统还未分配给事务的id,该值大于descriptors数组中的最大值(因为还没分配给事务的id是创建readview时刻当前系统中最大的,InnoDB从小到大给事务分配)。

这几个属性有什么用呢?利用上述属性做事务可见性判断

判断的核心思想是事务启动以前及以后所有还没提交的事务,它都不可见。源码如下

//id:一条记录的事务id
bool changes_visible(trx_id_t id, const table_name_t &name) const
    MY_ATTRIBUTE((warn_unused_result)) {
  ut_ad(id > 0);

  //如果这条记录的事务id<数组中最小值 或者 等于当前事务id,返回true那么当前事务可见这条记录
  if (id < m_up_limit_id || id == m_creator_trx_id) {
    return (true);
  }

  check_trx_id_sanity(id, name);

  //如果这条记录的事务id大于等于最大事务id,返回false那么当前事务不可见这条记录
  if (id >= m_low_limit_id) {
    return (false);

  } else if (m_ids.empty()) {
    return (true);
  }

  const ids_t::value_type *p = m_ids.data();

  return (!std::binary_search(p, p + m_ids.size(), id));
}

为了方便理解,下面对上面的代码做进一步的说明,一共分四种情况

如果最新记录上事务id<up_limit_id(min_trx_id),证明当前事务构建readview时这个事务已经提交了,所以可以看见这条记录

如果最新记录上事务id>=low_limit_id(max_trx_id),证明当前事务构建readview时这个事务还没有对记录进行修改操作,所以看不见这条记录

如果最新记录上事务id在up_limit_id和low_limit_id之间,且在readview数组中,证明当前事务构建readview时这个事务正在修改该条记录,所以看不见这条记录

如果最新记录上事务id在up_limit_id和low_limit_id之间,且不在readview数组中,证明当前事务构建readview时这个事务已经提交,所以可以看见这条记录

是不是字太多,记这么多东西简直是难为人。

总结下最核心的,InnoDB的事务快照读的情况下只能看见已经提交事务的数据,已经提交分为两种情况

  • 一条记录事务id<up_limit_id,证明当前事务构建readview时这个事务已经提交了,所以可以看见这条记录
  • 一条记录事务id在readview数组范围中,但不在readview数组中,也可以证明事务已经提交了

如果满足其中一种情况,事务则可以看见该记录

三、举例验证MVCC原理

事务隔离级别为可重复读。当前系统中有5个事务,5个事务都对id为1这行记录进行操作。
其中事务1和事务5已经提交,事务8进行快照读。

时刻 事务1 事务3 事务5 事务7 事务8
T1 begin; begin; begin; begin; begin;
T2 插入(1,事务1)记录
commit;
T3 修改id为1的名字为事务3 修改id为1的名字为事务5
commit; 修改id为1的名字为事务7;
查询id为1的记录(快照读)

事务8在T4时刻快照读创建ReadView,在T4时刻可以读取到事务几的数据呢?

首先看T4时刻ReadView中各个属性的值为多少

  • ReadView数组:拷贝全局未提交事务id,即[3,7,8]
  • up_limit_id:记录readview数组中最小值为3
  • low_limit_id:记录系统尚未分配的事务id为9

根据前面所说隐藏字段及undo log版本链,可以做成如下图
MySQL(四)—MVCC实现可重复读的原理

最后得出结论,事务8可以读取到事务5版本的数据

需要注意的是:
MVCC可以通过ReadView的方式实现读已提交和可重复读的隔离级别,但是两种隔离级别创建的ReadView的时间点不同。

  • 读已提交会在每次Select创建一个ReadView
  • 可重复读是第一次select之后创建ReadView,之后再select都会复用。

因此可重复读的隔离级别解决了不可重复读的问题,并一定程度上避免了幻读问题,但是没有真正结解决,请看下一篇


参考文献

事务实现源码级:http://mysql.taobao.org/monthly/2018/11/04/
事务概括:http://mysql.taobao.org/monthly/2017/12/01/
通俗易懂级MySQL MVCC
深入理解MVCC:https://www.cnblogs.com/kismetv/p/10331633.html

上一篇:MySQL的四个隔离级别是如何实现的


下一篇:MySQL-MVCC实现原理