1、初探undo log

相信之前讲解了redo log日志之后,大家对这块都理解的更加深刻了,那么今天我们就带着大家来探索 另外一种日志,就是undo log日志,也就是回滚日志,这种日志要应对的场景,就是事务回滚的场景!

那么首先大家先思考一个问题,假设现在我们一个事务里要执行一些增删改的操作,那么必然是先把对 应的数据页从磁盘加载出来放buffer pool的缓存页里,然后在缓存页里执行一通增删改,同时记录 redo log日志,对吧

但是现在问题来了,万一要是一个事务里的一通增删改操作执行到了一半,结果就回滚事务了呢? 比如一个事务里有4个增删改操作,结果目前为止已经执行了2个增删改SQL了,已经更新了一些buffer pool里的数据了,但是还有2个增删改SQL的逻辑还没执行,此时事务要回滚了怎么办?

这个时候就很尴尬了,如果你要回滚事务的话,那么必须要把已经在buffer pool的缓存页里执行的增删 改操作给回滚了 但是怎么回滚呢?毕竟无论是插入,还是更新,还是删除,该做的都已经做了啊! 所以在执行事务的时候,才必须引入另外一种日志,就是undo log回滚日志 这个回滚日志,他记录的东西其实非常简单,比如你要是在缓存页里执行了一个insert语句,那么此时 你在undo log日志里,对这个操作记录的回滚日志就必须是有一个主键和一个对应的delete操作,要能 让你把这次insert操作给回退了。

那么比如说你要是执行的是delete语句,那么起码你要把你删除的那条数据记录下来,如果要回滚,就 应该执行一个insert操作把那条数据插入回去。 如果你要是执行的是update语句,那么起码你要把你更新之前的那个值记录下来,回滚的时候重新 update一下,把你之前更新前的旧值给他更新回去。 如果你要是执行的是select语句呢?不好意思,select语句压根儿没有在buffer pool里执行任何修改, 所以根本不需要undo log!其实你在执行事务期间,之前我们最开始的几篇文章就讲过,你除了写redo log日志还必须要写undo log日志,这个undo log日志是至关重要的,没有他,你根本都没办法回滚事 务!

2、 看看INSRET语句的undo log回滚日志长什么样?

比如你执行了INSERT语句,那么你的undo log必须告诉你插入数据的主键ID,让你在回滚的时候可以 从缓存页里把这条数据给删除了; 如果你执行了DELETE语句,那么你的undo log必须记录下来被删除的数据,回滚的时候就得重新插入 一条数据; 如果你执行了UPDATE语句,那么你必须记录下来修改之前的数据,回滚的时候就得把数据给更新回 去

那么今天我们就一起来看看这个INSERT语句的undo log日志到底长什么样子呢? INSERT语句的undo log的类型是TRX_UNDO_INSERT_REC,这个undo log里包含了以下一些东西:

  • 这条日志的开始位置
  • 主键的各列长度和值
  • 表id
  • undo log日志编号
  • undo log日志类型
  • 这条日志的结束位置

    接下来我们来给大家解释一下,首先,一条日志必须得有自己的一个开始位置,这个没什么好说的是 吧? 那么主键的各列长度和值是什么意思?大家都知道,你插入一条数据,必然会有一个主键! 如果你自己指定了一个主键,那么可能这个主键就是一个列,比如id之类的,也可能是多个列组成的一 个主键,比如“id+name+type”三个字段组成的一个联合主键,也是有可能的。 所以这个主键的各列长度和值,意思就是你插入的这条数据的主键的每个列,他的长度是多少,具体的 值是多少。即使你没有设置主键,MySQL自己也会给你弄一个row_id作为隐藏字段,做你的主键。 接着是表id,这个就不用多说了,你插入一条数据必然是往一个表里插入数据的,那当然得有一个表 id,记录下来是在哪个表里插入的数据了。 undo log日志编号,这个意思就是,每个undo log日志都是有自己的编号的。 而在一个事务里会有多个SQL语句,就会有多个undo log日志,在每个事务里的undo log日志的编号都 是从0开始的,然后依次递增。 至于undo log日志类型,就是TRX_UNDO_INSERT_REC,insert语句的undo log日志类型就是这个东 西。 最后一个undo log日志的结束位置,这个自然也不用多说了,他就是告诉你undo log日志结束的位置是 什么。

    大家可以想象一下,有了这条日志之后,剩下的事儿就好办了 万一要是你现在在buffer pool的一个缓存页里插入了一条数据了,执行了insert语句,然后你写了一条 上面的那种undo log,现在事务要是回滚了,你直接就把这条insert语句的undo log拿出来。 然后在undo log里就知道在哪个表里插入的数据,主键是什么,直接定位到那个表和主键对应的缓存 页,从里面删除掉之前insert语句插入进去的数据就可以了,这样就可以实现事务回滚的效果了!

