一、前言

全称Multi-Version Concurrency Control,即多版本并发控制,主要是为了提高数据库的并发性能。以下文章都是围绕InnoDB引擎来讲,因为myIsam不支持事务。
同一行数据平时发生读写请求时,会上锁阻塞住。但mvcc用更好的方式去处理读—写请求,做到在发生读—写请求冲突时不用加锁。
这个读是指的快照读,而不是当前读,当前读是一种加锁操作,是悲观锁。那它是怎么做到读—写不加锁的呢,快照读和当前读又是什么呢?下面就为大家一一介绍。

二、正文

1.什么是当前读?

它读取的数据库记录,都是当前最新的版本,会对当前读取的数据进行加锁,防止其他事务修改数据。是悲观锁的一种操作。
如下操作都是当前读:

  • select lock in share mode (共享锁)
  • select for update (排他锁)
  • update (排他锁)
  • insert (排他锁)
  • delete (排他锁)
  • 串行化事务隔离级别

    2.什么是快照读?

    快照读的实现是基于多版本并发控制,即MVCC,既然是多版本,那么快照读读到的数据不一定是当前最新的数据,有可能是之前历史版本的数据。
    如下操作是快照读:

  • 不加锁的select操作(注:事务级别不是串行化)

    3.快照读和MVCC关系?

    MVCCC是“维持一个数据的多个版本,使读写操作没有冲突”的一个抽象概念。
    这个概念需要具体功能去实现,这个具体实现就是快照读。(具体实现下面讲)

    4.数据库并发场景?

    读-读:不存在任何问题,也不需要并发控制
    读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读
    写-写:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失

    5.MVCC解决并发哪些问题?

    mvcc用来解决读—写冲突的无锁并发控制,就是为事务分配单向增长的时间戳。为每个数据修改保存一个版本,版本与事务时间戳相关联。

读操作只读取该事务开始前的数据库快照。

解决问题如下:
并发读-写时:可以做到读操作不阻塞写操作,同时写操作也不会阻塞读操作。
解决脏读、幻读、不可重复读等事务隔离问题,但不能解决上面的写-写 更新丢失问题。
因此有了下面提高并发性能的组合拳:
MVCC + 悲观锁:MVCC解决读写冲突,悲观锁解决写写冲突
MVCC + 乐观锁:MVCC解决读写冲突,乐观锁解决写写冲突

6.MVCC的实现原理?

它的实现原理主要是版本链,undo日志 ,Read View来实现的

6.1版本链

我们数据库中的每行数据,除了我们肉眼看见的数据,还有几个隐藏字段,得开天眼才能看到。分别是trx_id、db_roll_pointer。

  • trx_id

6byte,最近修改(修改/插入)事务ID:记录创建这条记录/最后一次修改该记录的事务ID。

  • roll_pointer(版本链关键)

7byte,回滚指针,指向这条记录的上一个版本(存储于rollback segment里)
image.png
如上图,trx_id是当前操作该记录的事务ID,而roll_pointer是一个回滚指针,用于配合undo日志,指向上一个旧版本。
image.png
每次对数据库记录进行改动,都会记录一条undo日志,每条undo日志也都有一个roll_pointer属性(INSERT操作对应的undo日志没有该属性,因为该记录并没有更早的版本),可以将这些undo日志都连起来,串成一个链表,所以现在的情况就像下图一样:

对该记录每次更新后,都会将旧值放到一条undo日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被roll_pointer属性连接成一个链表,我们把这个链表称之为版本链,版本链的头节点就是当前记录最新的值。另外,每个版本中还包含生成该版本时对应的事务id(trx_id),这个信息很重要,在根据ReadView判断版本可见性的时候会用到。

6.2 undo日志

Undo log 主要用于记录数据被修改之前的日志,在表信息修改之前先会把数据拷贝到undo log里。

当事务进行回滚时可以通过undo log 里的日志进行数据还原。

Undo log 的用途

保证事务进行rollback时的原子性和一致性,当事务进行回滚的时候可以用undo log的数据进行恢复。
用于MVCC快照读的数据,在MVCC多版本控制中,通过读取undo log的历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据版本。

undo log主要分为两种:

  • insert undo log

代表事务在insert新记录时产生的undo log , 只在事务回滚时需要,并且在事务提交后可以被立即丢弃

  • update undo log(主要)

事务在进行update或delete时产生的undo log ; 不仅在事务回滚时需要,在快照读时也需要;

所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除

6.3 Read View(读视图)

事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照。

记录并维护系统当前活跃事务的ID(trx_id)(没有commit,当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以越新的事务,ID值越大),是系统中当前不应该被本事务看到的其他事务id列表。

