|
本文分为三块内容:页、记录与索引;缓冲池与redo日志;undo日志、事务与MVCC。
存储引擎、页及其结构、操作
存储引擎
MySQL是一种关系型数据库,这种数据库不仅存储数据,还存储数据与数据之间的关系。MySQL中建立的库可以类比成电脑里的文件夹,库中建立的表可类比成文件。现实生活中我们用来存储数据的文件有不同的类型,每种文件类型对应各自不同的处理机制:比如处理文本用txt类型,处理表格用excel,处理图片用png等。数据库中的表也应该有不同的类型,表的类型不同,会对应MySQL不同的存取机制,表类型又称为存储引擎,MySQL根据不同的表类型会有不同的处理机制。存储引擎说白了就是如何存储数据、如何为存储的数据建立索引和如何更新、查询数据等技术的实现方法。因为在关系数据库中数据的存储是以表的形式存储的,所以存储引擎也可以称为表类型(即存储和操作此表的类型)。在Oracle 和SQL Server等数据库中只有一种存储引擎,所有数据存储管理机制都是一样的。而MySql 数据库提供了多种存储引擎。用户可以根据不同的需求为数据表选择不同的存储引擎,用户也可以根据自己的需要编写自己的存储引擎,SQL 解析器、SQL 优化器、缓冲池、存储引擎等组件在每个数据库中都存在,但不是每个数据库都有这么多存储引擎。MySQL 的插件式存储引擎可以让存储引擎层的开发人员设计,他们希望的存储层--例如,有的应用需要满足事务的要求,有的应用则不需要对事务有这么强的要求 ;有的希望数据能持久存储,有的只希望放在内存中,临时并快速地提供对数据的查询。
页的概念、结构
MySQL5.5版本后默认的存储引擎是InnoDB。InnoDB为了不同的目的而设计了多种不同类型的页,页是磁盘和内存之间交互的基本单位,我们把存放表中数据记录的页叫做索引页或数据页。页的结构如下:

记录的概念、结构
这里的一条记录指的是一条数据,比如插入了一条“insert into user values (...);”数据,就被认为是一条记录,因此,页面的最小记录和最大记录,就是指插入的最小数据和最大数据信息,只不过这里的最小最大是一个抽象的概念,类似正负无穷大的意思。User Records 包含很多User Record,这里记录之间是以单链表形式维护的。Free Space表示页中还没有使用的存储空间,与User Records是一种此消彼长的关系。一条记录里,包含记录的额外信息和记录的真实数据两部分内容,如下图所示:

额外信息需要关注的字段就是记录头信息,包含很多内容:
- deleted flag:逻辑删除标记,默认为0,若被删除则将标记改为1;
- min_rec_flag:用于表示B树中最小的非叶节点的目录项(0表示叶子节点)
- n_owned:记录成组,在每个组最后一条记录的头信息中用于存储该组一共有多少条记录(0表示不知道,其他数字表示该记录作为记录头信息中存储记录总数的最大记录)
- heap_no:由于记录是连续存放的(其数据结构就是单链表。如何提高单链表里查询记录的效率?引入分组的概念,这页与n_ownd密切相关)。整个记录被称为一个堆(其中0表示infimum,1表示supremum,插入记录后heap_no从2开始的),heap_no则表示页面中堆固定的相对位置
- record_type:即数据所表示的类型(其中,0表示普通类型,1表示B+树中非叶目录项,2表示页面最小记录,3表示页面最大记录)
- next_record:指向的是真实记录的首位置。这里的好处是,如果要查找下一条记录的额外信息,就走本记录真实数据+下一条记录额外信息;如果要查找下一条记录的真实数据,就走next_record+下一条记录真实数据。如果next_record放本记录首位置,那么取本记录的所有信息都会多走一片内存空间;如果放本记录末尾,那么查询下一条记录任何数据都必须遍历完本记录的数据显得效率低。因此next_record放中间是一种折中方式 。
记录的操作
删除记录

