点击蓝色“程序员DMZ ”关注我哟html
好看记得加个“星标”哈!程序员
前言
web
本专题大纲:面试

我从新整理了大纲,思考了好久,决定单独将MySQL的事务实现原理跟Spring中的事务示例分为两篇文章,由于两者毕竟没有什么实际关系,实际上若是你对MySQL的事务原理不感兴趣也能够直接跳过本文,等待接下来两篇应用及源码分析,不过我以为知识的学习应该慢慢行成一个体系,为了创建一个完善的体系应该要对数据库自己事务的实现有必定认知才行。sql
本文为Spring事务专题
第三篇,在前两篇文章中咱们已经对Spring中的数据访问有了必定的了解,那么从本文开始咱们正式接触事务,在分析Spring中事务的实现以前咱们应该要对事务自己有必定的了解,同时也要对数据库层面的事务如何实现有必定了解。话很少说,咱们开始正文数据库
本文大纲:缓存

初识事务
为何须要事务?
这里又要掏出那个烂大街的银行转帐案例了,以A、B两个帐户的转帐为例,假设如今要从A帐户向B帐户中转入1000员,当进行转帐时,须要先从银行帐户A中取出钱,而后再存入银行帐户B中,SQL
样本以下:微信
// 第一步:A帐户余额减小减小1000
update balance set money = money -500 where name= ‘A’;
// 第二步:B帐户余额增长1000
update balance set money = money +500 where name= ‘B’;
若是在完成了第1步的时候忽然宕机了,A的钱减小了而B的钱没有增长,那A岂不是白白丢了1000元,这时候就须要用到咱们的事务了,开启事务后SQL
样本以下:并发
// 第一步:开始事务
start transaction;
// 第二步:A帐户余额减小减小1000
update balance set money = money -500 where name= ‘A’;
// 第三步:B帐户余额增长1000
update balance set money = money +500 where name= ‘B’;
// 第四步:提交事务
commit;
什么是事务
事务(Transaction)是访问和更新数据库的程序执行单元;事务中可能包含一个或多个sql语句,这些语句要么都执行成功,要么所有执行失败。app
事务的四大特性(ACID)
-
原子性(Atomicity,或称不可分割性)
「一个事务必须被视为一个不可分割的最小工做单元,整个事务中全部的操做要么所有提交成功,要么所有失败回滚,对于一个事务来讲,不可能只执行其中的一部分操做,这就是事务的原子性」
-
一致性(Consistency)
「数据库老是从一个一致性的状态转换到另一个一致性的状态,在事务开始以前和以后,数据库的完整性约束没有被破坏。在前面的例子中,事务结束先后A、B帐户总额始终保持不变」
-
隔离性(Isolation)
「隔离性是指,事务内部的操做与其余事务是隔离的,并发执行的各个事务之间不能互相干扰。严格的隔离性,对应了事务隔离级别中的Serializable (可串行化),但实际应用中出于性能方面的考虑不多会使用可串行化。」
-
持久性(Durability)
「持久性是指事务一旦提交,它对数据库的改变就应该是永久性的。接下来的其余操做或故障不该该对其有任何影响。」
事务的隔离级别
在前文中咱们介绍了隔离性,但实际上隔离性比想象的要复杂的多。在SQL标准中定义了四种隔离级别,每一种隔离级别都规定了一个事务所作的修改,哪些在事务内和事务间是可见的,哪些是不可见的,较低级别的隔离一般能够执行跟高的并发,系统的开销也更低
未提交读(READ UNCOMMITTED)
在这个隔离级别下,事务的修改即便没有提交,对其余事务也是可见的。事务能够读取未提交的数据,这也被称之为脏读
。这个级别会带来不少问题,从性能上来讲,READ UNCOMMITTED
不会比其余的级别好太多,可是却会带来不少问题,除非真的有很是必要的理由,在实际应用中通常不多使用。
提交读(REDA COMMITED)
大多数数据系统的默认隔离级别都是REDA COMMITED
(MySql不是),REDA COMMITED
知足前面提到的隔离性的简单定义:一个事务开始时,只能看到已经提交的事务所作的修改。换句话说,一个事物从开始直到提交前,所作的修改对其余事务不可见。这个级别有时候也叫作不可重复读
,由于执行两次相同的查询可能会获得不一样的结果。
可重复读(REPEATABLE READ)
REPEATABLE READ
解决了脏读
以及不可重复度的问题
。该级别保证了同一个事务屡次读取一样记录的结果是一致的。可是理论上,可重复度仍是没法解决另一个幻读
的问题。所谓幻读,指的是当某个事务在读取某个范围内的记录时,另一个事务又在该范围内插入了新的记录,当以前的事务再次读取该范围的记录时,就会产生幻行。
不可重复读跟幻读的区别在于,「前者是数据发生了变化,后者是数据的行数发生了变化」。
可串行化(SERIALIZABLE)
SERIALIZABLE
是最高的隔离级别,它经过强制事务串行执行,避免前面说的幻读。简单来讲SERIALIZABLE
会在读取的每一行数据上都加锁,因此可能会致使大量的超时和锁争用的问题。实际应用中也不多使用这个隔离级别,只有在很是须要确保数据一致性并且能够接受没有并发的状况下,才考虑此级别。
保存点
咱们能够在事务执行的过程当中定义保存点,在回滚时直接指定回滚到指定的保存点而不是事务开始之初,有点像咱们玩游戏的时候能够存档而不是每次都要从新再来
定义保存点的语法以下:
SAVEPOINT 保存点名称;
当咱们想回滚到某个保存点时,可使用下边这个语句(下边语句中的单词WORK
和SAVEPOINT
是无关紧要的):
ROLLBACK [WORK] TO [SAVEPOINT] 保存点名称;
MySQL中的事务跟原理
MySQL中的事务
-
「MySQL中不是全部的存储引擎都支持事务」,例如
MyISAM
就不支持事务,实际上支持事务的只有InnoDB
跟NDB Cluster
,「本文关于事务的分析都是基于InnoDB
」 -
「MySQL默认采用的是自动提交的方式」,也就是说若是不是显示的开始一个事务,则系统会自动向数据库提交结果。在当前链接中,还能够经过设置AUTOCONNIT变量来启用或者禁用自动提交模式。
-
开启自动提交功能
SET AUTOCOMMIT = 1;
MySQL中默认状况下的自动提交功能是已经开启的。
-
关闭自动提交功能。
SET AUTOCOMMIT = 0;
关闭自动提交功能后,只用当执行COMMIT命令后,MySQL才将数据表中的资料提交到数据库中。若是执行ROLLBACK命令,数据将会被回滚。若是不提交事务,而终止MySQL会话,数据库将会自动执行回滚操做。
-
「MySQL的默认隔离级别是可重复读(REPEATABLE READ)」。
事务的实现原理
咱们要探究MySQL
中事务的实现原理,实际上就是要弄明天它的ACID
特性是如何实现的,在这里有必要先说明的是,「ACID
中的一致性是事务的最终目标,前面提到的原子性、持久性和隔离性,都是为了保证数据库状态的一致性」。因此咱们要分析的就是MySQL
的原子性、持久性和隔离性的实现原理,在分析事务的实现原理以前咱们须要补充一些InnoDB
的相关知识
-
InnoDB
是一个将表中的数据存储到磁盘上的存储引擎,因此即便关机后重启咱们的数据仍是存在的。而真正「处理数据的过程是发生在内存中的」,「因此须要把磁盘中的数据加载到内存中,若是是处理写入或修改请求的话,还须要把内存中的内容刷新到磁盘上」。而咱们知道读写磁盘的速度很是慢,和内存读写差了几个数量级,因此当咱们想从表中获取某些记录时,InnoDB
存储引擎须要一条一条的把记录从磁盘上读出来么?不,那样会慢死,InnoDB
采起的方式是:「将数据划分为若干个页,以页做为磁盘和内存之间交互的基本单位,InnoDB中页的大小通常为 16 KB。也就是在通常状况下,一次最少从磁盘中读取16KB的内容到内存中,一次最少把内存中的16KB内容刷新到磁盘中。」 -
咱们还须要对MySQL中的日志有必定了解。MySQL的日志有不少种,如二进制日志(bin log)、错误日志、查询日志、慢查询日志等,此外InnoDB存储引擎还提供了两种事务日志:「redo log(重作日志)和undo log(回滚日志)。其中redo log用于保证事务持久性;undo log则是事务原子性和隔离性实现的基础。」
-
InnoDB做为MySQL的存储引擎,数据是存放在磁盘中的,但若是每次读写数据都须要磁盘IO,效率会很低。为此,InnoDB提供了「缓存(Buffer Pool)」,Buffer Pool中包含了磁盘中部分数据页的映射,做为访问数据库的缓冲:「当从数据库读取数据时,会首先从Buffer Pool中读取,若是Buffer Pool中没有,则从磁盘读取后放入Buffer Pool;当向数据库写入数据时,会首先写入Buffer Pool,Buffer Pool中修改的数据会按期刷新到磁盘中(这一过程称为刷脏)。」
-
InnoDB
存储引擎文件主要能够分为两类,表空间文件及重作日志文件(redo log file),表空间文件又能够细分为两类,共享表空间跟独立表空间。「undo log位于共享表空间中的undo段中」,每一个表空间都被划分红了若干个页面,「凡是页面的读写都在buffer pool中进行,这意味着undo log也须要先写入到buffer pool,因此undo log的生成也须要持久化,也就是说undo log的生成须要记录对应的redo log」。(注意:不是全部的undo log的生成都会产生对应的redo log,对于操做临时表生成的undo log并不会生成对应的undo log,由于修改临时表而产生的undo日志
只须要在系统运行过程当中有效,若是系统奔溃了,那么在重启时也不须要恢复这些undo
日志所在的页面,因此在写针对临时表的Undo页面
时,并不须要记录相应的redo日志
。)
持久性实现原理
经过前面的补充知识咱们知道InnoDB引入了Buffer Pool
来优化读写的性能,可是虽然Buffer Pool
优化了性能,但同时也带来了新的问题:「若是MySQL宕机,而此时Buffer Pool中修改的数据尚未刷新到磁盘,就会致使数据的丢失,事务的持久性没法保证」。
基于此,redo log
就诞生了,「redo log是物理日志,记录的是数据库中数据库中物理页的状况」,redo log包括两部分:一是内存中的日志缓冲(redo log buffer),该部分日志是易失性的;二是磁盘上的重作日志文件(redo log file),该部分日志是持久的。在概念上,innodb经过「force log at commit」机制实现事务的持久性,即在事务提交的时候,必须先将该事务的全部事务日志写入到磁盘上的redo log file和undo log file中进行持久化。
看到这里可能有的小伙伴又会有疑问了,既然redo log也须要在事务提交时将日志写入磁盘,为何它比直接将Buffer Pool中修改的数据写入磁盘(即刷脏)要快呢?主要有如下两方面的缘由:
(1)刷脏是随机IO,由于每次修改的数据位置随机,但写redo log是追加操做,属于顺序IO。
(2)刷脏是以数据页(Page)为单位的,MySQL默认页大小是16KB,一个Page上一个小修改都要整页写入;而redo log中只包含真正须要写入的部分,无效IO大大减小。
这里我以文章开头的例子进行说明redo log
为什么能保证持久性:
// 第一步:开始事务
start transaction;
// 第二步:A帐户余额减小减小1000
update balance set money = money -500 where name= ‘A’;
// 第三步:B帐户余额增长1000
update balance set money = money +500 where name= ‘B’;
// 第四步:提交事务
commit;