Read View主要是用来做可见性判断的, 即当我们某个事务执行快照读的时候,对该记录创建一个Read View读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据。

Read View几个属性

  • trx_ids: 当前系统活跃(未提交)事务版本号集合。
  • low_limit_id: 创建当前read view 时“当前系统最大事务版本号+1”。
  • up_limit_id: 创建当前read view 时“系统正处于活跃事务最小版本号”
  • creator_trx_id: 创建当前read view的事务版本号;

Read View可见性判断条件
image.png
(1)trx_id < up_limit_id || trx_id == creator_trx_id(显示)
如果数据事务ID(trx_id)小于read view中的最小活跃事务ID(up_limit_id),则可以肯定该数据是在当前事务启之前就已经存在了的,所以可以显示。

或者数据的事务ID(trx_id)等于creator_trx_id ,那么说明这个数据就是当前事务自己生成的,自己生成的数据自己当然能看见,所以这种情况下此数据也是可以显示的。

(2)trx_id >= low_limit_id(不显示)
如果数据事务ID(trx_id)大于read view 中的当前系统的最大事务ID,则说明该数据是在当前read view 创建之后才产生的,所以数据不显示。

(3)如果trx_id < low_limit_id则进入下一个判断,判断trx_id是否在活跃事务(trx_ids)中
不存在:则说明read view产生的时候事务已经commit了,这种情况数据则可以显示。
已存在:则代表我Read View生成时刻,你这个事务还在活跃,还没有Commit,你修改的数据,我当前事务也是看不见的。
通过上面的介绍我们已经大概了解了MVCC的基本原理,下面带大家实践下,以便更好的理解。

6.4 MVCC实践操作

比如有如下一条数据:
image.png
现在有个事务id是60的执行的这条记录的修改语句
image.png
此时在undo日志中就存在版本链
image.png
ReadView中主要就是有个列表(trx_ids)来存储我们系统中当前活跃着的读写事务,也就是begin了还未提交的事务。通过这个列表来判断记录的某个版本是否对当前事务可见。假设当前列表里的事务id为[80,100]。

此时有一个事务id为100的事务,修改了name,使得的name等于小明2,但是事务还没提交。则此时的版本链是
image.png
那此时另一个事务发起了select 语句要查询id为1的记录,那此时生成的ReadView 列表只有[100]。那就去版本链去找了,首先肯定找最近的一条,发现trx_id是100,也就是name为小明2的那条记录,发现在列表内(trx_ids),所以不能访问。

这时候就通过指针继续找下一条,name为小明1的记录,发现trx_id是60,小于列表中的最小id,所以可以访问,直接访问结果为小明1。

那这时候我们把事务id为100的事务提交了,并且新建了一个事务id为110也修改id为1的记录,并且不提交事务
image.png
这时候版本链就是
image.png
这时候之前那个select事务又执行了一次查询,要查询id为1的记录。

这个时候关键的地方来了

如果你是已提交读隔离级别,这时候你会重新一个ReadView,那你的活动事务列表(trx_ids)中的值就变了,变成了[110]。

按照上面的说法,由于[110]在活动事务列表(trx_ids)中,所以不能访问,这时候就通过指针继续找下一条,发现trx_id是100,name是小明2。

如果你是可重复读隔离级别,这时候你的ReadView还是第一次select时候生成的ReadView,也就是列表的值还是[100]。所以select的结果是小明1。所以第二次select结果和第一次一样,所以叫可重复读!

也就是说已提交读隔离级别下的事务在每次查询的开始都会生成一个独立的ReadView,而可重复读隔离级别则在第一次读的时候生成一个ReadView,之后的读都复用之前的ReadView。

这就是Mysql的MVCC,通过版本链,实现多版本,可并发读-写,写-读。通过ReadView生成策略的不同实现不同的隔离级别。

三、总结
本章首先介绍了快照读,然后介绍了MVCC和快照读的关系,最后详细分析了MVCC的实现原理并且通过一个实际应用使大家更加清晰的理解MVCC的具体实现。

文章来源:https://blog.csdn.net/SeekN/article/details/118552170

undo log由purge线程负责清理,比当前活跃事务还早的undo log就可以进行清理,保证文件不会一直增长。

read view创建时间点

未提交读(RU)
读已提交级别(RC)是在每次读请求时都会创建一份,解决脏读
可重复读级别(RR)是在第一个读请求发起时创建,解决不可重复读,脏读
串行化 解决脏读、不可重复读、幻读

脏读:读取到未提交的数据
不可重复读:两次读取的数据不一致
幻读:两次读取的数据条数不一样