MySQL事务 MVCC的实现原理

文章目录

  • 谈谈事务
      • 小结
    • 快照读
    • 当前读
  • MVCC实现原理:
      • undolog,版本链 和 页面中的记录 之间的关系
      • 表中的记录:
      • undo log
      • Undo log用途:
      • undo log分类:
      • 读视图(Read View)
  • 解释

谈谈事务

id=1,k=1
MySQL事务 MVCC的实现原理_第1张图片

B事务查到的 k 是3,事务A查到的 k 是1.
下面就来讲解为什么?

首先我们需要注意 mysql失误启动的时机,begin 或 start transaction 命令并不是一个事务的起点,在执行到他们之后的第一个操作InnoDB表的语句,事务才开始真正启动(一致性视图在此时创建),如果想立刻启动一个事务使用 start transaction with consistent snapshot这个命令此时一致性视图立刻创建)。

Mysql有两个视图概念:

  1. 一个View是快照读,在开启事务之后,select查询到的都是这个快照里的值。
  2. 另一个是InnoDb在实现 MVCC(多版本并发控制)时用到的一致性读视图,consistent read view,用于支持 RC(Read Committed,读提交)和 RR(Repeatable Read,可重复读)隔离级别的实现,update就是从这里获取的值,是数据库里面的最新值。

每个事务都有一个事务ID,transaction id,每次事务更新数据的时候,都会生成一个新的数据版本,每个数据版本也有一个事务id 叫 row trx_id。row trx_id=transaction id。

下图就是k被多个事务更新后的状态。

MySQL事务 MVCC的实现原理_第2张图片
事实上v1,v2,v3不是物理上真实存在的,只有v4存在,U1,U2,U3存在并且存在于undo log(回滚日志),需要v2时,拿undo log里面的U3,U2,对v4进行操作,这样就可以拿到v2了。

那么当一个事务启动后,是怎么对其它更新操作视而不见的呢?
就是根据版本号row trx-id,如果一个数据版本是在我启动之前生成的,就认;如果是我启动以后才生成的,我就不认,我必须要找到它的上一个版本(通过undo log)

在实现上, InnoDB 为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”的所有事务 ID。“活跃”指的就是,启动了但还没提交。

最早开启的事务的事务ID最小,记为低水位(sessionA),后开启的事务ID大,记为高水位
MySQL事务 MVCC的实现原理_第3张图片

一个数据版本的 row trx_id,有以下几种可能:

  1. 如果落在绿色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的;
  2. 如果落在红色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的;
  3. 如果落在黄色部分,那就包括两种情况
    - a. 若 row trx_id 在数组中,表示这个版本是由还没提交的事务生成的,不可见;(对sessionB来说当事务C执行到update,但还没有提交时,不可见)
    - b. 若 row trx_id 不在数组中,表示这个版本是已经提交了的事务生成的,可见。(对sessionB来说当事务C执行完update,事务已经提交,事务C不在数组中了,可见)

我们再来看看上面的例子:
假设事务 A 开始前,系统里面只有一个活跃事务 ID 是 99;
事务A,B,C 的版本号分别是100,101,102
MySQL事务 MVCC的实现原理_第4张图片
事务C先更改k=2;如果此时直接在事务B中查看k,其实还是1,(事务B会找小于等于101的版本并且已经提交的),但有update,更新操作会在最新的版本上更新,这样101这个版本比102版本还新。此时再select,查找的就是101版本的k=3了。

事务A的select还是会向前找版本小于等于100并且已经提交的历史版本,所以就找到90版本的k=1了。

事务B中set在102版本的基础上修改,采用的就是当前读(读最新的值)
更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”(current read)。

除了 update 语句外,select 语句如果加锁,也是当前读。
下面这两个 select 语句,就是分别加了读锁(S 锁,共享锁)和写锁(X 锁,排他锁)。
select 语句需要先获取锁,只能等事务B提交释放锁后才能执行。所以就看到了B提交的数据了。

mysql> select k from t where id=1 lock in share mode;
mysql> select k from t where id=1 for update;

MySQL事务 MVCC的实现原理_第5张图片
MySQL事务 MVCC的实现原理_第6张图片
如果事务C变成上面这种情况,怎么办会怎么样呢?
根据行锁的两阶段锁协议(update会获取写锁,在commit之后才会释放),事务B会阻塞,直到事务C提交后,事务B才会update
MySQL事务 MVCC的实现原理_第7张图片

一致性读依赖mvcc快照,利用事务id递增特性,来做读取数据时历史版本的选择;当前读实际上是由行锁来实现的,持有行锁的更新操作才能进行当前读,否则更新操作会阻塞

读提交隔离级别下的事务状态图:
start transaction with consistent snapshot; 的意思是从这个语句开始,创建一个持续整个事务的一致性快照。所以,在读提交隔离级别下,这个用法就没意义了,等效于普通的 start transaction。
MySQL事务 MVCC的实现原理_第8张图片
读提交:
事务A读到的 k=2.
事务B查询到的结果 k=3.

读提交的逻辑和可重复读的逻辑类似,它们最主要的区别是:

  1. 在可重复读隔离级别下,只需要在事务开始的时候创建一致性视图,之后事务里的其他查询都共用这个一致性视图;
  2. 在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图。

小结