❝这里须要对redo log的刷盘补充一点内容:
MySQL支持用户自定义在commit时如何将log buffer中的日志刷log file中。这种控制经过变量 innodb_flush_log_at_trx_commit 的值来决定。该变量有3种值:0、一、2,「默认为1」。但注意,这个变量只是控制commit动做是否刷新log buffer到磁盘。
当设置为1的时候,事务每次提交都会将log buffer中的日志写入os buffer并调用fsync()函数刷到log file on disk中。这种方式即便系统崩溃也不会丢失任何数据,可是由于每次提交都写入磁盘,IO的性能较差。 当设置为0的时候,事务提交时不会将log buffer中日志写入到os buffer(内核缓冲区),而是每秒写入os buffer并调用fsync()写入到log file on disk中。也就是说设置为0时是(大约)每秒刷新写入到磁盘中的,当系统崩溃,会丢失1秒钟的数据。 当设置为2的时候,每次提交都仅写入到os buffer,而后是每秒调用fsync()将os buffer中的日志写入到log file on disk。 「能够看到设置为0或者2时,都有可能丢失1s的数据」
❞
原子性实现原理
前面提到了,所谓原子性就是指整个事务是一个不可分隔的总体,组成事务的一组SQL要么所有成功,要么所有失败,要达到这个目的就意味着当某一个SQL执行失败时,咱们要可以撤销掉其它SQL的执行结果,在MySQL中这是依赖undo log(回滚日志)
来实现。
undo log属于「逻辑日志」(前面提到的redo log属于物理日志,记录的是数据页的状况),咱们能够这么认为,「当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录。」
但执行发生异常时,会根据undo log中的记录进行回滚。undo log主要分为两种
-
insert undo log -
update undo log
「insert undo log是指在insert 操做中产生的undo log」,由于insert操做的记录,只对事务自己可见,对其余事务不可见。故该undo log能够在事务提交后直接删除,不须要进行purge操做。
「而update undo log记录的是对delete 和update操做产生的undo log」,该undo log可能须要提供MVCC机制,所以不能再事务提交时就进行删除。提交时放入undo log链表,等待purge线程进行最后的删除。
❝补充:purge线程两个主要做用是:清理undo页和清除page里面带有Delete_Bit标识的数据行。在InnoDB中,事务中的Delete操做实际上并非真正的删除掉数据行,而是一种Delete Mark操做,在记录上标识Delete_Bit,而不删除记录。是一种"假删除",只是作了个标记,真正的删除工做须要后台purge线程去完成。
❞
这里咱们就来看看insert undo log的结构,以下:

在上图中,undo type
记录的是undo log的类型,对于insert undo log
,该值始终为11(TRX_UNDO_INSERT_REC
),undo no
在一个事务中是从0
开始递增的,也就是说只要事务没提交,每生成一条undo日志
,那么该条日志的undo no
就增1。table id记录undo log所对应的表对象。若是记录中的主键只包含一个列,那么在类型为TRX_UNDO_INSERT_REC
的undo日志
中只须要把该列占用的存储空间大小和真实值记录下来,若是记录中的主键包含多个列(复合主键),那么每一个列占用的存储空间大小和对应的真实值都须要记录下来(图中的len
就表明列占用的存储空间大小,value
就表明列的真实值),「在回滚时只须要根据主键找到对应的列而后删除便可」。end of record记录了下一条undo log在页面中开始的地址,start of record记录了本条undo log在页面中开始的地址。
对undo log有必定了解后,咱们再回头看看文章开头的例子,分析下为何undo log能保证原子性
// 第一步:开始事务
start transaction;
// 第二步:A帐户余额减小减小1000
update balance set money = money -500 where name= ‘A’;
// 第三步:B帐户余额增长1000
update balance set money = money +500 where name= ‘B’;
// 第四步:提交事务
commit;

考虑到排版,这里我只画了一条语句的流程图,第二条也是同样的,每次更新或者插入前,先记录undo,再修改内存中数据,再记录redo。
隔离性实现原理
咱们知道,一个事务中的读操做是不会影响到另一个事务的,因此在讨论隔离性咱们主要分为两种状况
-
一个事务中的写操做,对另一个事务中写操做的影响 -
一个事务中的写操做,对另一个事务中读操做的影响
写操做之间的隔离是经过锁来实现的,MySQL中的锁机制要详细来说是很复杂的,要讲明白整个锁须要从索引开始介绍,限于笔者能力及文章篇幅,本文只对MySQL中的锁机制作一个简单的介绍
MySQL中的锁机制(InnoDB)
读锁跟写锁
-
读锁
又称为
共享锁`,简称S锁,顾名思义,共享锁就是多个事务对于同一数据能够共享一把锁,「都能访问到数据,可是只能读不能修改。」 -
写锁
又称为
排他锁`,简称X锁,顾名思义,排他锁就是不能与其余所并存,如一个事务获取了一个数据行的排他锁,其余事务就不能再获取该行的其余锁,包括共享锁和排他锁,可是获取排他锁的事务是能够对数据就行读取和修改。
行锁跟表锁
-
表锁在操做数据时会锁定整张表,并发性能较差;
-
行锁则只锁定须要操做的数据,并发性能好。
-
可是因为加锁自己须要消耗资源(得到锁、检查锁、释放锁等都须要消耗资源),所以在锁定数据较多状况下使用表锁能够节省大量资源。MySQL中不一样的存储引擎支持的锁是不同的,例如MyIsam只支持表锁,而InnoDB同时支持表锁和行锁,且出于性能考虑,绝大多数状况下使用的都是行锁。
意向锁
-
意向锁分为两种,意向读锁(IS)跟意向写锁(IX)
-
意向锁是表级别的锁
-
为何须要意向锁呢?思考一个问题:若是咱们想对某个表加一个表锁,那么在加锁以前咱们须要去检查表中的每一行记录是否已经被单独加了行锁,这样的话岂不是意味着咱们须要去遍历表中全部的记录依次进行检查,遍历是不可能的,这辈子都不可能遍历的,基于效率的考虑,咱们能够在每次给行记录加锁时先给当前表加一个意向锁,若是咱们要对行加读锁(S)的话,那么就先给表加一个意向读锁(IS),若是要对行加写锁(X)的话,那么先给表加一个意向写锁(IX),这样当咱们须要给整个表加锁的时候就能够经过先判断表上是否已经存在了意向锁来决定是否能够上锁了,避免遍历,提升了效率。
-
意向锁跟普通的读锁写锁间的兼容性以下:
IS | IX | S | X | |
---|---|---|---|---|
IS | 兼容 | 兼容 | 兼容 | 不兼容 |
IX | 兼容 | 兼容 | 不兼容 | 不兼容 |
S | 兼容 | 不兼容 | 兼容 | 不兼容 |
X | 不兼容 | 不兼容 | 不兼容 | 不兼容 |
注:IS(意向读锁/意向共享锁), IX(意向写锁/意向排他锁), S(读锁/共享锁),X(写锁/排他锁)
从上图中能够看出,意向锁之间都是兼容的,这是由于意向锁的做用仅仅是来快速判断是否能够直接上表锁。
「接下来介绍的这几种锁都属于行锁」,为了更好的理解这几种锁,咱们先建立一个表
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(10) NOT NULL,
PRIMARY KEY (`id`),
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
其中id为主键,没有建其他的索引,插入以下数据
INSERT INTO `test`.`user`(`id`, `name`) VALUES (1, 'a张大胆');
INSERT INTO `test`.`user`(`id`, `name`) VALUES (3, 'b王翠花');
INSERT INTO `test`.`user`(`id`, `name`) VALUES (6, 'c范统');
INSERT INTO `test`.`user`(`id`, `name`) VALUES (8, 'd朱逸群');
INSERT INTO `test`.`user`(`id`, `name`) VALUES (15, 'e董格求');
Record Lock(记录锁)
-
锁定单条记录 -
也分为S锁跟X锁
若是咱们对id为3的记录添加一个行锁,对应以下(图中每一列表明数据库中的一行记录):