如上图所示,删除一条记录后,对页几乎没有影响,受影响的是这条被删除的记录的deleted_flag、next_record标记位和最大记录的n_owned。当这条记录在逻辑上被删除后,系统并不会马上将其清理掉,而是会将其加入垃圾链,以保证有相同数据插入时避免重新申请内存空间,从而实现空间复用的目的。
页内的目录与单页遍历记录
在页结构里,有大量记录都由单链表连接时,要查找特点记录比较麻烦。如果将页中各记录分组,再将分组头或尾放到槽位slot里,而每个槽位作为记录分组的页目录,这样查找记录就方便多了。最小记录单独占一个分组,而普通记录组可包含4-8条记录,含有supremum最大记录所在的组可以包含1-8条记录。因此,如果查找单页里的记录时,通过二分法找页目录里特定的slot,slot保存着对应分组里的最大记录的物理位置,而从分组头开始遍历单链表,由于单链表中记录数量极少,因此就一个组内而言,遍历的速度是极快的。情形如下:

如果是查找多页的情况,则会引入索引。
事实上,page directory是由slot生成的。试想象以下情形:刚开始时页里默认只有infimum和supremum,即两条记录,分别隶属于各自的分组。当插入第一条普通记录时,该记录的下一条记录指向supremum,n_ownd变为2;此后不断添加记录,对supremum这个分组来说,当记录总数达到8且准备插入第9个记录时,该分组会按插入记录的堆号分裂为2个分组,并产生一个slot作为新分组的页目录。以此类推,只要记录总数达到分裂的条件,分组就会分裂并生成一个slot,并将新记录插入到合适的分组链表里。因此,分组的无限细分产生slot,slot组成了页目录。
页满仍插入数据的情况
在往页中插入记录时,随着插入的记录愈来愈多,不可避免地会出现页满后还有记录插入的情况。那么,这个时候记录会有开辟新页、数据插入和数据迁移的过程。如下图所:

记录插入导致必须分页的情况下,MySQL会首先向系统申请一个页的内存空间,这时free链是满的,经过计算发现id=7的记录应该在现在id=10的位置,那么id=10的记录会先迁移到2号页,记录迁移以后,id=7的记录才会插入至指定位置。
数据迁移和页分裂的关系
数据迁移得分情况讨论:①如果是因为数据量过多,则相当于扩展页,局部数据需要重新插入 ②如果是因为纯粹的数据迁移,则相当于插入新数据
而页分裂就是纯粹指插入记录的数据量过多的情况了。
索引
上面提到了索引,就是在形如一条记录需要查找多个页的情况下,如果需要遍历的页数量太多,那么将各页分组,指定组的物理位置,将各物理位置连接起来形成索引,则对于多页查询的情况,是极有好处的。
InnoDB存储引擎采用的数据结构是B+树,B+树相对于B树的特点是,叶子节点是有指针的(MySQL则采用双向指针),非叶子节点的元素与叶子节点有部分冗余。这里,叶子节点存储的是完整的数据即数据页,而非叶节点则存储的是主键索引即索引页。现在试着构建索引:
index_demo表 | c1(主键) | c2 | c3 | 1 | 4 | u | 3 | 9 | d | 4 | 4 | a | 5 | 3 | y | 8 | 7 | a | 10 | 4 | 0 | 12 | 7 | d | 20 | 2 | e | 100 | 9 | x | 209 | 5 | b | 220 | 6 | 1 | 300 | 8 | a |

以上可以理解为简陋版的索引,存在的问题如下:
- 页数量增多导致页编制目录在有限的16KB下无限分裂,继而目录项太多,还是会存在遍历目录项的情况
- 删掉某个页,可能会产生页目录的大量移动现象
解决方式是什么?InnoDB给出的解决方案是建立真正的索引,比如聚簇索引、非聚簇索引、联合索引。
聚簇索引
注意:这里的目录项其实就是记录头信息里面的record_type=1,即B+树的非叶节点的目录项类型。目录项本质上就是页外的目录。
- 本质上是多抽了几层,更容易判断。一个page目录项包含了c1主键+页号。
- 实际上,层次不超过4层,一亿条数据以内满足。

事实上,一个页可以存储很多条记录,以100条为例;一个页的目录项即非叶节点存储的数据量远大于叶子节点存储的数据量,以1000为例,那么三层B+树可存储的记录为:100*1000*1000=1亿条记录。因此,记录通常不会超过4层结构。
非聚簇索引
- 二级索引 = 非聚簇索引
- 建立二级索引,指向一级索引即主键,因此查询c1、c2的时候不需要回表;查询c3会回表

联合索引

如何保证目录项的唯一值?c2+c1+pageNo:

三类索引结构总结
- 聚簇索引:
- 叶子节点(完整记录)
- 非叶节点(目录项=主键列的值+页号)
- 非聚簇索引:
- 叶子节点(c2-列索引+c1-主键所在列)
- 非叶节点(目录项=索引列的值+页号)
- 联合索引:
- 叶子节点(c2-索引列1+c3-索引列2+c1-主键所在列)
- 非叶节点(目录项=c2-索引列1+c3-索引列2+页号)
缓冲池
概念
由于磁盘与内存之间传输数据的差异极大,因此它们之间交互的大部分数据并不是直接直接交互,而是通过缓冲池来解决这种速度差异。磁盘与内存的交互单元是页,缓冲池中存储数据的主要单元也是页,且都是16KB,缓冲池的物理结构如图:

free链表、flush链表、LRU链表
可见,控制块和缓冲页是一一对应的,控制块专门控制缓冲页的读写情况。在实际情景中,存在这样的情况:磁盘想要读数据到缓冲池BP中,它如何得知BP中是否有空闲的缓冲页呢?其实,就是因为控制块与缓冲页一一对应的关系,有空闲的控制块就代表存在与之一一对应的空闲缓冲页,因此将空闲的控制块通过双向链表连接起来,就形成了free链表。

如果要将记录持久化到磁盘,如果一条一条记录持久化,显得效率低下。与free链表类似,将所有含待持久化到磁盘的脏页对应的控制块连接起来,也就形成了flush链表:

设想一种常见,有些数据是我们经常访问的,而有些数据可能只顺带访问一下甚至不访问。而BP的内存大小是有限的,有什么办法可以实质性提高BP的访问效率呢?
我们加载到BP上且经常访问的数据称为热数据,加载到BP上不常访问的数据称为冷数据。如果我们采用LRU算法,即淘汰最近最少使用的缓存页,保留最近常使用的缓存页。
这样需要考虑两个问题:①预读性是否强 ②是否存在全表扫描的情况
预读性是指,访问页记录里的数据时,根据局部性原理,该记录附近的记录也有可能会被访问,因此也有可能会将其加载到BP中。根据预读的顺序可将其分为线性预读和非线性预读,线性预读是指预读该记录所在页的下一个区,而随机预读是指预读该记录所在的整个页。
全表扫描,顾名思义就是查询一条记录,比如(select * from......),所查到的所有记录都往BP里放,可以想到的是如果数据量足够大,那么查询的所有数据可能不能全放到BP里,那么就只能走磁盘到内存的慢IO路径,无疑是效率极低的。
很遗憾单纯的LRU算法并不能解决以上两个问题。不过,最近最少使用的策略是对的,在此基础上,如果将整个缓冲页对应的控制块按照比例划分,一个用于专门存放热数据的记录,另一个用于专门存放冷数据的记录,即划分young区和old区,就能解决以上两个问题,如下图所示:

首先,对于预读的问题,可以根据innodb_old_blocks_pct的默认值调整一下,如果涉及到多页查询,就将参数调大一些,否则调小一点;其次,对于全表扫描的问题,可以在超过innodb_old_blocks_time默认值的情况下,如果仍有请求打到BP上要求访问全局扫描的部分值,则将这部分值从old区读取到young区。
Chunk和Buffer Pool
BP有多个实例,每个实例对应若干chunk,它们是一种包含与被包含的关系。

缓冲池总结
缓冲池的链表是双向链表:
- free链表
- flush链表
- 冷热LRU链表(innodb_old_blocks_pct、innodb_old_blocks_time)
redo日志
如果我们只在内存的Buffer Pool中修改了页面,假设在事务提交后突然发生了某个故障,导致内存中的数据都失效了,那么这个已经提交的事务在数据库中所做的更改也就丢失了。针对这种问题,怎么处理呢?
①提交一次就同步到数据库一次。但这种方式不可取,因为1条记录就占用16KB,显得浪费;其次有随机IO的问题 ②redo日志。记录某时某刻MySQL在什么物理位置修改了什么数据并解析成什么样的数据。这样的好处是能简化持久化数据带来的麻烦,即记录一次修改并以顺序IO的方式读写。
redo,顾名思义,就是“重做”一次。参考软件测试的瀑布开发模型,redo日志根据日志类型的难易程度,分为redo简单日志类型和redo复杂日志类型。
redo日志类型

