目录
- 1.索引
-
- (1)局部性原理
-
- a.局部性原理在计算机中的地位
- b.page
- [c.池化技术(Buffer Pool)](#c.池化技术(Buffer Pool))
- (2)如何理解索引
- (3)索引的原理
- (4)索引的相关操作
- (5)索引创建原则
- 2.事务
-
- (1)语句分类
- (2)为什么需要事务
- (3)事务的基本理解
- (4)事务的隔离级别
-
- [a.读未提交(Read Uncommitted)](#a.读未提交(Read Uncommitted))
- [b.读提交(Read Committed)](#b.读提交(Read Committed))
- [c.可重复读(Repeatable Read)](#c.可重复读(Repeatable Read))
- d.串行化(Serializable)
- e.四种隔离级别特性总结
- f.全局和当前会话隔离性
- (5)事务的特性
- (6)多版本并发控制(MVCC)
-
- a.多事务执行时的线程安全问题
- b.表的三个隐藏字段
- c.版本控制链形成过程
- [d.读视图(Read View)](#d.读视图(Read View))
-
- ①快照读和当前读
- [②Read View的设计](#②Read View的设计)
- ③确定快照读读到的历史版本的逻辑
- e.RC和RR的本质区别
1.索引
(1)局部性原理
a.局部性原理在计算机中的地位
局部性原理在整个计算机的设计理念中非常重要。在微观设计中,内存和磁盘的读取方式都是一次性读取一大块空间;而在更宏观的视角来看,内存本身就是局部性原理的产物,系统将CPU高频访问和使用的数据从磁盘中提取出来,从而提高访问效率。
MySQL作为数据库,应当有着体现局部性原理的设计,就是索引,它也是MySQL的核心原理。
b.page
page就是上面所讲的微观设计,真正系统读取数据都是一次读取一个范围内的数据,而不会一个字节一个字节地读。 page指的就是MySQL和磁盘进行数据交互的基本单位(16KB --- InnoDB,针对不同存储引擎存在差异),无论读还是写,在MySQL眼里一个原子的大小就是16KB。 和内存、磁盘的设计一致,都是典型的用空间换时间,即局部性原理。
c.池化技术(Buffer Pool)
mysqld在内存中运行的时候,其实会单独为mysqld进程申请一块名为Buffer Pool的的大内存空间,用来进行各种缓存。这种操作就是前面提及的局部性原理,用于减少系统和磁盘IO的次数,提高效率。 就像一个小孩每次都要花钱,还不如先从家长那里取100块,最后不买东西的时候再还回去。
(2)如何理解索引
索引用通俗的话来说就是书前面的目录,用一两页的数据来索引几百页的正文。在MySQL中,索引具体指主键索引(primary key)、唯一键索引(unique)、普通索引(index)、全文索引(fulltext,解决中子文索引问题)。
其中最重要的就是主键索引,这在前面介绍过。但这个东西不是必要的,难道有的表就没有主键索引吗?如果没有主动添加主键,MySQL会生成一个默认主键(根据我们的插入顺序来递增),只不过不会展示出来罢了。
下面是没有主键索引的表,它会默认生成一个主键,我们可以看到显示的顺序是没有规律的,是按照我们插入的顺序显示的。
我们可以加入主键,会发现id变得有序了。但注意,MySQL并不保证select的默认显示一定按照主键排序,这里展示只是说明添加主键后存储结果出现变化,其根源就是索引由默认主键索引变成指定的主键索引。

针对不同列建立主键索引有什么用?使用默认索引不好吗?
我们依然需要用书目录来理解,当我们用id来作为索引时,就相当于用id来建立目录,如 id < 8 的到第7页,id <10 的到第10页,这样的目录才是真正有效的,因为我们指定的主键通常会被我们大量用于定位行。 而默认索引可能会写第18次插入的数据到第7页找,第7次插入的数据到第4页找。这种目录写了跟没写一样,最后系统只能以近乎于遍历的方式去找 id = 8 对应的行。
这里也建议我们指定主键索引时尽量用数字 ,这样省得系统进行转换(和用户名一样,底层都是转为数字进行操作的),也比较符合规范,后面的讲解中我们都默认主键就是数字。
(3)索引的原理
首先我们知道MySQL和磁盘进行IO都是以page为单位进行的,但是我们一直没探讨一个page里面装的到底是什么,索引在哪呢?接下来就会好好讲讲数据是如何存到page里面的,索引是以什么样的方式体现在page里的。
a.page的构成
下面这张表,id是主键索引。接下来将从这张表出发来讲解索引的原理。

下面是其中一个page(16KB)的简单组成,可以看到它由数据部分和指针部分组成,用内核链表的形式将该表的数据连接起来。
数据存储部分使用链表的原因是链表增删快,查找慢,所以我们后续只需要解决查找问题就行了,索引就是解决这个问题的。

由于id是主键,我们可以看到在一个page内主键是有序的,数据插入时也会按照主键进行有序插入。这意味着只要我想访问3,找到1就可以推断出3就在1后面的第3个位置,这种设计是后面索引的关键,就和书的页码是连续的一样,目录里面不会将所有页的信息保存,只会保存一些关键信息。 这里我们要仔细体会。
但是这个结构中并没有目录这一概念的体现,我想要找到3,还是得从1开始线性查找,所以这个page是残缺的,我们需要引入目录部分。

一个page就叫做一个页,当我们找到一个page的时候,我们就去它的页目录部分,如果我们要找2,我们看到目录里面有1和3,我们就访问1,再根据链表去访问2;如果我们要访问4,我们就通过目录直接进入3,再向后去找4。这个时候你或许就明白为什么索引需要有序了,只有这样目录提供的信息才是有效的。
以上就是单个page的完整组成。
b.多层目录
一个page里面就有针对page数据的目录,但这又有一个问题,如果我进入了这个page,但是我想访问6怎么办?我想访问17怎么办?

这说明我们首先要解决的问题是要找到合适的page,页目录只是page内的目录,我们还需要定位page的目录。
这里我们就获得了两个page的目录,它们没有数据部分,只有页目录和指针,页目录是id = 1和id = 6以及id = 11和id = 16。所以当我们在第一个page想要访问1~10中的任意一个数字时,页目录总能告诉它下一个应该找到哪个page。
但是,如果我们想要访问1~20中的任意一个呢?我到底该走左边那个page还是右边那个呢?这个时候我们应该清楚,我们需要造出一棵树,只有从树顶出发,我们才能完全确定后面应该怎么办。每往上走一层,都会对下一层的page做出一层目录。
我们再向上做一层目录。

到这一步,相信很多人就彻底明白这个索引是什么东西了。
如我们想要找17,从第一层的page出发,在其目录里面看到1和11,根据主键索引的有序性,它会直接到11对应的第二层page中。到了第二层之后,看到目录里面有11和16,它就继续找到16对应的第三层page,再在其目录中看到16和18,它就走16,根据链表找到17。这就是基于索引整个查找的流程。在这里我们也能想通,如果使用默认的主键的话,那这棵树将毫无任何规律可言,我们用id来找,但是树的目录却是用我们插入的顺序来建立的,因此查询效率几乎没有提高。
从物理角度来看,存一张表就是将上图中的所有page存到磁盘中。但逻辑上上述的这棵树就叫做B+树。
c.基于B+树的索引
①B+树的特性在索引中的作用
我们来看看这棵B+数有什么特性。首先,真正存的有数据的就是最底下那层,也是我们最初提出的方案。每个page内有页目录,对该page内的有效数据进行索引。但是我们还需要对存的有数据的page本身进行索引,这就是搭建上面两层的核心原因。因此,我们可以将page分成两个种类,一种称为专用于索引的page,一种称为专用于存放数据的page,前者专门用来定位后者,后者里面也有快速定位数据的目录。
其次,同层级的page都是有指针互相连接的。根据主键索引的有序性,当我们第一次访问10的时候从根出发,那如果我接下来要访问11呢? 还从根出发是不是有点浪费时间了? 所以同层级的page用指针相互连接可以增加区域访问数据的效率。
②为什么不用其它数据结构
B+树是对B树的改进。B树的特点是不存在明显的page分类,每个节点内既有指针又有有效数据,并且同层的各个节点之间没有指针连接。使用B树既不满足数据集中存储,也不能实现区域访问,并且由于既存指针又存数据导致目录条数受限,树往往更高,所以我们不使用B树。
二叉树、AVL树层数还是偏高,B+树可以是多叉树,一个page里面的目录可以有几百上千条,压到3、4层就能存取上亿的数据量。而哈希在范围查找上也不是很好,所以大部分情况下B+树就是最优解。
当然MySQL官方是支持哈希建立索引的,但主流的存储引擎InnoDB和MyISAM都不支持,所以我们暂时不考虑这些。
③聚簇索引和非聚簇索引
MyISAM和InnoDB都是采用的B+树,但MyISAM使用的是聚簇索引,InnoDB是非聚簇索引。
对于这两个存储引擎来说,上层的page都是专用于索引的,最底层的page专用于存储数据,但是非聚簇索引MyISAM还会做一层分离,最底层page存的数据实际上是有效数据的地址,这样的话在宏观看来我们就可以把整棵B+树都当做索引用的。而聚簇索引InnoDB存的就是数据本身,这样看来整棵B+树上层用于索引,最底层用于数据存储。 因此聚簇指的就是索引和存储相结合,非聚簇是索引和存储分离。
d.辅助索引
经过上面的讲解,我们可以说对主键索引已经有了很深的认识。但我们还需要思考一个问题,同一张表还可以有唯一键、普通索引,一张表怎么支撑多个索引呢?
对于MyISAM,它的B+树的最底层存的是有效数据的地址,索引和数据分离(非聚簇索引),所以辅助索引就是创建一棵新的同结构B+树,和主键索引的树没有任何区别。
InnoDB就没这么好操作了,因为InnoDB的主键索引B+树的最底层是有效数据而不是地址。如果多个索引创建多棵同结构的B+树的话,那就意味着数据会成倍地增加。所以InnoDB的辅助索引和主键索引肯定有所不同。
**解决办法是辅助索引的B+树最底层存的是主键值而非完整数据。当我们使用辅助索引时先通过B+树找到主键值,再到主键索引的B+树找到完整数据,整个过程需要两次索引,这个过程称为回表查询。**这样就既解决了同一张表多个索引的问题,也避免了聚簇索引同结构B+树导致的多份冗杂数据的存储。
(4)索引的相关操作
a.创建主键索引
一张表中最多有一个主键索引,可以是复合主键。主键索引的优势是效率高(InnoDB只需要查找一棵B+树即可找到)。但注意创建主键索引的列,它的值不能为null,且不能重复。主键索引的列基本上是数字
sql
-- 在创建表的时候,直接在字段名后指定 primary key
create table user1(id int primary key, name varchar(30));
-- 在创建表的最后,指定列为主键索引
create table user2(id int, name varchar(30), primary key(id));
-- 创建表以后再添加主键
create table user3(id int, name varchar(30));
alter table user3 add primary key(id);
b.创建唯一索引
一张表中,可以有多个唯一索引 。如果在某一列建立唯一索引,必须保证这列不能有重复数据,但可以可以为空,B+树自然会留出特殊区域用于null的查询。
sql
-- 在表定义时,在某列后直接指定unique唯一属性
create table user4(id int primary key, name varchar(30) unique);
-- 创建表时,在表的后面指定列为unique
create table user5(id int primary key, name varchar(30), unique(name));
-- 创建表以后再添加unique
create table user6(id int primary key, name varchar(30));
alter table user6 add unique(name);
c.普通索引的创建
一张表中可以有多个普通索引(在实际开发中用的比较多),如果经常使用某列来进行查询等,但是该列有重复的值,那么我们就应该使用普通索引。
sql
--在表的定义最后,指定某列为索引
create table user8(id int primary key,
name varchar(20),
email varchar(30),
index(name)
);
--创建完表以后指定列为普通索引
create table user9(id int primary key, name varchar(20), email varchar(30));
alter table user9 add index(name);
-- 创建一个索引名为 idx_name 的索引
create table user10(id int primary key, name varchar(20), email varchar(30));
create index idx_name on user10(name);
d.全文索引的创建
当对文章大量字段进行检索时,会使用到全文索引。但存储引擎必须是MyISAM,且默认的全文索引仅支持英文,不支持中文。如果对中文进行全文检索,可以使用sphinx的中文版。
sql
-- 对title和body创建全文索引
create table articles (
id int unsigned auto_increment not null primary key,
title varchar(200),
body text,
fulltext (title, body)
)engine=MyISAM;
e.索引查询
我们怎么知道这张表有多少索引,有没有快速的工具来进行查看呢?
sql
-- 向指定的表获取索引信息
show keys from user;

f.删除索引
sql
-- 删除主键索引
alter table user1 drop primary key;
-- 普通索引的删除
alter table user10 drop index idx_name; # 索引名就是show keys from 表名中的 Key_name 字段
drop index name on user8; # drop index 索引名 on 表名
(5)索引创建原则
1.比较频繁的作为查询条件的字段应该创建索引
2.唯一性太差的字段不适合单独创建索引
3.更新非常频繁的字段不适合作创建索引
4.不会出现在where子句中的字段不该创建索引
2.事务
show engines可以查看MySQL上的存储引擎信息,只有InnoDB才支持事务。MyISAM不支持,所以后续的讲解都是建立在InnoDB之上的。
(1)语句分类
在MySQL中不同关键字对应的语句有自己的种类:
DDL(data definition language)数据定义语言,用来维护存储数据的结构
代表指令: create,drop,alter
DML(data manipulation language)数据操纵语言,用来对数据进行操作
代表指令: insert,delete,update
DML中又单独分了一个DQL,数据查询语言
代表指令: select
DCL(Data Control Language)数据控制语言,主要负责权限管理和事务
代表指令: grant,revoke,commit
我们完成的所有功能,都是由上述几种语句经过对应的排列组合完成的。简单来说,事务就是对单条或一组语句的包装和管理,后面会详细展开。
(2)为什么需要事务
Linux遇到多线程时为什么要考虑加锁?就是防止一个函数在极短时间内被两个执行流进入,从而导致逻辑出现错误,例如买票系统,两个执行流同时发现还剩一张票,于是都被if允许进入,导致最终系统剩余票数为负数。这说明在多执行流的时候临界资源一定要进行保护,要有相应的防范措施。数据库被多执行流访问是肯定的,因此MySQL中急需处理多执行流带来的各种问题,事务就是最终的解决方案。
(3)事务的基本理解
a.begin和commit、回滚
当我们写下begin;之后,我们就创建了一个事务,通过commit;终止,在这中间我们可以任意执行命令。 我们也可以创建回滚点(savepoint),只要没有commit,我们可以在任何时候回滚到想要的地方(rollback to默认回滚到begin处)。**如果在会话commit前就退出了,会默认回滚到begin处,等于该事务什么都没干。

我们在begin和commit间执行的DML语句都可以认为是临时的,可以被回滚的,并且在ctrl + \异常推出时会自动回滚到begin处。但注意,这只针对DML语句,DDL、DCL语句在MySQL中会隐式commit,即就算没有commit,这个数据也持久的保存起来了,且无法被回滚。
b.autocommit
那个begin和commit哪来的?之前怎么没发现它们的存在?事实上,如果没有显式写begin和commit,我们执行的每一条语句都被隐式创建了一个事务,我们可以简单理解成每条语句都被单独用隐藏的begin和commit包装了起来。因此,默认执行时就算崩溃,之前的DML都不会失效,是因为它们早就被单独当作一个事务commit了。
sql
begin; # 被隐藏
insert into tb21 values (1);
commit; # 被隐藏
这个隐藏的begin和commit由autocommit决定,我们可以将其设置为0,这样我们就需要手动commit(不需要手写begin),如果没有commit,那么退出后我们的DML语句就都失效了。所以我们一般就默认保证autocommit为1就行。

c.事务的保护对象
到这里,我们或许能意识到,事务存在的核心任务是保护DML语句,而DDL和DCL多少会出现隐式commit导致,甚至不受事务隔离等级的约束。因为事务更多地是针对数据本身进行保护,而DDL和DCL是针对数据结构及其权限进行操作。至于DQL不对数据进行任何修改,不在讨论范围内。
(4)事务的隔离级别
根据上述的讲解,我们对事务这个概念有了基本的理解。我们执行的每一个DML语句都是被包装在begin和commit之间的,在commit之前都可称为临时数据,提交后才可持久。
但是如果有两个及以上的事务同时运行,其中一个事务修改了数据,其它事务应不应该看到呢?如果能看到,那这是否说明不同事务之间就存在执行顺序的依赖关系了呢?于是我们需要隔离级别来管理它们。
隔离等级至少在两个会话中才能体现,即多执行流访问临界资源的情况。
a.读未提交(Read Uncommitted)
读未提交展开成一句话就是 "一个事务可随时读到另一个事务位于begin和commit之间指令执行后的结果" ,举个例子就是A会话正处于begin和commit之间,仍未提交,即事务正在处理中,在这个时候A更新了一个数据,这时B会话就能通过select马上看到这个变化,即B能读到A未提交的数据(脏读)。
实际生产中几乎不可能用这种隔离级别,后面提到的隔离级别所有的缺点在RU这里占完了,还独占个脏读的特性。
b.读提交(Read Committed)
该隔离级别是大多数数据库的默认的隔离级别,但不是MySQL的默认的隔离级别。 一个事务只能看到其他的已经提交的事务所做的改变。
这种隔离级别会导致一个问题,即一个事务执行时,如果多次 select,可能得到不同的结果,因为事务执行期间可能有其他事务提交了,提交的数据中干扰了该事务的读取结果。这个问题叫不可重复读。
c.可重复读(Repeatable Read)
这是 MySQL 默认的隔离级别,它确保同一个事务在执行中多次读取数据时,一定会看到同样的数据,就算其它事务事实上已经修改了该事务读取的数据。
在理想情况下,可重复读意味着启动事务的一瞬间,在这个事务眼里整个数据库的数据都是静态的。 大多数情况却是如此,但是对于有的数据库,如果出现了insert这条DML语句,那么insert的结果还是会被看到,这种现象叫做幻读,MySQL解决了幻读。
d.串行化(Serializable)
这是事务的最高隔离级别,InnoDB通过行级锁(共享锁)实现 ,强制事务排序,使之不可能相互冲突。意思是当有两个事务修改同一块资源时,整个mysqld同时只允许一条事务执行,就算它们来自不同会话。这解决了所有数据库的幻读问题。
**在MySQL中,两个事务不能同时对同一行数据一边读一边写(会加锁读),一边写一边写,只有两边都在读才不会触发串行化。**只要还没有commit就视为正在执行事务,这个时候锁就一直在该事务手上,其它事务无法访问,所以串行化被触发的概率并不低。
串行化可能会导致超时和锁竞争,这种隔离级别太极端,实际生产基本不使用。
e.四种隔离级别特性总结
下面是对上述4种隔离级别的特性总结

f.全局和当前会话隔离性
我们怎么查看设置当前会话的隔离性呢?
全局隔离性是global.transaction_isolation,它被写在配置文件中,每创建一个会话默认都是这个隔离性,和环境变量一样。
当前会话隔离性是session.transaction_isolation,每个会话可以临时更换而不影响全局设置。
sql
-- 查看全局隔离级别
select @@global.transaction_isolation;
-- 查看当前会话隔离级别
select @@session.transaction_isolation;
-- 查看当前会话隔离级别的简写
select @@transaction_isolation;
修改全局隔离性有个易错点,即通过SQL语句修改的全局隔离性在mysqld重启后会恢复原样,因为本质上SQL语句不会修改对应的配置文件。当然,mysqld一般启动了就很少关,在短时间内修改新连接会话的隔离性是可行的。 session隔离性一般是马上生效,实在不行就改global然后重启会话。
sql
-- 修改当前会话隔离级别
set session transaction isolation level read committed;
-- 修改全局隔离级别
set global transaction isolation level read committed;
(5)事务的特性
通过上述的讲解,事务是个什么基本上我们已经了解的差不多了,接下来就根据它是什么来总结它实现的特性,这一步总结可以帮我们加深理解事务。
a.原子性(A)
一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。如果事务在执行过程中发生了错误,就会被回滚(rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
在上面例子中也深刻体现这一点,在begin和commit中间我们可以随意执行DML,但是只有提交上去之后数据才会被持久的保存下来,如果中途退出的话该事务的所有DML全部失效,这就是原子性的体现。
b.持久性(D)
经过前面的渗透,这个概念应该不难理解了。当事务处理结束后(commit),对数据的修改就是永久的,即便系统故障也不会丢失,在commit前就是临时保存的,不具备持久性。
c.隔离性(I)
这就是前面讲过的事务的隔离级别体现出来的特性,**数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。**事务隔离分为不同级别,包括读未提交RU、读提交RC、可重复读RR和串行化S。
d.一致性(C)
一致性不对应具体的用法,这个性质其实是由前三个性质共同维护的。
无论是事务开始前和事务结束以后,数据库的完整性没有被破坏,并且其写入的数据完全符合所有的预设规则(原子性、持久性、隔离性),保证后续操作的顺利。
另一种说法是,事务执行的结果,使得MySQL从一个一致性状态,变到另一个一致性状态。当事务成功提交时,数据库一直处于一致性状态(并行时有隔离性和持久性维护)。如果事务中断,这个时候会通过原子性来保障一致性,否则我们不清楚此时到底写了多少数据到mysqld里面。
其实一致性和用户的业务逻辑强相关,需要用户业务逻辑做支撑,即一致性是由用户决定的。
(6)多版本并发控制(MVCC)
观察这四种隔离级别,它们是如何做到的?DML语句修改了数据之后凭什么其他事务观察不到变化,其底层是什么?这就是下面要讨论的多版本并发控制(MVCC)
a.多事务执行时的线程安全问题
隔离级别根本上是为了解决什么问题?
例如,有两个事务正同时运行,其中一个事务修改了一个数据,另一个事务该不该看到?其中一个事务修改了一个数据,另一个事务能不能继续修改?
这其实就是读-读,读-写,写-写问题。
读-读 : 不存在任何问题,不需要任何并发控制,因为DQL未对数据进行修改等操作。
读-写 : 有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读。RC、RR和S是对读写问题进行不同级别的处理。
写-写 : 有线程安全问题,可能会存在更新丢失问题,需要加锁处理,RC、RR、S都有对应的加锁机制。
我们后面重点从读-写问题出发,探讨RR和RC的如何实现版本控制及快照读。
b.表的三个隐藏字段
我们创建一张表后除了设定的列,还自带有隐藏列
DB_ROW_ID:隐含的自增ID(隐藏主键) ,如果表中没有主键,InnoDB会自动生成一个聚簇索引,在索引部分已经提及
DB_TRX_ID: 最近修改该行的事务ID
DB_ROLL_PTR: 回滚指针,指向这条记录的上一个版本(历史版本保存在undo log中)
其中undo日志就是mydqld独立申请的一段内存缓冲区,和Buffer Pool申请的空间不同。
MySQL以服务进程的方式在内存中运行。表数据、索引是在Buffer Pool中缓存的,在合适的时候将数据刷新到磁盘当中的;undo日志则是保存临时数据和进行版本控制,保存在独立的缓冲区中。
以上三个隐藏字段是版本控制链中的核心成员,后面会进一步展开。
c.版本控制链形成过程
①加锁并备份
当我们有一条数据即将被修改时,就要开始创建版本控制链了,即进行MVCC
第一步就是对该行进行加锁,只有一个执行流当前能对该行进行操作。该行会从Buffer Pool中拷贝一份到undo log中,注意Buffer Pool专用于数据和索引的缓存,undo log独立用于版本控制等。 注意这个拷贝过程可以是写时拷贝,即用到的时候再真正拷贝,这里我们就按普通拷贝理解就行。

②修改指定列和隐藏列字段
接下来就根据我们的指令修改指定列和隐藏列,事务ID即造成数据修改的事务的ID,回滚指针指向上一条undo log里面的记录。

③释放锁
只有当事务提交之后,该行的锁才会被释放,也就是说在这期间其它事务无法来再次修改该行的数据,这样的话写-写问题就解决了。 可以看出RR和RC也都是有锁的。
④版本链继续加长
按照上面的逻辑,事务3又来修改该行了。按照加锁备份,修改数据和隐藏列,释放锁的顺序,我们可以不断加长版本链,每次都能完整保存该条数据是由谁修改的。

这样,我们就有了一条基于链表记录的历史版本链。回滚,其实就是用历史数据覆盖当前数据。上面的一个一个版本,我们可以称之为一个一个的快照。
这里有个细节,就是事务只要提交了就没办法回滚,在实际操作中,上图版本链中可能多条都对应一个事务ID,并且事务提交后undo log并一定马上清空。
⑤delete的版本控制说明
delete也都有自己的版本控制方式,delete其实不会真正删除数据,而是会置标签flag为0,就相当于删除了,这样的话回滚也能找到之前的删除的数据。
d.读视图(Read View)
版本链形成了,那RR和RC是如何利用版本链来确定当前读到的数据应该是什么样的呢? 这就是读视图干的事了。
①快照读和当前读
快照读:读取数据快照(历史版本),而非数据的最新版本。读取历史版本可以并行执行
当前读:读取数据的最新版本,并对读取的行加锁,防止其他事务并发修改。由于加锁,当前读是串行的。 这难道不就是前面版本链中遇到的吗?所以涉及到增删改操作的都是当前读,它们的执行也都是串行的。我们要知道修改的第一步就是读到数据,所以说增删改是当前读并不意外,它们会在当前读后进行下一步操作。
至于select是当前读还是快照读,需要根据隔离级别确定。
②Read View的设计
快照读是读取历史数据的,但是读取哪一个历史数据呢?这就是Read View要解决的问题。
当执行快照读操作时,MySQL会为当前事务生成一个读视图的对象,为我们判断当前能读到哪些数据。
下面是读视图对应类设计的简化版。
cpp
class ReadView
{
private:
int m_low_limit_id; // 高水位,大于等于这个ID的事务均不可见
int m_up_limit_id; // 低水位:小于这个ID的事务均可见
int m_creator_trx_id; // 创建该 Read View 的事务ID
int m_ids[100]; // 创建视图时的活跃事务id列表,这里int[100]只是方便理解
// 配合purge,标识该视图不需要小于m_low_limit_no的undo log,
// 如果其他视图也不需要,则可以删除小于m_low_limit_no的undo log
int m_low_limit_no;
bool m_closed; // 标记视图是否被关闭
};
下面是对其中重要字段的讲解:
m_ids是一张列表,用来维护Read View生成时系统正在活跃的事务ID,说明当前事务执行select时列表里的事务仍未commit,这张表是核心。
m_up_limit_id记录m_ids列表中事务ID最小的ID,注意是最小的事务ID 。
m_low_limit_id记录了ReadView生成时系统尚未分配的下一个事务ID,也就是目前正在执行的事务ID的最大值+1 。
m_creator_trx_id指的是创建该ReadView的事务ID,也是执行select语句的事务ID。
③确定快照读读到的历史版本的逻辑
读取版本链的时候,在undo log中我们能找到每个版本及其对应修改的事务ID。根据当前快照读创建的ReadView中的当前事务ID和版本链中记录修改事务ID,再结合事务ID递增的特点,我们能够很好的解决快照读应该读哪个历史版本的问题。

当执行快照读时,先创建ReadView并获取最大最小事务ID,之后去找要访问的数据对应的版本链,从最新的开始往前回溯,通过节点ID和当前活跃ID进行比较找到能够访问的最新的历史记录。
其中节点ID位于正在运行的事务ID区间内但不在m_ids的原因是有的事务执行时间长,导致m_up_limit_id偏低,有部分事务提前退出,这部分事务又实实在在地先于ReadView提交,所以我们还是要读。
e.RC和RR的本质区别
快照读会先生成ReadView,判断后返回可以读到的历史版本。那么RR和RC的区别到底在哪呢?
**RR级别下ReadView只会在第一次调用快照读(select)的地方创建(注意不是begin开始处),第二次第三次都是用同一张ReadView。**这意味着早于ReadView创建的事务所做的修改均是可见的,所以RR级别可能看到ID比自己还大的事务做出的修改。只要这个事务晚于begin时创建,但又早于ReadView创建时提交就会遇到这种情况。
而RC级别下的事务每次快照读都会生成一个新的ReadView,因此能一直获取最新的commit后的结果,这也是它不可重复读的根本原因。
ReadView是限制能看见哪些事务的根本,RR会在调用时生成一次并用到结束,RC却会在调用出时刻更新,这就是RC和RR的底层区别。