文章目录
事务
事务四大性质(ACID)
原子性(Atomicity)
原子性是指一个事务的执行过程是完整的,即要么从第一条sql语句开始执行到最后一条sql语句执行完成并提交数据库,或者进行数据回滚,即全部不执行,恢复到事务开始前的执行状态
一致性(Consistency)
一致性是指符合现实世界的约束,例如银行进行转账时账户余额必须大于0,否则不能进行转账
隔离性(Isolation)
- 隔离性是指在多个事务对同一条记录同时进行访问修改时遵守并发原则,如果学过多线程的话,其实这里本质上可以理解为多个线程对共享数据进行访问修改造成的线程不安全问题;
- 例如多个事务同时修改同一条记录,如事务A将其中的data从1修改为2,在事务A未提交数据库的情况下,事务A再次访问数据b的数据肯定还是为2,不能在未提交情况下让其他事务将该记录进行修改,画个图再加深一下印象~
持久性(Durabillity)
- 持久性是指事务在提交数据库后就永久保存在了磁盘中,在不被修改的情况下数据的结果永久被保留
- 一个小tip:可以使用英文单词(acid)来记住事务的4大特性ACID
事务并发出现的问题
脏写
- 脏写是指当事务并发对一条记录中的同一个字段进行修改时,如果事务A修改数据后未提交数据库(此时事务A的数据可以进行回滚),此时事务B将事务A未提交数据库的数据进行了修改,此时则说明发生了脏写
- 我们举个例子理解一下:
- 如图所示,如果事务A对数据修改后未进行提交的情况下,事务B进行对数据的修改并提交,如果此时事务A对其进行回滚,那么事务B提交的数据则会无效!,这种情况是无法接受的,因为这违背了事务的持久性原则
- 再考虑另外一种情况,如果事务A回滚失败不就不会违背事务的持久性了吗?是的,但是这样违背了事务的原子性,事务A的执行结果只能是未执行和执行完成的状态,不可能处于中间状态,所以脏写是不可接受的
脏读
脏读是指一个事务读到了另一个未提交事务修改过的数据,此时可能出现脏读(其实脏读和脏写本质是一样的,如果学过多线程的话,其本质就是线程之间并发执行时,如果不加锁的话,并发访问相同资源会造成带来的线程不安全),我们再画个图理解一下~(注:原数据库在开启事务A、B前,该记录的name=mysql)
因为事务A未提交数据库,可以进行数据回滚,那么就会出现事务B在同个事务中的不同时间读取相同记录但是数据不相同的情况
不可重复读
不可重复读是指如果一个事务只能读到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交后,该事务都能查询得到最新值,其实这种情况我们会认为理所应当,但是它确实也是读取数据时会产生的一种情况
幻读
- 幻读是指:A事务读取了一个范围的数据,之后B事务向该范围中插入了新数据,当A事务再次对该范围的数据进行检索时发现了新插入的新纪录,此时将插入的记录称为幻影记录,但是在MySQL中,通过锁机制或者MVCC可以解决大多数情况下的幻读问题
- 幻读和不可重复读的区别在于不可重复读更多强调的是:对于相同查询语句执行后出现的原数据的变化(如name从mysql变成oracle);而不可重复读更多强调的是:对于相同查询语句执行后会出现不同数量的记录(如表中的数据从5条记录增加为10条记录)
- 作者看过一些博文,在开启可重复读隔离级别后,把在无论何时查询结果相同的现象叫做幻读,作者认为这是不准确的,这是MySQL根据幻读现象,提出了可重复读隔离级别机制,并且解决了大多数情况下的幻读现象(因为有一种情况幻读不会被解决,这个坑我们在MVCC再填上~)
MySQL隔离机制解决事务并发
因为脏写问题太严重,会导致数据库数据出现错误,因此mysql的4种隔离机制都解决了脏写问题,这一点请谨记,而对于脏写出现的问题,MySQL是通过锁机制限制在修改记录时不允许并发修改,这个类似于java中的Synchronized关键字限制线程的并发访问
读未提交(Read Uncommit)
读未提交是指事务A可以读取到事务B未提交数据库的修改记录,即可能出现脏读,而且可能出现不可重复读、幻读的情况
读已提交(Read Commit)
读已提交是指在读取同一条记录时,事务A可以读取到事务B已提交数据库的修改记录,但是还有可能出现不可重复读、幻读的现象
可重复读(Repeatble Read)
可重复读是指在读取同一条记录时,事务A开启后,在第一次读取到记录后,接下来无论何时对记录进行读取,读取到的结果都是相同的,因此我们认为MySQL在很大程度上解决了幻读问题
串行化
串行化是指任何事务对同一条记录的访问都是需要按顺序执行的,无法并发进行访问,因此串行化的安全性是最高的,但是其并发性低导致执行效率特别低
隔离级别的sql语句
查看当前隔离级别
默认事务隔离级别为RR(Repeatable Read:可重复读),可通过sql语句select @@transaction_isolation
进行查看,如图:
设置隔离级别
set [session/global] transaction isolation level isolation_level
- session是开启当前会话的隔离级别,global是开启当前全局的会话隔离级别
- session会话设置隔离级别后只会对后续开启的事务有效
- global会话设置隔离级别只会对后续开启的会话有效
总结
如图,总结了各个隔离级别下仍然可能出现的问题
MVCC机制
MVCC机制解决了什么问题
相关概念
我们先引进3个概念:
- 事务并发读-读操作:指多个事务对同一条记录进行同时的读取操作,此时不涉及记录的修改,无需加锁,可并发访问
- 事务并发读-写操作:指对于同一条记录来说,有一个事务对其进行写操作时,另外的其他多个事务对其进行读取操作,此时就会出现脏读、不可重复读、幻读的现象
- 事务并发写-写操作:指多个事务对同一条记录进行写操作时,必须进行互斥修改,即每次修改记录都确保只有一个事务对其进行修改(依赖于MySQL中的锁机制),否则可能会出现脏写的情况
- 综上所述,读-读和写-写操作都不会产生并发问题,只有读-写会产生一系列问题,针对读-写问题,由上文可知,实际上MySQL提出了读已提交隔离机制解决了脏读问题,在这基础上又提出了可重复读解决了读已提交隔离机制下产生的不可重复读问题
- 对于读未提交的隔离机制而言,我们只需每次读取其最新记录即可,而对于读已提交和可重复读这2种隔离机制来讲,MySQL的设计者提出了2种解决方案对这2种隔离机制进行了实现,一个是通过不加锁的MVCC,另外一个是通过锁机制,二者的区别在于读-写场景下是否允许读取到记录的旧版本(我们后面再进行解释),显而易见的是读-写场景下不加锁的MVCC机制运行效率肯定高于锁机制(作者一再强调的是MVCC机制是在不加锁的情况实现了读已提交和可重复读隔离机制,但是MVCC只能在读-写场景下使用)
总结
我们画个图总结一下上面所述内容
MVCC实现原理
相关概念
在介绍MVCC之前我们先介绍几个相关概念:
-
MVCC:多版本并发控制(Multi-Version Concurrency Control)
-
对于每张表来说,mysql都会自动添加2个隐藏列,一个是trx_id,一个是roll_point
-
trx_id:当一个事务对某条记录进行改动时,都会把该事务的id赋值给该trx_id隐藏列
-
roll_point:每次对某条记录进行改动时,就会把旧版本写入到undo日志中,而B+树索引页面记录的是最新的修改记录(表中的记录都是通过B+树这种数据结果进行组织起来的,B+树的叶子节点存放了表中的记录),旧记录会通过roll_point指针链接到指向存放在undo日志中的旧记录
-
我们画个图理解一下:
-
我们把使用roll_point将不同版本的同一条记录串起来的的链表称为版本链,其实也可以猜个八九不离十了,我们最终会通过某种规则去匹配版本链中的某一条记录使得读取时满足读已提交或者可重复读隔离机制,如果读取到的是undo Log中的记录,我们此时称访问到的记录是旧记录,也就是将所有未提交数据库的记录都进行了屏蔽
-
ReadView(一致性视图):
- m_ids:当前活跃的事务列表(也就是尚未提交数据库的事务)
- min_trx_id:当前活跃的事务最小id值
- max_trx_id:在生成ReadView时,系统应该分配给下一个事务的事务id值,递增分配,不出意外的话是当前活跃最大id值+1
- creator_id:只有在对表中记录进行更改时才会为事务分配id值,否则id值为0,creator_id代表生成该ReadView的事务的事务id
MVCC执行规则
我们先上图~,根据下图来解释读已提交和可重复读隔离机制的实现
- 当trx_id小于min_trx_id时记录可被访问
- 当trx_id大于等于min_trx_id且小于max_trx_id时,要查看trx_id是否在m_ids列表中,如果在则不可被访问,如果不在则可访问
- 当trx_id大于等于max_trx_id时记录不可被访问
- 其实是否可以被访问的本质是看生成ReadView时刻后,当我们查看记录时,修改某记录的事务是否已经提交数据库,若提交trx_id不在m_ids列表,而min_trx_id是为了快速帮助判断是否可以访问记录,因为大部分记录实际上都不会在同一个时间点进行修改
读已提交下的MVCC
根据上面的执行规则,我们通过一个图来理解MVCC如何实现读已提交,我们先约定将要进行写操作(也就是进行记录修改)的记录称为R记录:
假设在D开启事务之前,A、B、C事务都对R记录进行过写操作,分配的trx_id分别为100,101,102,我们聚焦于D事务:
t5时刻
- 首先看t5时刻:在t5时刻D事务开启后假设对R执行了select语句,那么会生成一个ReadView,我们可以看到此时C事务处于活跃状态,而A、B事务在D事务之前已经执行结束并且提交了数据库,那么此时m_ids就是102,min_trx_id为102,max_trx_id为103(前文讲过,如果事务不对记录进行修改,不会为该事务的trx_id赋值,因此事务D此时不会被赋值),我们画张图再理一理R记录当前的版本链情况:
2. 我们再根据前面MVCC执行规则图来看一下:首先D事务执行Select语句后会定位到B+树中的记录,发现trx_id为102,而min_trx_id为102,即min_trx_id刚好等于trx_id,根据执行图规则,不能被访问,因此顺着roll_point查看undo Log中的记录,发现第一条记录trx_id为101,小于min_trx_id,所以可以访问该条记录,访问结束
3. 可能有读者会问能不能在顺着roll_point往下访问,实际上在读已提交的隔离级别下,我们读取到的肯定是默认在对同一条数据修改的情况下,数据库已经提交修改后的最新记录,因此肯定是读取到符合条件的第一条记录即可~
t6时刻
-
再看t6时刻,当我们再次执行select语句对上文的R记录进行访问时,会重新生成一个
ReadView,为了区分我们称第二次生成的ReadView为ReadView2,对于读已提交来说,当每次进行记录读取时都是会生成一个ReadView的**,这一点非常重要,这与可重复读隔离机制的区别就在这了,具体区别下面再把坑填上~
-
再进行ReadView2分析,此时m_ids为空,因为D事务只读取记录不进行修改,那么则不会分配trx_id,m_ids列表中自然也没有活跃的事务id了,因而min_ids也为空, max_trx_id为102,此时事务A、B、C都已经提交数据库,那么此时可直接读取B+树中的记录即可(因为没有活跃的事务id,说明B+树中该记录的trx_id肯定不在m_ids中,那么根据规则直接读取即可在),m_ids为空本质上说明生成该ReadView之前所有与该记录有关的事务都已经提交数据库了
可重复读下的MVCC
-
还记得上文说对于读已提交隔离机制来说,当每次对R记录读取时都会生成一个ReadView,而可重复读隔离机制则是在第一次执行select语句时会生成ReadView,以后每次执行相同的查询语句,都会沿用旧的ReadView对版本链进行记录的匹配,也正是通过这个机制所以在MySQL中基本解决了幻读现象
-
根据上面的执行规则,我们通过一个图来理解MVCC如何实现可重复读:
-
注意,这里的流程图已经是可重复读隔离级别下的流程图了
-
假设事务A、B、C在开启事务D前都对相同的R记录进行了修改,分配的trx_id分别为100,101,102
- 对于t5时刻来说:当事务D进行对R记录进行查询,生成的ReadView内容为m_ids等于102,min_trx_id等于102,max_trx_id为103,根据下面版本链图,我们可以很清楚知道D事务会读取到trx_id等于101的记录
- 对于t6时刻来说:在执行与t5时刻相同的select语句后,不会再生成ReadView,而是沿用t5时刻生成的ReadView,那么根据t5时刻的ReadView及匹配规则,即便此时事务C已经提交,但是事务D仍然会读取到trx_id为101的记录,而不是trx_id为102的记录,因为此时ReadView中的内容依旧是m_ids等于102,min_trx_id等于102,max_trx_id为103,读者可根据规则再次进行匹配验证,这里不再赘述
- 对于t8时刻来说,此时在t8时刻之前的t7时刻已经开启了一个新事务E,而且也对记录进行了修改,因此分配了事务trx_id为103,但是ReadView保持不变,此时m_ids依然为102,min_trx_id为102,max_trx_id为103,此时我们再画个图看看此时的版本链情况:
4.此时在t8时刻事务C读取R该记录,同样的根据匹配原则,因为max_trx_id等于trx_id,因此读取不到该记录,往下遍历到记录为trx_id为102,刚好等于min_trx_id,同样不符合,再往下遍历到trx_id为101,小于min_trx_id,符合读取规则,进行读取,读取语句执行完成
- 对于t5时刻来说:当事务D进行对R记录进行查询,生成的ReadView内容为m_ids等于102,min_trx_id等于102,max_trx_id为103,根据下面版本链图,我们可以很清楚知道D事务会读取到trx_id等于101的记录
可重复读隔离级别下的幻读
- 通常情况下如果我们开启事务A后只会根据该事务A读取到的记录了对该表中的记录进行操作,但是!,如果此时事务B进行插入操作,如果在事务A中,我们对表中未显示但是表中已经存在的记录进行写操作则会又出现幻读,因为此时B+树的最新记录的trx_id为A事务的trx_id,因此A事务可以进行读取,那么则会发生幻读现象
- 我们做个实验验证一下
第一次事务A查询结果如图:
第二次事务A修改了表中未出现的记录,查询结果如图:
参考文献
该博客是在读了《MySQL是怎么运行的》这本书后写的,大量参考借鉴了书中内容,在此对本书作者表示感谢!