一.Mysql在表中存储记录是每条记录会有3个隐藏字段(其实准确来讲是4个隐藏字段)
DB_TRX_ID :6 byte,最近修改( 修改/插入 )事务ID,记录创建这条记录/最后一次修改该记录的事务ID
DB_ROLL_PTR : 7 byte,回滚指针,指向这条记录的上一个版本(简单理解成,指向历史版本就行,这些数据一 般在 undo log 中)
DB_ROW_ID : 6 byte,隐含的自增ID(隐藏主键),如果数据表没有主键, InnoDB 会自动以 DB_ROW_ID 产生一 个聚簇索引
补充:实际还有一个删除flag隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除flag变了
二.undo日志
先说说undo log的概念:undo log是mysql中比较重要的事务日志之一,顾名思义,undo log是一种用于撤销回退的日志,在事务没提交之前,MySQL会先记录更新前的数据到 undo log日志文件里面,当事务回滚时或者数据库崩溃时,可以利用 undo log来进行回退。
在MySQL中,undo log日志的作用主要有两个:
1、提供回滚操作
每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到 undo 日志中
这里给个例子,比如本来一张表中有这么一条记录
事务ID为10的事务对该条记录进行了update修改,将name(张三)改成name(李四)
,因为要修改,所以要先给该记录加行锁(排他锁,即此时其他事务不得对该条记录进行操作)
修改前,现将改行记录拷贝到undo log中,所以,undo log中就有了一行副本数据。
所以现在 MySQL 中有两行同样的记录。现在修改原始记录中的name,改成 '李四'。并且修改原始记录的隐藏字 段 DB_TRX_ID 为当前事务10 的ID。而原始记录的回滚指针 DB_ROLL_PTR 列, 里面写入undo log中副本数据的地址,从而指向副本记录,既表示我的上一个版本就是它。
如果此时事务10commit提交了事务,那么事务10释放对该条记录锁,注意在提交事务后,可能并不会马上将该条记录存储在undo log中的历史版本进行清除处理,因为为了支持 MVCC(多版本并发控制),一些 该记录存储在undo log中的历史版本 可能会被保留一段时间,以便其他事务能够读取到事务提交前的数据版本。比如这里,事务10commit提交了事务就将undo log中的数据立马清除的话,有可能在这个时候还有其他的事务在读取这里的
所以不能够马上将其清除。
如果此时事务10并未提交事务,那么事务10便可以通过这条版本链进行回滚操作。
总而言之,当一个事务对一条记录进行操作时,会存在这么一个基于链表记录的历史版本链,其中undo log中记录着对应记录的历史数据。而所谓的回滚,无非就是用历史数据,覆盖当前数据。
补充:上面的例子我们是通过update更新记录的方式来进行阐述undo log可以帮我们存储这么一个历史版本链,那么如果是该改条记录进行delete的操作,右如何形成版本链呢?
其实方式是一样的,因为删数据不是清空,而是设置flag(上面提到的每条记录都有的一个隐藏字段)为删除即可。也可以形成版本链。
那么如果是事务过程中进行了insert操作,在底层又会怎么做呢?
因为`insert`是插入,也就是之前没有数据,那么`insert`也就没有历史版本。但是一般为了回滚操 作,insert的数据也是要被放入undo log中,如果当前事务commit了,那么这个undo log 的历史insert记录就可以被 清空了。
2.参与多版本控制(MVCC)的过程
具体如何实现的呢,下面开始讲解
首先先了解一些快照读与当前读:
快照读:读取历史版本(或者说可见版本),就叫做快照读(比如一般的select,当然如果是在SERIALIZABLE隔离级别下,普通的select也会对查询记录加上共享锁)。
当前读:读取最新的记录,就是当前读。增删改(会对对应记录加排他锁),都叫做当前读,select也有可能当前读,比如:select ... lock in share mode(会对对应记录加上共享锁), select ... for update(排他锁)。当前读不涉及到MVCC,因为它总是对最新的记录做操作
快照读参与MVCC版本控制的过程,所以我们再来说说与快照读先关的一个概念Read View
Read View就是事务进行 快照读 操作的时候生产的 读视图 (Read View),在该事务执行的快照读的那一刻,会生成数 据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增 的,所以最新的事务,ID值越大)。
Read View也参与MVCC版本控制的过程,通过之前说的表中每条记录其实都有的一个隐藏字段DB_TRX_ID的值与Read View中的记录进行对比来判断该条记录对于对应事务来说是否可见
现在我们谈谈快照读对于MVCC的价值所在。
在多个事务同时增删改的时候,都是当前读,是要加锁的。而对于查就要分类讨论,
如果查也要读取最新版(当前读)(对应select ... lock in share mode(会对对应记录加上共享锁),或者select ... for update(排他锁)),那么也就需要加锁,这就是串行化。 但如果是快照读(对应隔离等级非穿行化的一般的select),读取历史版本(可见版本)的话,是不受加锁限制的。也就是可以并行执行!换言之,提高了效率。并且由于快照读这种机制的存在,即使多个事务在执行时的各种CURD操作交织在一起,但是我们可以让不同的事务看到它该看到的内容,达到隔离的效果。
快照读的实现依赖与刚才所讲的Read View
Read View 在 MySQL 源码中,就是一个类,本质是用来进行可见性判断的。 即当我们某个事务执行快照读的时候,对该记录创建一个 Read View 读视图(注意:Read View 读视图不是事务创建出来的,在可重复读级别下,Read View 读视图是在首次快照时创建的,并且该Read View会使用到本次事务结束为止,而在读提交隔离级别下,每次快照读都会新生成一个Read View读视图),这个Read View 记录了当前所有事务状态,通过对表中记录中DB_TRX_ID字段与Read View 中的记录做比较,来判断当前事务是否能够看到该条数据,是否能够看到记录在undo log中的该版本的数据。
下面是Read View类的部分源码
class ReadView {
// 省略...
private:
/** 高水位,大于等于这个ID的事务均不可见*/
trx_id_t m_low_limit_id
/** 低水位:小于这个ID的事务均可见 */
trx_id_t m_up_limit_id;
/** 创建该 Read View 的事务ID*/
trx_id_t m_creator_trx_id;
/** 创建视图时的活跃事务id列表*/
ids_t m_ids;
/** 配合purge,标识该视图不需要小于m_low_limit_no的UNDO LOG,
* 如果其他视图也不需要,则可以删除小于m_low_limit_no的UNDO LOG*/
trx_id_t m_low_limit_no;
/** 标记视图是否被关闭*/
bool m_closed;
// 省略...
};
我们主要关心下面的几个字段
m_ids; //一张列表,用来维护Read View生成时刻,系统正活跃的事务ID
up_limit_id; //记录m_ids列表中事务ID最小的ID(up就是最小id,虽然up给人的感觉应该是上限,但确实没有写错)
low_limit_id; //ReadView生成时刻系统尚未分配的下一个事务ID(注意不是现存事务ID的最大值+1)(low在这里就是表示上限的意思,虽然low给人的感觉应该是下限,但这里同样也没有写错)
creator_trx_id //创建该ReadView的事务ID
我们在实际读取数据版本链的时候,是能读取到每一个版本对应的事务ID的,即:当前记录的 DB_TRX_ID隐藏字段,通过该字段的值与事务的Read View类对象中的字段进行对比判断即可得知当前记录(或者版本数据)对于当前事务是否可见。
如果记录的DB_TRX_ID 值小于 Read View 中的 m_up_limit_id 值,表示这个版本的记录是在创建 Read View 前已经提交的事务生成的,所以该版本的记录对当前事务可见。
如果记录的DB_TRX_ID 值等于Read View 中的 m_up_limit_id 值,表示这个版本的记录是当前事务生成的,所以该版本的记录对当前事务可见。
如果记录的 DB_TRX_ID值大于等于 Read View 中的 m_low_limit_id 值,表示这个版本的记录是在创建 Read View 后才启动的事务生成的,所以该版本的记录对当前事务不可见。
如果记录的DB_TRX_ID 值在 Read View 的 [min_trx_id , max_trx_id) 之间,需要判断 DB_TRX_ID是否在 m_ids 列表中:
如果记录的DB_TRX_ID 在 m_ids 列表中,表示生成该版本的记录的事务在当前事务Read View类对象生成时还没提交事务,所以该版本的记录对当前事务不可见。
如果记录的DB_TRX_ID不在 m_ids列表中,表示生成该版本的记录的事务在当前事务Read View类对象生成时已经提交了事务,所以该版本的记录对当前事务可见。
下面是判断记录(或者记录版本)可见性的相关部分源码,是不是和上面的分析一致?
'如果查到不应该看到当前版本,接下来就是遍历下一个版本,直到找到符合条件的版本,或者查找完整个版本链都没有找到。
这种通过「版本链」来控制并发事务访问同一个记录时的行为就叫 MVCC(多版本并发控制)
现在我们再来谈谈造成RC隔离级别和RR隔离级别之间差异的原因;
Read View生成时机的不同,造成了RC,RR级别下快照读的结果的不同
在RR级别下的某个事务的对某条记录的第一次快照读会创建一个快照及Read View, 将当前系统活跃的其他事务 记录起来
此后在调用快照读的时候,还是使用的是同一个Read View,所以只要当前事务在其他事务提交更新之前使用过 快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改不可见;
即RR级别下,快照读生成Read View时,Read View会记录此时所有其他活动事务的快照,这些事务的修改对于 当前事务都是不可见的。而早于Read View创建的已提交事务所做的修改均是可见
而在RC级别下的,事务中,每次快照读都会新生成一个Read View, 这就是我们在RC级别下的事务中可以 看到别的事务提交的更新的原因
总之在RC隔离级别下,是每个快照读都会生成并获取最新的Read View;而在RR隔离级别下,则是同一个事务 中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View。
正是RC每次快照读,都会形成Read View,所以,RC才会有不可重复读问题。