Gap Lock(间隙锁)
-
锁定一个范围,可是不包含记录自己 -
间隙锁的主要做用在于防止幻读的发生,虽然也有S锁跟X锁的区分,可是它们的做用都是相同的,并且若是你对一条记录加了 间隙锁
(不管是共享间隙锁
仍是独占间隙锁
),并不会限制其余事务对这条记录加记录锁
或者继续加间隙锁
,再强调一遍,间隙锁
的做用仅仅是为了防止幻读的发生。
假设咱们要对id为6的记录添加间隙锁,那么此时锁定的区域以下所示
其中虚线框表明的是要锁定的间隙,其实就是当前须要加间隙锁的记录跟上一条记录之间的范围,可是间隙锁不会锁定当前记录,如图所示,id=6的记录并无被加锁。(图中虚线框表锁间隙,没有插入真实的记录)

Next-Key Lock(Gap Lock+Record Lock)
假设咱们要对id为6的记录添加Next-Key Lock
,那么此时锁定的区域以下所示

跟间隙锁最大的区别在于,Next-Key Lock
除了锁定间隙以外还要锁定当前记录
❝经过锁实现了写、写操做之间的隔离性,实际上咱们也能够经过加锁来实现读、写之间的隔离性,可是这样带来一个问题,读、写须要串行执行这样会大大下降效率,因此MySQL中实现读写之间的隔离性是经过MVCC+锁来实现的,对于读采用快照都,对于写使用加锁!
❞
MVCC(多版本并发控制)
版本链
在介绍MVCC
以前咱们须要对MySQL中的行记录格式有必定了解,其实除了咱们在数据库中定义的列以外,每一行中还包含了几个隐藏列,分别是
-
row_id:行记录的惟一标志 -
transaction_id:事务ID -
roll_pointer:回滚指针
「row_id是行记录的惟一标志,这一列不是必须的。」
MySQL会优先使用用户自定义主键做为主键,若是用户没有定义主键,则选取一个Unique
键做为主键,若是表中连Unique
键都没有定义的话,则InnoDB
会为表默认添加一个名为row_id
的隐藏列做为主键。也就是说只有在表中既没有定义主键,也没有申明惟一索引的状况MySQL才会添加这个隐藏列。
「transaction_id表明的是事务的ID」。当一个事务对某个表执行了增、删、改操做,那么InnoDB
存储引擎就会给它分配一个独一无二的事务id
,分配方式以下:
-
对于只读事务来讲,只有在它第一次对某个用户建立的「临时表执行增、删、改操做」时才会为这个事务分配一个
事务id
,不然的话是不分配事务id
的。 -
对于读写事务来讲,只有在它「第一次对某个表(包括用户建立的临时表)执行增、删、改操做」时才会为这个事务分配一个
事务id
,不然的话也是不分配事务id
的。有的时候虽然咱们开启了一个读写事务,可是在这个事务中全是查询语句,并无执行增、删、改的语句,那也就意味着这个事务并不会被分配一个
事务id
。
「roll_pointer表示回滚指针,指向该记录对应的undo log」。前文已经提到过了,undo log记录了对应记录在修改前的状态,经过roll_pointer
咱们就能够找到对应的undo log,而后根据undo log进行回滚。
❝在以前介绍undo log的时候咱们只介绍了insert undo log的数据格式,实际上除了insert undo log还有update undo log,而
❞update undo log
中也包含roll_pointer
跟transaction_id
。update undo log
中的roll_pointer
指针其实就是保存的被更新的记录中的roll_pointer
指针
「除了这些隐藏列之外,实际上每条记录的记录头信息中还会存储一个标志位,标志该记录是否删除。」
咱们以实际的例子来讲明上面三个隐藏列的做用,仍是以以前的表为例,如今对其执行以下SQL:
# 开启事务
START TRANSACTION;
# 插入一条数据
INSERT INTO `test`.`user`(`id`, `name`) VALUES (16, 'e杜子騰');
# 更新插入的数据
UPDATE `test`.`user` SET name = "史珍香" WHERE id = 16;
# 删除数据
DELETE from `test`.`user` WHERE id = 16;
咱们经过画图来看看上面这段SQL在执行的过程当中都作了什么