3、MySQL运行时多个事务同时执行是什么场景?

所谓的事务呢,其实或多或少每个人都是知道一点的,我们今天只不过是站在MySQL内核原理的角度, 拔高到事务的层面给大家回顾一下。 其实大家可以思考,平时我们是不是一般都是写一个业务系统,然后业务系统会去对数据库执行增删改 查? 然后我们应该都知道一件事情,通常而言,我们都是在业务系统里会开启事务来执行增删改操作的

所以一般来说,业务系统是执行一个一个的事务,每个事务里可能是一个或者多个增删改查的SQL语句 这个事务的概念想必不用我多说了,其实就是一个事务里的SQL要不然一起成功就提交了,要不然有一 个SQL失败,那么事务就回滚了,所有SQL做的修改都撤销了!

接着问题来了,这个业务系统他可不是一个单线程系统啊!他是有很多线程的! 因为他面向的终端用户是有很多人的,可能会同时发起请求,所以他需要多个线程并发来处理多个请求 的 于是,这个业务系统很可能是基于多线程并发的对MySQL数据库去执行多个事务的!

那么每个事务里面的多个SQL语句都是如何执行的呢? 其实就是我们之前给大家讲过的那一套原理了,包括从磁盘加载数据页到buffer pool的缓存页里去,然 后更新buffer pool里的缓存页,同时记录redo log和undo log

每个事务如果提交了,那么就皆大欢喜,这个提交的过程我之前最早就讲过了,他有一些步骤,包括在 redo log里记录事务提交标识之类的。 如果事务提交之后,redo log刷入磁盘,结果MySQL宕机了,是可以根据redo log恢复事务修改过的缓 存数据的。 如果要回滚事务,那么就基于undo log来回滚就可以了,把之前对缓存页做的修改都给回滚了就可以 了。 这就是在MySQL内核层面,把多个事务和我们之前讲解的buffer pool、redo log、undo log几个机制 都结合在一起的一个场景讲解。想必大家都是可以理解的。 但是这里就有很多问题了: 多个事务并发执行的时候,可能会同时对缓存页里的一行数据进行更新,这个冲突怎么处理?是否 要加锁? 可能有的事务在对一行数据做更新,有的事务在查询这行数据,这里的冲突怎么处理?

4、多个事务并发更新以及查询数据,会出现的四种问题

每个事务都会执行各种增删改查的语句,把磁盘上的数据页加载到buffer pool的缓存页里来,然后更新 缓存页,记录redo log和undo log,最终提交事务或者是回滚事务,多个事务会并发干上述一系列事 情。 所以今天我们就要来看看,如果多个事务要是对缓存页里的同一条数据同时进行更新或者查询,此时会 产生哪些问题呢? 这里实际上会涉及到脏写、脏读、不可重复读、幻读,四种问题。

1、先看第一种问题,脏写

这个脏写的话,他的意思就是说有两个事务,事务A和事务B同时在更新一条数据,事务A先把他更新为 A值,事务B紧接着就把他更新为B值 , 大家可以看到,此时事务B是后更新那行数据的值,所以此时那行数据的值是不是B值? 没错的。而且此时事务A更新之后会记录一条undo log日志,大家应该还记得吧。事务A是先更新的, 他在更新之前,这行数据的值为NULL,对吧? 所以此时事务A的undo log日志大概就是:更新之前这行数据的值为NULL,主键为XX 好,那么此时事务B更新完了数据的值为B,结果此时事务A突然回滚了,那么就会用他的undo log日志 去回滚。 此时事务A一回滚,直接就会把那行数据的值更新回之前的NULL值!所以此时事务A回滚了,可能看起 来这行数据的值就是NULL了

然后就尴尬了,事务B一看,我的妈呀,为什么我更新的B值没了?就因为你事务A反悔了就把数据值回 滚成NULL了,搞的我更新的B值也没了,这也太坑爹了吧! 所以对于事务B看到的场景,就是自己明明更新了,结果值却没了,这就是脏写!

