MySQL-MVCC

MVCC,即(Multiversion Concurrency Control)多版本并发控制,

MVCC 是通过数据行的多个版本管理来实现数据库的 并发控制 。这项技术使得在InnoDB的事务隔离级别下执行 一致性读 操作有了保证。

最早的数据库系统,只有读读之间可以并发,读写,写读,写写都要阻塞。引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行。

解决脏读、不可重复读、幻读这些问题有两种解决方案:

  1. MVCC,写操作进行加锁,读-写不冲突
  2. 读、写操作都采用加锁的方式

读、写操作中,读就是快照,写就是当前读。

1.快照读

快照读,又叫一致性读,读取的是快照数据。不加锁的简单的SELECT都属于快照读,快照读可能读取到的并不一定是数据的最新版本,也可能是之前的历史版本。这是要保证一致性读,就算一个事务更新了某行数据,另一个事务读取的还必须是当前事务开始时的数据版本,保证一致性读。

2.当前读

当前读读取的是记录的最新版本,而不是历史版本的数据,读取的时候还要保证其他并发事务不能修改当前记录,会对读取的就进行加锁。

因此加锁的SELECT或对数据增删改查都会进行当前读。

3.MVCC的实现原理

MVCC的实现依赖于:隐藏字段、Undo Log、Read View

3.1 隐藏字段

在InnoDB存储结构那篇文章中,页中记录的用户记录中,在记录的真实数据的部分有三个隐藏的列。

  1. row_id,不是必须的,表示行ID,唯一标识一条记录
  2. transaction_id,是必须的,每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的 事务id 赋值给trx_id 隐藏列。
  3. roll_pointer,是必须的,表示回滚指针,每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到 undo日志 中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。

我们主要看后两个。

3.2 Undo Log日志

insert undo log: 事务在insert新记录时产生的undo log
只在事务回滚时需要,并且在事务提交后可以被立即丢弃
update undo log: 事务在进行update或delete时产生的undo log,不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除

引用:https://blog.csdn.net/qq_39150049/article/details/120638319

对该记录每次更新后,都会将旧值放到一条 undo日志 中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,我们把这个链表称之为 版本链 ,版本链的头节点就是当前记录最新的值。

每个版本中还包含生成该版本时对应的 事务id 。

3.3 Read View

Read View是事务进行快照读操作的时候生产的读视图,在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,InnoDB为每个事务构造了一个数组,用来记录并维护系统当前活跃事务的ID(活跃指的是启动了但还没提交),当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大。(不过只有增删改才会被分配事务ID,在一个只读事务中的事务ID默认是0)

Read View解决的问题就是判断哪一个版本链中的哪个版本是当前事务可见的。


Read View中主要包含:

  1. creator_trx_id:创建这个Read View的事务ID
  2. trx_ids:表示在生成Read View时当前系统中活跃的读写事务的事务ID列表
  3. up_limit_id:活跃列表中活跃的事务中最小的事务ID
  4. low_limit_id:值等于系统中最大的事务ID(包括已提交和未提交)+1

当隔离级别为可重复读的时候,就避免了不可重复读,这是因为一个事务只在第一次 SELECT 的时候会获取一次 Read View,而后面所有的 SELECT 都会复用这个 Read View


Read View的对比规则

  • 如果 row 的 trx_id 落在绿色部分( trx_id<min_id ),表示这个版本是已提交的事务生成的,这个数据是可见的;
  • 如果 row 的 trx_id 落在红色部分( trx_id>max_id ),表示这个版本是由将来启动的事务生成的,是不可见的(若 row 的 trx_id 就是当前自己的事务是可见的);
  • 如果 row 的 trx_id 落在中间粉色部分(min_id <=trx_id<= max_id),那就包括两种情况
    • 若 row 的 trx_id 在视图数组中,表示这个版本是由还没提交的事务生成的,不可见(若 row 的 trx_id 就是当前自己的事务是可见的);
    • 若 row 的 trx_id 不在视图数组中,表示这个版本是已经提交了的事务生成的,可见。

这篇文章里写有真实流程分析,不错

https://blog.csdn.net/qq_39150049/article/details/120638319


核心点在于 ReadView 的原理, READ COMMITTD 、 REPEATABLE READ 这两个隔离级别的一个很大不同就是生成ReadView的时机不同:

  • READ COMMITTD 在每一次进行普通SELECT操作前都会生成一个ReadView
  • REPEATABLE READ 只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复使用这个ReadView就好了。

MySQL-MVCC
https://vickkkyz.fun/2022/04/07/计算机/mysql/MVCC/
作者
Vickkkyz
发布于
2022年4月7日
许可协议