从上图中咱们能够看到,每对记录进行一次增、删、改时,都会生成一条对应的undo log,而且被修改后的记录中的roll pointer
指针指向了这条undo log,同时若是不是新增操做,那么生成的undo log中也会保存一个roll pointer
,其值是从被修改的数据中复制过来了,在咱们上边的例子中update undo log的roll pointer
就复制了insert进去的数据中的roll pointer
指针的值。
另外咱们会发现,根据当前记录中的roll pointer
指针,咱们能够找到一个有undo log组成的链表,这个undo log链表其实就是这条记录的版本链
。
ReadView(快照)
对于使用READ UNCOMMITTED
隔离级别的事务来讲,因为能够读到未提交事务修改过的记录,因此直接读取记录的最新版本就行了;
对于使用SERIALIZABLE
隔离级别的事务来讲,MySQL规定使用加锁的方式来访问记录;
对于使用READ COMMITTED
和REPEATABLE READ
隔离级别的事务来讲,都必须保证读到已经提交了的事务修改过的记录,也就是说假如另外一个事务已经修改了记录可是还没有提交,是不能直接读取最新版本的记录的,核心问题就是:「须要判断一下版本链中的哪一个版本是当前事务可见的」。
为了解决这个问题,MySQL提出了一个ReadView
(快照)的概念,「在Select操做前会为当前事务生成一个快照,而后根据快照中记录的信息来判断当前记录是否对事务是可见的,若是不可见那么沿着版本链继续往上找,直至找到一个可见的记录。」
「ReadView」(快照)中包含了下面几个关键属性:
-
m_ids
:表示在生成ReadView
时当前系统中活跃的读写事务的事务id
列表。 -
min_trx_id
:表示在生成ReadView
时当前系统中活跃的读写事务中最小的事务id
,也就是m_ids
中的最小值。 -
max_trx_id
:表示生成ReadView
时系统中应该分配给下一个事务的id
值。❝
小贴士:注意max_trx_id并非m_ids中的最大值,事务id是递增分配的。比方说如今有id为1,2,3这三个事务,以后id为3的事务提交了。那么一个新的读事务在生成ReadView时,m_ids就包括1和2,min_trx_id的值就是1,max_trx_id的值就是4。
❞ -
creator_trx_id
:表示生成该ReadView
的事务的事务id
。❝
小贴士:咱们前边说过,只有在对表中的记录作改动时(执行INSERT、DELETE、UPDATE这些语句时)才会为事务分配事务id,不然在一个只读事务中的事务id值都默认为0。
❞
当生成快照后,会经过下面这个流程来判断该记录对当前事务是否可见

