MySQL第一讲 一遍让你彻底掌握MVCC多版本并发控制机制原理

        Mysql在可重复读隔离级别下,同样的sql查询语句在一个事务里多次执行查询结果相同,就算其它事务对数据有修改也不会影响当前事务sql语句的查询结果。这个隔离性就是靠MVCC(Multi-Version Concurrency Control)机制来保证的,对一行数据的读和写两个操作默认是不会通过加锁互斥来保证隔离性,避免了频繁加锁互斥,而在串行化隔离级别为了保证较高的隔离性是通过将所有操作加锁互斥来实现的。Mysql在读已提交和可重复读隔离级别下都实现了MVCC机制。

查看隔离级别

 select @@tx_isolation;

查看MySQL事物的隔离级别报错:
mysql> select @@tx_isolation;
ERROR 1193 (HY000): Unknown system variable 'tx_isolation'
老版本MySQL叫tx_isolation
MySQL8叫transaction_isolation
 
select @@transaction_isolation;

select @@global.transaction_isolation; 

设置全局事务隔离级别

set global transaction isolation level repeatable read

设置当前session会话的

set session transaction isolation level repeatable read

undo日志版本链与read view机制详解

undo日志版本链是指一行数据被多个事务依次修改过后,在每个事务修改完后,Mysql会保留修改前的数据undo回滚日志。其中聚簇索引记录中都会包含下面这连个必要的隐藏列(row_id并不是必要的;在创建的表有主键时,或者有不允许为NULL的UNQIUE键试,都不会包含row_id列)

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

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

需要判断版本链中的哪个版本是是当前事务可见的,因此有了一致性视图的概念。其中有四个属性比较重要

  • m_ids: 在生成ReadView时,当前活跃的读写事务的事务id列表
  • min_trx_id:  在生成ReadView时,当前系统中活跃的读写事务中最小的事务Id,也就是m_ids中的最小值
  • max_trx_id: 在生成ReadView时,系统应该分配给下一个事务的事务id值(主要max_trx_id不是m_ids中的最大事务id值,事务id是递增分配的)
  • creator_trx_id: 生成该ReadView的事务的事务id(只有对表中的记录进行改动,即执行 insert,update,delete操作,才会为事务分配唯一的事务id,否则事务的事务id默认为0)

事务里的任何sql查询结果需要从对应版本链里的最新数据开始逐条跟read-view做比对从而得到最终的快照结果。

版本链比对规则:

版本链中的当前版本是否可以被当前事务可见的要根据这四个属性按照以下几种情况来判断

  • 当 trx_id = creator_trx_id 时:当前事务可以看见自己所修改的数据, 可见
  • 当 trx_id < min_trx_id 时 : 生成此数据的事务已经在生成readView前提交了, 可见
  • 当 trx_id >= max_trx_id 时 :表明生成该数据的事务是在生成ReadView后才开启的, 不可见
  • 当 trx_id  在(min_trx_id ,max_trx_id)内 时

    • trx_id 在 m_ids 列表里面 :生成ReadView时,活跃事务还未提交,不可见
    • trx_id 不在 m_ids 列表里面 :事务在生成readView前已经提交了,可见

如果某一个版本的数据对当前事务不可见,就顺着版本链往下找下一个版本的数据,并继续使用上面的方法判断记录的可见性,直到版本链中的最后一个版本。

对于删除的情况可以认为是update的特殊情况,会将版本链上最新的数据复制一份,然后将trx_id修改成删除操作的trx_id,同时在该条记录的头信息(record header)里的(deleted_flag)标记位写上true,来表示当前记录已经被删除,在查询时按照上面的规则查到对应的记录如果delete_flag标记位为true,意味着记录已被删除,则不返回数据。

注意:

  • 读提交(read committed RC) 是在每一次读取数据的时候生成ReadView的
  • 可重复读(repeatable read RR)是在第一次读取数据的时候生成ReadView的

总结:

MVCC机制的实现就是通过read-view机制与undo版本链比对机制,使得不同的事务会根据数据版本链对比规则读取同一条数据在版本链上的不同版本数据。MVCC使得数据库读不会对数据加锁,提高了数据库的并发处理能力。借助MVCC,数据库可以实现READ COMMITTED,REPEATABLE READ等隔离级别,用户可以查看当前数据的前一个或者前几个历史版本,保证了ACID中的I特性(隔离性)。