所谓脏写,就是我刚才明明写了一个数据值,结果过了一会儿却没了!真是莫名其妙。 而他的本质就是事务B去修改了事务A修改过的值,但是此时事务A还没提交,所以事务A随时会回滚, 导致事务B修改的值也没了,这就是脏写的定义。

2、第二种问题,脏读

假设事务A更新了一行数据的值为A值,此时事务B去查询了一下这行数据的值,看到的值是不是A值? 没错, 好,现在事务B可能还挺high的,拿着刚才查询到的A值做各种业务处理。大家知道,每个事务都是业务 系统发出的,所以业务系统里的事务B此时肯定会拿到刚查出来的A值在做一些业务处理。 但是接着坑爹的事情发生了,事务A突然回滚了事务,导致他刚才更新的A值没了,此时那行数据的值回 滚为NULL值! 然后事务B紧接着此时再次查询那行数据的值,看到的居然此时是NULL值?事务B此时简直欲哭无泪, 所以这就是坑爹的脏读,他的本质其实就是事务B去查询了事务A修改过的数据,但是此时事务A还没提 交,所以事务A随时会回滚导致事务B再次查询就读不到刚才事务A修改的数据了!这就是脏读。

其实一句话总结,无论是脏写还是脏读,都是因为一个事务去更新或者查询了另外一个还没提交的事务 更新过的数据。 因为另外一个事务还没提交,所以他随时可能会反悔会回滚,那么必然导致你更新的数据就没了,或者 你之前查询到的数据就没了,这就是脏写和脏读两种坑爹场景。

3、第三个问题, 不可重复读

这个问题是这样的:假设我们有一个事务A开启了,在这个事务A里会 多次对一条数据进行查询 然后呢,另外有两个事务,一个是事务B,一个是事务C,他们俩都是对一条数据进行更新的。 然后我们假设一个前提,就是比如说事务B更新数据之后,如果还没提交,那么事务A是读不到的,必须 要事务B提交之后,他修改的值才能被事务A给读取到,其实这种情况下,就是我们首先避免了脏读的发 生。 因为脏读的意思就是事务A可以读到事务B修改过还没提交的数据,此时事务B一旦回滚,事务A再次读 就读不到了,那么此时就会发生脏读问题。 我们现在假设的前提是事务A只能在事务B提交之后读取到他修改的数据,所以此时必然是不会发生脏读 的 好了,但是你以为没有脏读就万事大吉了吗?绝对不是,此时会有另外一个问题,叫做不可重复读 假设缓存页里一条数据原来的值是A值,此时事务A开启之后,第一次查询这条数据,读取到的就是A 值 , 接着事务B更新了那行数据的值为B值,同时事务B立马提交了,然后事务A此时可是还没提交!

大家注意,此时事务A是没提交的,他在事务执行期间第二次查询数据,此时查到的是事务B修改过的 值,B值,因为事务B已经提交了,所以事务A可以读到的了 , 紧接着事务C再次更新数据为C值,并且提交事务了,此时事务A在没提交的情况下,第三次查询数据, 查到的值为C值

好,那么上面的场景有什么问题呢? 其实要说没问题也可以是没问题,毕竟事务B和事务C都提交之后,事务A多次查询查到他们修改的值, 是ok的。 但是你要说有问题,也可以是有问题的,就是事务A可能第一次查询到的是A值,那么他可能希望的是在 事务执行期间,如果多次查询数据,都是同样的一个A值,他希望这个A值是他重复读取的时候一直可以 读到的!他希望这行数据的值是可重复读的! 但是此时,明显A值不是可重复读的,因为事务B和事务C一旦更新了值并且提交了,事务A会读到别的 值,所以此时这行数据的值是不可重复读的!此时对于你来说,这个不可重复读的场景,就是一种问题 了! 不知道大家看到这里理解了没?如果没理解,反复把这个例子看几遍,理解一下! 上面描述的,其实就是不可重复读的问题,其实这个问题你说是问题也不一定就是什么大问题,但是说 他有问题,确实是有问题的。 因为这取决于你自己想要数据库是什么样子的,如果你希望看到的场景就是不可重复读,也就是事务A 在执行期间多次查询一条数据,每次都可以查到其他已经提交的事务修改过的值,那么就是不可重复读 的,如果你希望这样子,那也没问题。 但是如果你希望的是,假设你事务A刚开始执行,第一次查询读到的是值A,然后后续你希望事务执行期 间,读到的一直都是这个值A,不管其他事务如何更新这个值,哪怕他们都提交了,你就希望你读到的 一直是第一次查询到的值A,那么你就是希望可重复读的。 如果你期望的是可重复读,但是数据库表现的是不可重复读,让你事务A执行期间多次查到的值都不一 样,都是别的提交过的事务修改过的值,那么此时你就可以认为,数据库有问题,这个问题就是“不可 重复读”的问题!