-
从上图中咱们能够看到,在根据当前数据库中运行中的读写事务id,会去生成一个ReadView。 -
而后根据要读取的数据记录中的事务id(方便区别,记为 r_trx_id
)跟ReadView中保存的几个属性作以下判断
-
若是被访问版本的 r_trx_id
属性值与ReadView
中的creator_trx_id
值相同,意味着当前事务在访问它本身修改过的记录,因此该版本能够被当前事务访问。 -
若是被访问版本的 r_trx_id
属性值小于ReadView
中的min_trx_id
值,代表生成该版本的事务在当前事务生成ReadView
前已经提交,因此该版本能够被当前事务访问。 -
若是被访问版本的 r_trx_id
属性值大于或等于ReadView
中的max_trx_id
值,代表生成该版本的事务在当前事务生成ReadView
后才开启,因此该版本不能够被当前事务访问。 -
若是被访问版本的 r_trx_id
属性值在ReadView
的min_trx_id
和max_trx_id
之间,那就须要判断一下r_trx_id
属性值是否是在m_ids
列表中,若是在,说明建立ReadView
时生成该版本的事务仍是活跃的,该版本不能够被访问;若是不在,说明建立ReadView
时生成该版本的事务已经被提交,该版本能够被访问。 -
若是某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本。若是最后一个版本也不可见的话,那么就意味着该条记录对该事务彻底不可见,查询结果就不包含该记录。
实际上,提交读跟可重复读在实现上最大的差别就在于
-
提交读每次select都会生成一个快照 -
可重复读只有在第一次会生成一个快照
总结
本文主要介绍了事务的基本概念跟MySQL中事务的实现原理。下篇文章开始咱们就要真正的进入Spring的事务学习啦!铺垫了这么久,终于开始主菜了......
在前面的大纲里也能看到,会分为上下两篇,第一篇讲应用以及在使用过程当中会碰到的问题,第二篇咱们就深刻源码分析Spring中的事务机制的实现原理!
「参考」:
书籍:掘金小册《MySQL 是怎样运行的:从根儿上理解 MySQL》:https://juejin.im/book/6844733769996304392
书籍:《MySQL技术内幕:InnoDB存储引擎》:关注公众号,程序员DMZ
,后台回复InnoDB
便可领取
书籍:《高性能MySQL》:关注公众号,程序员DMZ
,后台回复MySQL
便可领取
文章:《深刻学习MySQL事务:ACID特性的实现原理》:https://www.cnblogs.com/kismetv/p/10331633.html
文章:《详细分析MySQL事务日志(redo log和undo log)》:https://www.cnblogs.com/f-ck-need-u/p/9010872.html
文章:《Mysql事务实现原理》:https://www.lagou.com/lgeduarticle/82740.html
文章:《面试官:你说熟悉MySQL事务,那来谈谈事务的实现原理吧!》:https://mp.weixin.qq.com/s/jrfZr3YzE_E0l3KjWAz1aQ
文章:《InnoDB 事务分析-Undo Log》:http://leviathan.vip/2019/02/14/InnoDB%E7%9A%84%E4%BA%8B%E5%8A%A1%E5%88%86%E6%9E%90-Undo-Log/
文章:《InnoDB 的 Redo Log 分析》:http://leviathan.vip/2018/12/15/InnoDB%E7%9A%84Redo-Log%E5%88%86%E6%9E%90/
文章:《MySQL redo & undo log-这一篇就够了》:https://www.jianshu.com/p/336e4995b9b8
本文就到这里啦,若是本文对你有帮助的话,记得帮忙三连哈,感谢~!
我叫DMZ,一个在学习路上匍匐前行的小菜鸟!
往期精选
本文分享自微信公众号 - 程序员DMZ(programerDmz)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。