一条记录的真实数据里,隐藏的列除了上文指出的trx_id列的值以及roll_pointer列的值,其实还包含row_id列的值。这个row_id是起记录的唯一标识作用,在一条记录既没有指定主键id,且没有配置字段为非空和非unique的列的情况下row_id就会默认生成。生成规则是:内存维护了一个内存变量,当满足row_id的生成条件时,内存变量便赋值给row_id并加一,当row_id的值等于256的倍数时,会往7号表空间的Max_row_id赋值,由于表空间在BP里,如果发生故障就会存在数据丢失,因此row_id需要存到redo日志(redo日志是写一条就会持久化到磁盘)里,且是type=8的简单日志类型里,因为这并不涉及到复杂结构和复杂运算。

这里可以理解为,复杂日志类型里的各种参数会被传入到特定的函数中,最终生成正确的SQL。
redo日志组
一条SQL语句可以出现多个基于原子性的日志,多条日志则可构成日志组。
常见的日志组包括:Max Row ID、聚簇索引、二级索引等。了解日志组的概念后,这里有一点思考,即:这些日志组是如何由单个日志联系并串起来的呢?答案在下图:


对底层页面进行一次原子访问的过程被称为一个Mini-Transaction(MTR)。 一个事务可以包含n条SQL语句,一条SQL语句可以包含n个MTR,一条MTR可以包含n条redo日志。关系如下图:

redo log block

部分名词解释:
- log block header:类比page header
- LOG_BLOCK_HDR_NO:该block的编号
- LOG_BLOCK_HDR_DATA_LEN:整个block日志所占的字节数
- LOG_BLOCK_FIRST_REC_GROUP:日志组的第一条被解析的日志,可根据遍历找到其他日志
- LOG_BLOCK_CHECKPOINT_NO:checkpoint的编号
- log block body:专门存放redo日志的地方
- log block trailer:类比page trailer
redo log buffer
顾名思义,redo log其实也是一种磁盘文件,它是内存中每进行一次事务时,就会往redo日志缓冲中写入对应修改的数据,再同步到redo日志文件里。

redo日志写入缓冲的过程如下:

redo日志刷盘的几种情况:
1> log buffer空间不足50%的时候
2> 事务提交的时候
3> 后台有一个线程,大约以每秒一次的频率将log buffer中的redo日志刷新到磁盘。
4> 正常关闭服务器时
5> 做checkpoint时
一条记录被修改的整个过程:
- 从DB查询并放到BP中,从BP中读取到内存里
- 修改记录内容,一种行为是将脏数据放到flush链中;
- 另一种行为是从BP读取修改后的数据并解析:
- 事务-->SQL--->MTR-->redo日志--->redo log buffer-->同步刷到磁盘log file(空间小、顺序IO)
redo log file
磁盘上的log文件:
在MySQL的数据目录中,默认有名为ib_logfile0和ib_logfile1的两个文件, log buffer中的日志在默认情况下就是刷新到这两个磁盘文件中,也便于循环利用:

redo日志文件格式:

几个需要关注的概念:
- log file header
- LOG_HEADER_START_LSN:lsn的长度,起始位置的长度
- checkpoint:实际上就是BP记录刷进磁盘后,log file相应备份的位置需要调整
- LOG_CHECKPOINT_NO:checkpoint执行的次数
- LOG_CHECKPOINT_OFFSET:表示checkpoint的偏移量,该位置前表示已修改,之后表示未修改
- LOG_CHECKPOINT_LOG_BUF_SIZE:表示log缓冲大小
lsn
lsn全称为log sequence number,是一个全局变量 ,用来记录当前总共已经写入的redo日志量。 lsn初始值为8704,也就是说,一条redo日志也没写入的时候,lsn的值就是8704了。

redo日志刷新磁盘长度


其中, mtr_1(8,716~8,916)、mtr_2(8,916~9,948)、mtr_3(9,948~10,000) 。
flush链表

细究一下mtr3修改b和d的过程:

redo日志文件组中各个lsn值的关系:

innodb_flush_log_at_trx_commit
- 0:表示在事务提交时,不立即向磁盘同步redo日志,这个任务交给后台线程来处理;
- 1:表示在事务提交时,需要将redo日志同步到磁盘。(默认值)
- 2:表示在事务提交时,需要将redo日志写到操作系统的缓冲区中,但并不需要保证将日志真正刷新到磁盘。如果操作系统挂掉了,则数据丢失。折中方式。
redo日志总结
redo日志是为了防止出现写入BP的记录因为故障导致无法恢复的情况而做的一种补救措施。就是一种记录操作行为并解析成日志的方式,具有所需内存空间小和顺序读写的优点。redo日志有简单日志类型和复杂日志类型之分,若干具有关联性的日志可组成日志组,常见的日志组包括Max Row id、聚簇索引和二级索引。redo日志的结构是一个块,包含log block header、log block body、log buffer。一条记录被修改的过程是: 从DB查询并放到BP中,从BP中读取到内存里, 修改记录内容,一种行为是将脏数据放到flush链中; 另一种行为是从BP读取修改后的数据并解析: 事务-->SQL--->MTR-->redo日志--->redo log buffer-->同步刷到磁盘log file(空间小、顺序IO)。 知道redo log file、lsn、flush链表和innodb_flush_log_at_trx_commit 的概念即可。
undo日志
由于事务是要保证原子性的,但是有些情况会造成事务异常或回滚,所以数据库为了解决事务一致性问题而记录的日志,我们就称之为undo log(撤销日志)。由于SELECT操作并不会修改任何记录,所以并不需要记录相应的undo日志。
事务id
就是事务的编号,如下图:

- 什么时候分配事务id呢?
- 只读。针对临时表,MySQL自动生成。若对临时表做了增删改,才会对这个事务分配一个事务id
- 读写。第一次对表中记录做了增删改才会为这个事务分配一个事务id,事务id默认为0,即不修改。
- 如何开启?
- 只读。start transaction read only
- 读写。start transaction read write
- begin/start transaction
- 如何生成和维护事务id?
- 事务id就是⼀个数字,跟row_id的生成方式非常相似。
- 1> 全局变量,记录某个值,当需要获取trx_id的时候,将这个值赋值给trx_id,然后⾃增1
- 2> 每当达到256的倍数的时候,就会把值赋值给MAX TRX ID(在表空间⻚号为5)
几种操作对应的undo日志
对数据的不同操作,有着不同的undo日志去支撑这种操作。
insert操作对应的undo日志:
事务撤销插入记录:

- end of record 本条undo⽇志结束的⻚⾯地址。
- start of record 本条undo⽇志开始的⻚⾯地址。
- undo type 保存对应的undo⽇志类型
- undo no ,undo⽇志记录的编号
- table_id ,表的唯⼀标识
- 主键各列信息
delete操作对应的undo日志:


为什么加了个mark?delete mark+purge
- Info bits ,头结点的前4个⽐特值
- trx_id 旧记录,trx_id的值
- roll_pointer ,指向垃圾链,维护着一条垃圾链以记录被删除的值,与next_record维护的单链表类似
- len of index_col_info :索引列的长度,会加上自身长度
- 索引列各列信息
page free,存在于file page里,可空间复用,以省去空间分配申请的步骤
为什么加了个mark?delete mark+purge
- 阶段一:delete mark,就只将deleted_flag=1
- 阶段二:purge,提交+断开正常记录的指针+将已删除记录指向垃圾链表的头节点
update操作对应的undo日志:

更新日志分为两种情况:
- 不更新主键
- 更新长度一致,则直接复用。 old+new
- 不一致
- 删除
- 申请分配空间(旧长,占用原长且产生碎片;新长,重新申请)
- 更新主键
- delete mark
- 根据更新后的每列的值创建记录,并将其插入到聚簇索引里
undo日志总结
开启事务遇到回滚或异常时,SQL就撤销日志以保证事务的原子性。需要知道事务的定义、何时分配事务id、如何开启事务、如何生成事务id。对数据的不同操作,有着不同的undo日志去支撑这种操作:插入的undo log就是delete;删除的undo log就是insert,分为两步即:delete mark+purge ;修改的undo log则不一定,这取决于所修改的id上一次操作是什么,同时,需要注意更新主键和不更新主键对于更新日志的操作是不同的。
事务
什么是ACID?
- Atomicity 原子性 某个操作,要么全都执行完毕,要么全都回滚。
- Consistency 一致性 数据库中的数据全都符合现实世界中的约束,则这些数据就符合一致性。 比如性别约束男or女;人民币面值不能为负数;出生地址不能为null;等等
- Isolation 隔离性 多个事务访问相同数据时,对该数据不同状态的转换对应的数据库操作的执行顺序 有一定的规律,彼此不干涉。
- Durability 持久性 现实中的状态转换映射到数据库中,意味着对数据所做的修改都应该在磁盘中保存。
事务的状态有哪些?