下面结合实例分析在可重复读以及读已提交隔离级别下的分析当前事务读取的数据

  • 假设数据库中有一条初始数据 姓名是小杰,id是1 (id,姓名,trx_id,roll_point),插入此数据的事务id是1
  • 尤其要指出的是,只有这个事务操作了某些表的数据后当更改操作发生的时候(update,delete,insert),才会分配唯一的事务id,并且此事务id是递增的,单纯开启事务是没有事务id的,默认为0
  • T1时刻 事务100 更新name=A
  • T2时刻 事务100更新name=B
  • T3时刻事务200更新name=C

同颜色代表是同一事务内的操作

分析一下此时T4时刻事务300要读了,究竟会读到什么数据

此时 (生成一致性视图ReadView

  • m_ids 是[100,200]: 当前活跃的读写事务的事务id列表
  • min_trx_id 是 100
  • max_trx_id 是 201
  • creator_trx_id 为 0

此时版本链最上面一条记录trx_id(事务id)是 200,在m_ids列表内,因此不可见,根据roll_pointer跳到下一个版本,事务trx_id=100,同样在m_ids列表内,不可见,再根据roll_ponter跳到下一版本,事务trx_id=1, 符合txr_id<min_trx_id, 可见。所以读取的数据就是小杰

分析完第一个读,我们继续向下分析

  1. 当T5时刻时,事务100提交
  2. 当T6时刻时,事务300将名字改为D
  3. 当T7时刻时,事务400读取当前数据

此时这条数据的版本链如下

此时 (重新生成一致性视图ReadView

  • m_ids 是[200,300]: 当前活跃的读写事务的事务id列表
  • min_trx_id 是 200
  • max_trx_id 是 301
  • creator_trx_id 为 0

此时版本链最上面一条记录trx_id(事务id)是 300,活跃事务列表为[200,300],所以事务300数据不可见,同理事务200数据不可见,事务100在(min_trx_id,max_trx_id)内且不在活跃事务列表[200,300]中,表示此数据可见,所以读取出来的数据就是B

分析完第二个读,我们继续向下分析

  1. 当T8时刻时,事务200将名字改为E
  2. 当T9时刻时,事务200提交
  3. 当T10时刻时,事务300读取当前数据

此时这条数据的版本链如下

此时 (重新生成一致性视图ReadView

  • m_ids 是[300]: 当前活跃的读写事务的事务id列表
  • min_trx_id 是 300
  • max_trx_id 是 301
  • creator_trx_id 为 300

此时版本链最上面一条记录trx_id(事务id)是 200,在(min_trx_id,max_trx_id)内,且不在活跃事务列表 ,代表生成这个数据的事务已经在生成ReadView前提交了,此数据可见,所以读出的数据就是E.

当隔离级别是读已提交RC的情况下,每次读都会重新生成 一致性视图(ReadView)

  • T4时刻 事务300读取到的数据是小杰
  • T7时刻 事务400读取到的数据是B
  • T10时刻 事务300读取到的数据是E

那么在可重复读隔离级别下,上面那些读的数据会是怎么一回事

  • 一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的

在此可重复读RR隔离级别下,

1:T4时刻时事务300第一次读时 read-view是

  • m_ids 是[100,200]: 当前活跃的读写事务的事务id列表
  • min_trx_id 是 100
  • max_trx_id 是 201
  • creator_trx_id 为 0

很显然T4时刻读取的数据是和上面读已提交隔离级别下T4时刻读取的数据一样,也是小杰

2:T7时刻事务400读取据 时 read-view是

  • m_ids 是[200,300]: 当前活跃的读写事务的事务id列表
  • min_trx_id 是 200
  • max_trx_id 是 301
  • creator_trx_id 为 0

很显然T4时刻读取的数据是和上面读已提交隔离级别下T7时刻读取的数据一样,也是B

3:T10时刻事务300读取数据时,read-view就和上面读已提交隔离级别的read-view不一样了,因为可重复读隔离级别下事务开启后的read读会和第一次读的read-view视图保持一致,所以T10时刻的read-view视图一样是

  • m_ids 是[100,200]: 当前活跃的读写事务的事务id列表
  • min_trx_id 是 100
  • max_trx_id 是 201
  • creator_trx_id 为 300

而位于版本链最上面的记录的trx_id(事务id)是 200,数据是E,事务200在活跃事务列表 ,所以数据不可见,根据回滚指针找到上个版本,发现事务id是300,当前事务也是creator_trx_id =trx_id = 300,可见,所以读取的数据是D

当隔离级别是可重复读RR的情况下,事务只在第一次读的时候生成Read-View

  • T4时刻 事务300读取到的数据是小杰
  • T7时刻 事务400读取到的数据是B
  • T10时刻 事务300读取到的数据是D

 再继续分析如果没有事务300这条更改的这条记录,又该怎么继续向下分析呢?

  • m_ids 是[100,200]: 当前活跃的读写事务的事务id列表
  • min_trx_id 是 100
  • max_trx_id 是 201
  • creator_trx_id 为 300

而位于版本链最上面的记录的trx_id(事务id)是 200,数据是E,符合min_trx_id<=trx_id<max_trx_id ,但是事务200在活跃事务列表 ,所以数据不可见,根据回滚指针找到上个版本,发现事务id还是200继续查找上一个版本是100,100也在当前活跃列表里面,所以不可见根据回滚指针找上一个版本知道找到事务1,满足trx_id<min_trx_id,是可读数据所以读取的数据是小杰

我们再简单分析一个例子:

数据操作版本链如下:

 最开始事务假设为80,id=1的name为lilei,之后启动了事务100,事务200,事务300

T1时候 事务100修改操作id=2的数据

T2时候 事务200修改操作id=3的数据

T3时候 事务300修改操作id=1的数据 设置name=lilei300,并提交事务

T4时刻 事务400开始读id=1的数据,这时的read-view视图如下

  • m_ids 是[100,200]: 当前活跃的读写事务的事务id列表
  • min_trx_id 是 100
  • max_trx_id 是 401
  • creator_trx_id 为 0

这时位于版本链最上面的记录的trx_id(事务id)是 300,在(min_trx_id,max_trx_id)内,但是事务300不在活跃事务列表 ,表示事务已经提交是可见的

T5时刻,事务100修改操作id=1的数据 设置name=lilei1

T6时刻,事务100修改操作id=1的数据 设置name=lilei2

T7时刻 ,事务400开始读id=1的数据,这时如果是RR隔离级别

可得read-view视图如下:

  • m_ids 是[100,200]: 当前活跃的读写事务的事务id列表
  • min_trx_id 是 100
  • max_trx_id 是 401
  • creator_trx_id 为 0

这时位于版本链最上面的记录的trx_id(事务id)是 100,事务100在活跃列表中,表示数据不可见,继续查找版本链,发现事务300不在活跃事务列表 ,表示事务已经提交是可见的

T8时刻 事务100 提交事务 事务200修改操作id=1的数据,设置name=lilei3

T9时刻 事务200修改操作Id=1的数据,设置name=lilei4

T10时刻 事务400开始读id=1的数据,这时如果是RR隔离级别

可得read-view视图如下:

  • m_ids 是[100,200]: 当前活跃的读写事务的事务id列表
  • min_trx_id 是 100
  • max_trx_id 是 401
  • creator_trx_id 为 0

这时位于版本链最上面的记录的trx_id(事务id)是 200,事务200在活跃列表中,表示数据不可见,继续查找版本链,发现事务300不在活跃事务列表 ,表示事务已经提交是可见的,读取数据为lilei300

在RC隔离级别下 可得read-view视图如下:

  • m_ids 是[200]: 当前活跃的读写事务的事务id列表
  • min_trx_id 是 200
  • max_trx_id 是 401
  • creator_trx_id 为 0

这时位于版本链最上面的记录的trx_id(事务id)是 200,事务200在活跃列表中,表示数据不可见,继续查找版本链,发现trx_id=100<min_trx_id ,表示事务已经提交是可见的,读取数据为lilei2

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
MySQL的多版本并发控制MVCC)是一种并发控制机制,它主要是为了解决并发读写冲突的问题。在MVCC机制中,每个事务都可以看到数据库中的一个快照,这个快照是在事务开始时确定的。事务读取数据时,实际上是读取了该快照中的数据,而不是实际的数据。当事务需要修改数据时,MySQL会根据数据的版本号来判断是否可以进行修改。 MVCC的实现原理主要是在每一行数据后面保存多个版本号,并且还需要保存该版本号对应的事务ID。当开始一个事务时,MySQL会为该事务分配一个唯一的事务ID,该事务ID会被用于标记事务对应的数据版本号。当一个事务需要读取数据时,MySQL会根据该事务的事务ID和版本号来判断是否允许读取该数据。如果该事务的事务ID小于等于该数据的版本号,那么就可以读取该数据。如果该事务需要修改数据,则MySQL会为该数据在数据库中创建一个新版本,并将该新版本版本号和事务ID保存下来。这样,其他事务就可以继续读取原来的版本,而该事务则可以读取新版本并修改数据,从而实现并发控制。 需要注意的是,MVCC只能解决读写冲突的问题,而不能解决写写冲突的问题。此外,MVCC也会占用一定的存储空间,因为每个数据行都需要保存多个版本号和事务ID。因此,在使用MVCC机制时,需要注意存储空间和性能方面的问题。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员路同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值