MVCC
- MVCC的几个问题
- 1.update、insert、select和delete如何在MVCC中维护版本链?
- 2.select读取,是读取最新的版本呢?还是读取历史版本?
- 3.当前读和快照读
- 4.那为什么要有隔离级别呢?
- 5.如何保证,不同的事务,看到不同的内容呢?也就是如何如何实现隔离级别?
- Read View
- 一、Read View的作用
- 二、Read View的创建
- 三、Read View的使用
- Read view结构体理解
- 整体流程
- 再谈R-R下的快照读与R-C下的快照读
- RR 与 RC的本质区别
MVCC的几个问题
1.update、insert、select和delete如何在MVCC中维护版本链?
- 通过上述可知(
upadte
)通过MVCC维护。 - 而
delete
也是一样的,别忘了,删数据不是清空,而是设置flag为删除即可。也可以形成版本。 select
不会对数据做任何修改,所以,为select
维护多版本,没有意义。- 因为
insert
是插入,也就是之前没有数据,那么insert
也就没有历史版本。但是一般为了回滚操作,insert的数据也是要被放入undo log中,如果当前事务commit了,那么这个undo log 的历史insert记录就可以被清空了。
总结一下,也就是我们可以理解成,update
和delete
可以形成版本链,insert
暂时不考虑。
2.select读取,是读取最新的版本呢?还是读取历史版本?
默认状态R-R
我们来看看下面两张图
可以看出如果在更新操作前其他事务未查询,提交后再进行查询,则读取到的为最新更改数据;而如果在提交前进行了查询操作,为了不引起幻读,保持了事务内和第一次查询一样的结果
那么我们怎么在事务内获取最新数据呢?
select date from table lock in share mode(共享锁)
3.当前读和快照读
-
当前读:读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。增删改,都叫做当前读,select也有可能当前读,比如:select lock in share mode(共享锁), select for update
-
快照读:读取历史版本(一般而言),就叫做快照读。
4.那为什么要有隔离级别呢?
事务都是原子的。所以,无论如何,事务总有先有后。
但是经过上面的操作我们发现,事务从begin->CURD->commit,是有一个阶段的。也就是事务有执行前,执行中,执行后的阶段。但,不管怎么启动多个事务,总是有先有后的。
那么多个事务在执行中,CURD操作是会交织在一起的。那么,为了保证事务的“有先有后”,是不是应该让不同的事务看到它该看到的内容,这就是所谓的隔离性与隔离级别要解决的问题。
5.如何保证,不同的事务,看到不同的内容呢?也就是如何如何实现隔离级别?
Read View
Read View就是事务进行 快照读 操作的时候生产的 读视图 (Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)
Read View 在 MySQL 源码中,就是一个类,本质是用来进行可见性判断的。 即当我们某个事务执行快照读的时候,对该记录创建一个 Read View 读视图,把它比作条件,用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的 undo log 里面的某个版本的数据。
从内核源码解析数据库中的Read View
在数据库管理系统中,特别是那些支持多版本并发控制(MVCC)的系统中,Read View(读视图)是一个非常重要的概念。它决定了事务在读取数据时能看到哪些版本的数据。下面,我们将从内核源码的角度来深入解析Read View的实现和工作原理。
一、Read View的作用
在MVCC中,每个事务都有一个Read View,它代表了事务开始时刻的数据库快照。通过这个Read View,事务可以安全地读取数据,而不用担心其他事务的并发修改。Read View的作用是确保事务的隔离性,防止脏读、不可重复读和幻读等问题。
二、Read View的创建
在源码中,Read View的创建通常是在事务开始时进行的。这个过程会涉及到以下几个关键步骤:
-
获取当前活跃事务列表:遍历事务管理系统,找出当前所有活跃(即未提交)的事务,并将它们的事务ID加入到活跃事务列表中。
-
确定min_trx_id和max_trx_id:min_trx_id是活跃事务列表中的最小事务ID,而max_trx_id是下一个待分配的事务ID(通常是当前最大事务ID加1)。这两个值将用于后续判断数据版本的可见性。
-
创建Read View结构体:根据获取到的活跃事务列表、min_trx_id和max_trx_id等信息,创建一个Read View结构体。这个结构体通常包含了一些关键的成员变量,如事务ID列表、min_trx_id、max_trx_id等。
三、Read View的使用
在事务执行过程中,每次读取数据时都会使用到Read View。具体来说,读取数据的过程会按照以下步骤进行:
-
查找数据版本:根据主键或索引找到相应的数据行,并获取该数据行的所有版本信息(包括版本号和对应的事务ID)。
-
判断数据版本的可见性:遍历数据版本链表,对每个版本进行判断。如果版本的事务ID小于min_trx_id,说明这个版本在事务开始之前就已经提交了,因此是可见的;如果版本的事务ID等于当前事务的ID,说明这个版本是当前事务自己修改的,因此也是可见的;如果版本的事务ID在活跃事务列表中,说明这个版本是由其他未提交的事务修改的,因此是不可见的;如果版本的事务ID大于max_trx_id,说明这个版本是在当前事务开始后创建的,因此也是不可见的。
-
选择可见的数据版本:根据上一步的判断结果,选择第一个可见的数据版本作为读取结果。如果找不到可见的版本(即所有数据版本都不可见),则返回空或抛出异常。
Read view结构体理解
简略下来就是这几个主要字段
我们在实际读取数据版本链的时候,是能读取到每一个版本对应的事务ID的,即:当前记录的 DB_TRX_ID。那么,我们现在手里面有的东西就有,当前快照读的 ReadView 和 版本链中的某一个记录的 DB_TRX_ID。所以现在的问题就是,当前快照读,应不应该读到当前版本记录。
通过上图我们可以知道:
-
事务ID的大小与可见性:
- 一般来说,一个具有较大事务ID的事务能够“看到”具有较小事务ID的事务所做的更改(如果它们没有被其他后续事务覆盖或删除)。
- 一个具有较小事务ID的事务则不能“看到”具有较大事务ID的事务所做的更改,直到它重新获取一个新的、更大的事务ID(即,当该事务再次开始一个新的事务时)。
-
Read View:
- 在MVCC中,当事务想要读取一行数据时,它并不是直接读取该行的最新版本,而是基于当前事务的ID(或时间戳)和一个“read view”来确定应该读取哪个版本的数据。
- Read view通常包含了在当前事务开始时仍然活跃(即未提交或未回滚)的所有其他事务的ID。这是为了确保当前事务不会读取到任何可能在未来被这些活跃事务回滚的数据版本。
-
注意:
- 并不是所有具有较小ID的事务都不能看到具有较大ID的事务的更改。这取决于事务何时开始以及何时读取数据。
- 如果一个具有较小ID的事务在具有较大ID的事务提交之后开始,并且没有其他活跃事务干扰,那么它仍然可以看到那个具有较大ID的事务的更改。
- 此外,某些数据库可能允许事务使用特定的查询选项(如“脏读”、“不可重复读”或“幻读”)来故意读取未提交的数据或看到其他事务的更改,但这超出了标准MVCC行为的范围。
快照到事务ID不一定是连续的理解
——虽然事务ID一直是自增的,但是有的事务持续时间长,有的事务时间短;晚来的事务可能先完成提交,先来的事务可能没完成而在快照时并未提交
,所以快照所得的事务ID只有未完成的,也就是在版本链中的,已经提交的历史事务一定是能被看到的,所以可能产生快照到的事务ID不连续。
对应源码为
如果查到不应该看到当前版本,接下来就是遍历下一个版本,直到符合条件,即可以看到。上面的 readview 是当你进行select的时候,会自动形成。
整体流程
假设当前有条记录:
事务进行操作
- 事务4:修改name(张三) 变成name(李四)
- 当 事务2 对某行数据执行了 快照读 ,数据库为该行数据生成一个 Read View 读视图
//事务2的 Read View m_ids; // 1,3
up_limit_id; // 1
low_limit_id; // 4 + 1 = 5,原因:ReadView生成时刻,系统尚未分配的下一个事务ID
creator_trx_id // 2
此时对应的版本链为
只有事务4修改过该行记录,并在事务2执行快照读前,就提交了事务。
我们的事务2在快照读该行记录的时候,就会拿该行记录的 DB_TRX_ID 去跟 up_limit_id,low_limit_id和活
跃事务ID列表(trx_list) 进行比较,判断当前事务2能看到该记录的版本。
//事务2的 Read View
m_ids; // 1,3
up_limit_id; // 1
low_limit_id; // 4 + 1 = 5,原因:ReadView生成时刻,系统尚未分配的下一个事务ID
creator_trx_id // 2
-
//事务4提交的记录对应的事务ID
DB_TRX_ID=4
-
//比较步骤
DB_TRX_ID(4)< up_limit_id(1)
? 不小于,下一步
DB_TRX_ID(4)>= low_limit_id(5)
? 不大于,下一步
m_ids.contains(DB_TRX_ID)
? 不包含,说明,事务4不在当前的活跃事务中。
故,事务4的更改,应该看到。 所以事务2能读到的最新数据记录是事务4所提交的版本,而事务4提交的版本也是全局角度上最新的版本
再谈R-R下的快照读与R-C下的快照读
当前读和快照读在RR级别下的区别
流程表如图:
- 用例1与用例2:唯一区别仅仅是 表1 的事务B在事务A修改age前 快照读 过一次age数据
- 而 表2 的事务B在事务A修改age前没有进行过快照读。
事务中快照读的结果是非常依赖该事务首次出现快照读的地方,即某个事务中首次出现快照读,决定该事务后 续快照读结果的能力 delete同样如此
而对于R-C相同的操作
RR 与 RC的本质区别
- 正是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才会有不可重复读问题。