4、第四个问题,幻读

简单来说,你一个事务A,先发送一条SQL语句,里面有一个条件,要查询一批数据出来,比如“select from table where id>10”,类似这种SQL 然后呢,他一开始查询出来了10条数据, 接着这个时候,别的事务B往表里插入了几条数据,而且事务B还提交了 此时多了几行数 据出来。 接着事务A此时第三次查询,再次按照之前的一模一样的条件执行“select from table where id>10”这 条SQL语句,由于其他事务插入了几条数据,导致这次他查询出来了12条数据 于是此时事务A开始怀疑自己的双眼了,为什么一模一样的SQL语句,第一次查询是10条数据,第二次 查询是12条数据?难道刚才出现了幻觉?导致我刚才幻读了?这就是幻读这个名词的由来。

幻读指的就是你一个事务用一样的SQL多次查询,结果每次查询都会发现查到了一些之前没看到过的数 据 注意,幻读特指的是你查询到了之前查询没看到过的数据!此时就说你是幻读了。 说实在的,大家看完最近几篇文章,应该都有一个感觉,就是脏写、脏读、不可重复读、幻读,都是因 为业务系统会多线程并发执行,每个线程可能都会开启一个事务,每个事务都会执行增删改查操作。 然后数据库会并发执行多个事务,多个事务可能会并发的对缓存页里的同一批数据进行增删改查操作, 于是这个并发增删改查同一批数据的问题,可能就会导致我们说的脏写、脏读、不可重复读、幻读,这 些问题。 所以这些问题的本质,都是数据库的多事务并发问题,那么为了解决多事务并发问题,数据库才设计了 事务隔离机制、MVCC多版本隔离机制、锁机制,用一整套机制来解决多事务并发问题

5、 SQL标准中对事务的4个隔离级别

在SQL标准中规定了4种事务隔离级别,就是说多个事务并发运行的时候,互相是如何隔离的,从而避 免一些事务并发问题

这4种级别包括了:read uncommitted(读未提交),read committed(读已提交), repeatable read(可重复读),serializable(串行化)

不同的隔离级别是可以避免不同的事务并发问题的,所以大家一定要对这个事务隔离级别有一个深刻的 理解。

第一个read uncommitted隔离级别,是不允许发生脏写的

也就是说,不可能两个事务在没提交的情况下去更新同一行数据的值,但是在这种隔离级别下,可能发 生脏读,不可重复读,幻读。
感觉如何?是不是感觉这种隔离级别让你整个人都感觉不好了!因为脏读的话,就是人家事务在没提交 情况下修改的值,居然被你读到了,人家随时可能会回滚的!而且你执行期间多次查询一行数据,可能 读到的值都不同,因为别的事务随时会修改值再提交,这个值是不可重复读的!幻读更不用说了,肯定 会发生。

第二个是read committed隔离级别,这个级别下,不会发生脏写和脏读

也就是说,人家事务没提交的情况下修改的值,你是绝对读不到的!但是呢,可能会发生不可重复读和 幻读问题,因为一旦人家事务修改了值然后提交了,你事务是会读到的,所以可能你多次读到的值是不 同的! 这里教给大家一个稍微有点骚气的简写名词,就是RC,一般如果你在公司里做开发,有个其他团队的兄 弟讨论技术方案的时候,跟你来了句,把事务隔离级别设置成RC!这个时候你不要目瞪口呆,知道是读 已提交级别就行了。 你只要记住,这个级别在别的事务已经提交之后读到他们修改过的值就可以了,但是别的事务没提交的 时候,绝对不会读到人家修改的值。

第三个是REPEATABLE READ隔离级别,就是可重复读级别