一致性视图,当前读,行锁,以及可重复读和读提交的区别

  • 一致性视图:是一个逻辑上的概念,不是一个物理视图的概念,具体是由MVCC来实现的。(通过undo log回滚日志可以推出我们想要的历史版本)
  • 重复读:自事务开启后,在该事务内读到的数据就是 小于等于自己版本并且已经提交的历史版本
  • 读提交:select之前提交的最新版本。
  • 当前读:读最后面的一个版本,update时,需要先读在写(重复读里面的写操作就是当前读)
  • 一致性读:就是重复读,是指使用MVCC机制读取到某个事务已经提交的数据,其实是从undo里面获取的数据快照。

可重复读的核心就是一致性读(consistent read);而事务更新数据的时候,只能用当前读。

InnoDB 的行数据有多个版本,每个数据版本有自己的 row trx_id,每个事务或者语句有自己的一致性视图。普通查询语句是一致性读,一致性读会根据 row trx_id 和一致性视图确定数据版本的可见性。

  • 对于可重复读,查询只承认在事务启动前就已经提交完成的数据;
  • 对于读提交,查询只承认在语句启动前就已经提交完成的数据;

重复读 select一致性读 update当前读
读提交 select 一致性读(只不过,每一个语句执行前都会重新算出一个新的视图)
一致性读就是快照读 update当前读

快照读

快照读基于MVCC 和 undo log 来实现,适用于简单select 语句

  1. 读已提交
  2. 可重复读

当前读

当前读是基于 临键锁(行锁 + 间歇锁)来实现的,适用于 insert,update,delete, select … for update, select … lock in share mode 语句,以及加锁了的 select 语句。

读已提交 和 可重复读 的update 都是当前读

MVCC实现原理:

版本链,undo log(回滚日志),Read View来实现

undolog,版本链 和 页面中的记录 之间的关系

表中的记录 + undolog 组成 版本链

表中的行记录 和 undolog的行 ,两者的结构有一些不一样

undo log 串起来 是一个链表:
MySQL事务 MVCC的实现原理_第9张图片

表中的记录:

每行信息,除了我们看到的,还有几个隐藏字段db_trx_id、db_roll_pointer("DB_ROLL_PTR",)、db_row_id,还有一个删除标记字段flag

db_trx_id:最近修改(修改/插入)事务ID
db_roll_pointer:回滚指针,指向这条记录的上一个版本(存储于rollback segment里)
db_row_id:隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以db_row_id产生一个聚簇索引。
删除flag隐藏字段: 记录被更新或删除并不代表真的删除,而是删除flag变了
以下是某一条记录:
MySQL事务 MVCC的实现原理_第10张图片

undo log

undo log版本链基于undo log实现

undo log中主要保存数据的基本信息
在这里插入图片描述
undo log 还包含两个隐藏字段 trx_id 和 roll_pointer。

  • trx_id 表示当前当前操作的事务的 id,MySQL 会为每个事务分配一个 id,id 是递增的。
  • roll_pointer 是一个指针,指向这个事务之前的 undo log。

举个例子 以便更好理解上面两个字段:
事务 1 执行插入操作:

INSERT INTO student VALUES (1, '张三');

undo log:
MySQL事务 MVCC的实现原理_第11张图片
事务 2 执行修改操作:

UPDATE student SET name='李四' WHERE id=1;

undo log:回滚指针指向上一个undo log
MySQL事务 MVCC的实现原理_第12张图片
为了保证事务并发操作时,在写各自的undo log时不产生冲突,InnoDB采用回滚段的方式来维护undo log的并发写入和持久化。回滚段实际上是一种Undo文件组织方式。

Undo log用途:

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

undo log分类:

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

读视图(Read View)

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

记录并维护系统当前活跃事务的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可见性判断条件
db_trx_id < up_limit_id || db_trx_id == creator_trx_id(显示)

如果数据事务ID小于read view中的最小活跃事务ID,则可以肯定该数据是在当前事务启之前就已经存在了的,所以可以显示。

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

db_trx_id >= low_limit_id(不显示)

如果数据事务ID大于read view 中的当前系统的最大事务ID,则说明该数据是在当前read view 创建之后才产生的,所以数据不显示。如果小于则进入下一个判断

db_trx_id是否在活跃事务(trx_ids)中

不存在:则说明read view产生的时候事务已经commit了,这种情况数据则可以显示。

已存在:则代表我Read View生成时刻,你这个事务还在活跃,还没有Commit,你修改的数据,我当前事务也是看不见的。

解释

MVCC和事务隔离级别
上面所讲的Read View用于支持RC(Read Committed,读提交)和RR(Repeatable Read,可重复读)隔离级别的实现。

RR、RC生成时机
RC隔离级别下,是每个快照读都会生成并获取最新的Read View;

而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View,之后的查询就不会重复生成了,所以一个事务的查询结果每次都是一样的。

解决幻读问题

  • 快照读:通过MVCC来进行控制的,不用加锁。按照MVCC中规定的“语法”进行增删改查等操作,以避免幻读。重复读隔离级别下,一个事务中只使用当前读,或者只使用快照读都能避免幻读。
  • 当前读:通过next-key锁(行锁+gap锁)来解决问题的。

RC、RR级别下的InnoDB快照读区别
在RR级别下的某个事务的对某条记录的第一次快照读会创建一个快照及Read View, 将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,还是使用的是同一个Read View,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改不可见;

即RR级别下,快照读生成Read View时,Read View会记录此时所有其他活动事务的快照,这些事务的修改对于当前事务都是不可见的。而早于Read View创建的事务所做的修改均是可见

而在RC级别下的,事务中,每次快照读都会新生成一个快照和Read View, 这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因

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

你可能感兴趣的:(mysql,mysql,数据库,java)