mysq在Repeatable Read 和Read Commit下避免快照读:MVCC

一、mysq在Repeatable Read 和Read Commit下避免快照读:MVCC

mysq在Repeatable Read 和Read Commit下避免快照读:MVCC

1.当前读,读最新的,且不会被其他事务更改

mysq在Repeatable Read 和Read Commit下避免快照读:MVCC 

2.快照读:多版本并发控制,可能都到的是历史版本 

快照读:不加锁的非阻塞读,select

RC隔离级别下,快照读和当前读结果一样,都是已提交的最新

RR下,当前读结果是其他事务已经提交的最新结果,快照读是当前事务之前读到的结果。RR下创建快照读的时机决定了读到的版本。

3.MVCC的简要原理:

每行记录都含有两个隐藏列,分别是记录的创建时间与删除时间

每次开启事务都会产生一个全局自增ID

在RR隔离级别下

INSERT ->  记录的创建时间 = 当前事务ID,删除时间 = NULL

DELETE -> 记录的创建时间不动,删除时间 = 当前事务ID

UPDATE -> 将记录复制一次,通过Delete ,Insert修改

        老记录的创建时间不动,删除时间 = 当前事务ID

        新记录的创建时间 = 当前事务ID,删除时间 = NULL

SELECT -> 返回的记录需要满足两个条件:

        创建时间 <= 当前事务ID (记录是在当前事务之前或者由当前事务创建的)

        删除时间 == NULL || 删除时间 > 当前事务ID (记录是在当前事务之后被删除的)

二、mysq在Repeatable Read 和Read Commit下避免快照读:MVCC

1.Innodb引擎会为每个事务每一行添加3个隐藏字段实现的:DATA_TRX_ID、DATA_ROLL_PTR,DB_ROW_ID(可能没有)

DB_Trx_id:最近修改本数据的事务ID

db_roll_ptr:回滚指针

db_row_id:隐含的自增ID,如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引

另外,每条记录的头信息(record header)里都有一个专门的bit(deleted_flag来表示当前记录是否已经被删除

2.undo日志:

 UPDATE非主键语句的效果是

    老记录被复制到rollback segment中形成undo log,DB_TRX_ID和DB_ROLL_PTR不动

    新记录的DB_TRX_ID = 当前事务ID,DB_ROLL_PTR指向老记录形成的undo log

    这样就能通过DB_ROLL_PTR找到这条记录的历史版本。如果对同一行记录执行连续的update操作,新记录与undo log会组成一个链表,遍历这个链表可以看到这条记录的变迁)

eg:事务1里,修改第一行数据(DB_ROW_ID=1),修改field2=32,写日志

mysq在Repeatable Read 和Read Commit下避免快照读:MVCC

事务2在事务1之后,修改第一行数据(DB_ROW_ID=1)修改Field3=45

mysq在Repeatable Read 和Read Commit下避免快照读:MVCC 

每开启一个事务,该事务对应的DB_TRX_ID都会不同(依次递增),通过当前行的DB_ROLL_PRT可以读到undo日志,通过undo日志的DB_TRX_ID,与当前行的DB_TRX_ID大小比较就可以找到当前事务对本行的可见性(具体读到哪个版本的值,能否删除更新等)

3.read view

read_view中维护了系统中活跃事务集合的快照,这些活跃事务ID的最小值为up_limit_id,最大值为low_limit_id(不要搞反了!!!)
RR读:每个事务在开始都会根据当前系统的活跃事务链表创建一个read_view,read_view是用来检索行的可见性的。 
RC读:事务中,每个语句都会创建read_view。
假设当前的活跃事务链表如下所示:current-trx —> trx7 —> trx5 —> trx3 —> trx1;
read view的变量

read_view->creator_trx_id = current-trx;                       当前的事务id
read_view->up_limit_id = trx1;                                      当前活跃事务的最小id
read_view->low_limit_id = trx7;                                     当前活跃事务的最大id
read_view->trx_ids = [trx1, trx3, trx5, trx7];                   当前活跃的事务的id列表
read_view->m_trx_ids = 4;                                            当前活跃的事务id列表长度

以上是read view中变量组成,下面来分析一下快照的形成过程
low_limit_id,即当时活跃事务的最大id,如果读到row的data_trx_id>=low_limit_id,说明这些数据在当前事务开始时都还没有提交,这些数据都不可见。

up_limit_id,即当时活跃事务列表的最小事务id,如果row的data_trx_id<up_limit_id,说明这些数据在当前事务开始时都已经提交,这些数据均可见。

data_trx_id在up_limit_id和low_limit_id之间的row,如果这个data_trx_id在trx_ids的集合中,就说明开启当前的事务的时候,这个data_trx_id还处于活跃状态,即还未提交,那么这个row是不可见的;如果这个data_trx_id不在trx_ids的集合中,就说明开启当前的事务的时候,这个data_trx_id已经提交,那么这个row是可见的。
 

形式化的说就是:

SELECT操作返回结果的可见性是由以下规则决定的:

DB_TRX_ID < up_limit_id  -> 此记录的最后一次修改在read_view创建之前,可见

DB_TRX_ID > low_limit_id   -> 此记录的最后一次修改在read_view创建之后,不可见  ->  需要用DB_ROLL_PTR查找undo log(此记录的上一次修改),然后根据undo log的DB_TRX_ID再计算一次可见性

up_limit_id <= DB_TRX_ID <= low_limit_id -> 需要进一步检查read_view中是否含有DB_TRX_ID

    DB_TRX_ID ∉ read_view  -> 此记录的最后一次修改在read_view创建之前,可见

    DB_TRX_ID ∈ read_view -> 此记录的最后一次修改在read_view创建时尚未保存,不可见  ->  需要用DB_ROLL_PTR查找undo log(此记录的上一次修改),然后根据undo log的DB_TRX_ID再从头计算一次可见性

经过上述规则的决议,我们得到了这条记录相对read_view来说,可见的结果。

此时,如果这条记录的delete_flag为true,说明这条记录已被删除,不返回。

   如果delete_flag为false,说明此记录可以安全返回给客户端

也就是以上我们开启一个事务的时候,Innodb为每个行数据增加了三个隐藏字段,当事务里快照读的时候,使用DB_TRX_ID与生成的Read View(利用了undo日志)检测当前所读行的可见行,可以则返回可见的数据(可能是历史版本),不可以则不返回。

 

三、RR和RC隔离级别下的快照读

 用MVCC这一种手段可以同时实现RR与RC隔离级别

它们的不同之处在于:

RR:read view是在first touch read时创建的,也就是执行事务中的第一条SELECT语句的瞬间,后续所有的SELECT都是复用这个read view,所以能保证每次读取的一致性(可重复读的语义)

RC:每次读取,都会创建一个新的read view。这样就能读取到其他事务已经COMMIT的内容。

所以对于InnoDB来说,RR虽然比RC隔离级别高,但是开销反而相对少。

补充:RU的实现就简单多了,不使用read view,也不需要管什么DB_TRX_ID和DB_ROLL_PTR,直接读取最新的record即可。

 

参考:

 https://www.cnblogs.com/stevenczp/p/8018986.html

https://blog.****.net/mahl1990/article/details/79298470