事务并发执行时数据一致性有哪些问题?
- 脏写
- 脏读
- 如果一个事务读取到了另一个未提交事务修改过的数据,就意味着发生了脏读现象。
- 不可重复读
- 如果一个事务修改了另一个未提交事 务读取的数据,就意味着发生了不可重复读现象
- 虚幻读
- 如果一个事务先根据某些搜索条件(select ... where vip='是')查询了 一些记录,但是在该事务并未提交时, 另一个事务写入了一些符合上面搜索条件的记录(这里的写入可以是 insert、delete、update操作。例如: insert into ... values('0003',700,'是')),就意味着发生了幻读现象。
SQL事务隔离级别

由于无论哪种隔离级别,都不允许脏写的情况发生,所以没有列入到表格中。
注意是SQL事务隔离级别,不仅仅是MySQL !!!!
脏读、不可重复读、幻像读,并不是说一定不能发生,而是要根据业务,有选择性地允许发生,比如在主要是读的情况下,且不允许多个用户同时登录,脏读是允许的,在代码层面就阻止了。
为什么不同隔离级别能解决不同的事务并发问题?这就涉及到MVCC了。
MVCC
MVCC (Multi-Version Concurrency Control) ,即多版本并发控制,利用记录的版本链和ReadView,来控制并发事务访问相同记录时的行为。ReadView即一致性视图,用来判断版本链中的哪个版本是当前事务可见的。
版本链
在每次更新该记录后,都会将旧值放到一条undo日志中。随着更新次数的增多,所有的版本都会被roll_pointer属性连接成一条链表,这个链表就称之为版本链。

ReadView包含的内容
- m_ids 。在生成ReadView时,当前系统中活跃的读写事务的事务id列表,即还未提交。
- min_trx_id 。在生成ReadView时,当前系统中活跃的读写事务中最小的事务id;也就是m_ids中 的最小值。
- max_trx_id 。在生成ReadView时,系统应该分配给下一个事务的事务id值。
- creator_trx_id 。生成该ReadView的事务的事务id。
如何通过ReadView来判断记录的某个版本是可见的?(小于、等于、不在、坚持回溯)
- 如果trx_id == creator_trx_id,则表明当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
- 如果trx_id < min_trx_id,则表明生成该版本的事务在当前事务生成ReadView之前已经提交了,所以该版本可以被当前事务访问。
- 如果trx_id >= max_trx_id,则表明生成该版本的事务在当前事务生成ReadView之后才开启,所以该版本不可以被当前事务访问。
- 如果trx_id in m_ids,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问。
- 如果trx_id not in m_ids,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。
- 如果某个版本的数据对当前事务不可见,那就顺着版本链找到下一个版本的数据,并继续执行上面的步骤来判断记录的可见性,以此类推,直到版本链中的最后一个版本。
下面来测试一下以上理论:
ReadView生成的时机


- READ COMMITTED和REPEATABLE READ隔离级别之间一个非常大的区别就是——它们生成ReadView的时机不同!!
- READ COMMITTED——在一个事务中,每次读取数据前都生成一个ReadView。
- REPEATABLE READ——在一个事务中,只在第一次读取数据时生成一个ReadView。

- 两种情况下生成一个ReadView:
- 情况①:RW的ids为【0,0】
- 情况②:RW的ids为【10,0】
- 情况③:RW的ids为【20】
- 情况④:RW的ids为【】
每次读取数据前都生成一个ReadView,情况③时select操作,则由于已提交的trx_id=10<20,因此可以被看见,即发生了不可重复读的现象;而只在第一次读取数据时生成一个ReadView,由于事务1,2最开始读取的数据对应trx_id=2,小于10,因此即使此时去做select操作,也不会发生不可重复读的问题,这是可重复读的隔离级别的必然结果。
MVCC总结
MVCC是利用记录的版本链和ReadView,来控制并发事务访问相同记录时的行为。版本链就是roll_pointer连接的一条链表,RW可理解为事务id列表的几种id。MySQL就是通过RW的几种id与当前事务id作比较来判断目前事务id访问的版本是否可见,版本可见的情况包括:当前id小于、等于、不在RW版本链中,以及因为坚持回溯版本链最终到可见的地步,共四种情况。每次读取数据前都生成一个ReadView与只在第一次读取数据时生成一个ReadView分别对应了对已提交和可重复读的隔离级别。 |
|