这个级别下,不会发生脏写、脏读和不可重复读的问题,因为你一个事务多次查询一个数据的值,哪怕 别的事务修改了这个值还提交了,没用,你不会读到人家提交事务修改过的值,你事务一旦开始,多次 查询一个值,会一直读到同一个值! 我们给大家一个图看看这个RR级别的效果,以后记得这个骚气的简写词,就是RR,公司里有兄弟让你 把事务设置成RR的时候,你也不要一脸懵逼。
一个事务A,第一次查询一行数据的值是值A 接着事务B修改了这行数据的值为值B,还提交了, 接着事务A再次查询这行数据的值,读到的还是值A,因为他在事务执行期间,多次读一行数据,绝对读 到的都是一样的值,他是允许可重复读的!

这就是第三种隔离级别,给一个骚气的名字,RR级别,记住了RR级别保证你不会读到人家已经提交的 事务修改过的值!但是他还是会发生幻读的 因为假设你一次SQL是根据条件查询,比如“select * from table where id>10”,第一次查出来10条数 据,结果另外一个事务插入了一条数据,下次你可能会查出来11条数据,还是会有幻读问题的! RR隔离级别,只不过保证对同一行数据的多次查询,你不会读到不一样的值,人家已提交事务修改了这 行数据的值,对你也没影响!

最后一个隔离就别,就是serializable级别

这种级别,根本就不允许你多个事务并发执行,只能串行起 来执行,先执行事务A提交,然后执行事务B提交,接着执行事务C提交,所以此时你根本不可能有幻读 的问题,因为事务压根儿都不并发执行! 但是这种级别一般除非脑子坏了,否则更不可能设置了,因为多个事务串行,那数据库恨不能一秒并发 就只有几十了,性能会极差的。

6、 MySQL是如何支持4种事务隔离级别的?Spring事务注解是如何设置的?

上次我们讲完了SQL标准下的4种事务隔离级别,平时比较多用的就是RC和RR两种级别,那么在MySQL 中也是支持那4种隔离级别的,基本的语义都是差不多的

但是要注意的一点是,MySQL默认设置的事务隔离级别,都是RR级别的,而且MySQL的RR级别是可以 避免幻读发生的。

这点是MySQL的RR级别的语义跟SQL标准的RR级别不同的,毕竟SQL标准里规定RR级别是可以发生幻 读的,但是MySQL的RR级别避免了!

也就是说,MySQL里执行的事务,默认情况下不会发生脏写、脏读、不可重复读和幻读的问题,事务的 执行都是并行的,大家互相不会影响,我不会读到你没提交事务修改的值,即使你修改了值还提交了, 我也不会读到的,即使你插入了一行值还提交了,我也不会读到的,总之,事务之间互相都完全不影 响! 当然,要做到这么神奇和牛叉的效果,MySQL是下了苦功夫的,后续我们接着就要讲解MySQL里的 MVCC机制,就是多版本并发控制隔离机制,依托这个MVCC机制,就能让RR级别避免不可重复读和幻 读的问题。

然后给大家说一下,假设你要修改MySQL的默认事务隔离级别,是下面的命令,可以设置级别为不同的 level,level的值可以是REPEATABLE READ,READ COMMITTED,READ UNCOMMITTED, SERIALIZABLE几种级别。
SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL level;
但是一般来说,真的其实不用修改这个级别,就用默认的RR其实就特别好,保证你每个事务跑的时候都 没人干扰,何乐而不为呢?

另外,给大家说一下,假设你在开发业务系统的时候,比如用Spring里的@Transactional注解来做事务 这块,假设某个事务你就是有点手痒痒,就想给弄成RC级别,你就想读到人家已经提交事务修改过的 值,好,那么没问题。 在@Transactional注解里是有一个isolation参数的,里面是可以设置事务隔离级别的,具体的设置方式 如下: @Transactional(isolation=Isolation.DEFAULT),然后默认的就是DEFAULT值,这个就是MySQL默认支 持什么隔离级别就是什么隔离级别。 那MySQL默认是RR级别,自然你开发的业务系统的事务也都是RR级别的了。 但是你可以手动改成Isolation.READ_UNCOMMITTED级别,此时你就可以读到人家没提交事务修改的 值了,够坑的!估计一般没人自己坑自己吧! 也可以改成Isolation.READ_COMMITTED,Isolation.REPEATABLE_READ,Isolation.SERIALIZABLE几 个级别,都是可以的。 但是再次提醒,其实默认的RR隔离机制挺好的,真的没必要去修改,除非你一定要在你的事务执行期间 多次查询的时候,必须要查到别的已提交事务修改过的最新值,那么此时你的业务有这个要求,你就把 Spring的事务注解里的隔离级别设置为Isolation.READ_COMMITTED级别,偶尔可能也是有这种需求 的。