MySQL运行时多个事务同时执行是什么场景
引入
我们一般都是通过业务系统去对数据库执行CURD操作的
通常,我们都是在业务系统里开启事务来执行增删改操作的。比如:
也就是说,业务系统是通过执行一个一个的事务,每个事务可能是一个或者多个增删查改的SQL语句。
但是问题是业务一般是多线程的,也就是基于多线程并发对MySQL数据库去执行多个事务的,如下图:
问题:每个事务里面的多个SQL语句都是如何执行的呢?
如下图:从磁盘加载数据页到buffer pool缓存页里去,然后更新buffer pool里的缓存页,同时记录redo log和undo log日志。
如果事务提交之后,也就是redo log刷入磁盘,结果MySQL宕机了,那么可以根据redo log恢复事务修改过的缓存数据。
如果要回滚事务,那么就与undo log来回滚就可以了,把之前对缓存页做的修改都回滚了就可以了。
但是这个时候就有很多问题了:
- 多个事务并发执行的时候,可能会同时对缓存页里的一行数据进行更新,这个冲突怎么处理?是否要加锁?
- 可能有的事务在对一行数据做更新 ,有的事务在查询这行数据的时候,这里的冲突怎么处理?
多个事务并发更新以及查询数据,为什么会有脏写和脏读的问题
多个事务并发执行时候,对MySQL的缓存页里的同一行数据同时进行更新或者查询的时候,可能发生的脏写和脏读的问题
脏写
脏写就是说有两个事务,事务A和事务B同时在更新一条数据(这条数据初始值为NULL)
- 事务A先更新为A值
- 事务B紧接着把它更新为B值。
如下图:
此时这行数据为B值。
-
而且事务A更新之后会记录一条undo log日志:更新之前这行数据的值为NULL,主键为xx
-
那么此时事务B更新完了数据的值为B,结果此时事务A突然回滚了,那么会根据事务A的undo log日志去回滚,也就是那那行数据更新回之前的NULL值。
- 然后就尴尬了,事务B一看,为什么我更新的B值没了?就因为你事务A反悔了就把数据值回滚成NULL了,搞的我更新的B值也没了,这也太坑爹了吧!
所以对于事务B看到的场景,就是自己明明更新了,结果值却没有了,这就是脏写。
所谓脏写,就是我刚才明明写了一个数据,结果过了一会儿却没了!真是莫名其妙。
脏写的本质就是事务B去修改了事务A修改过的值,但是此时事务A还没有提交,所以事务A随时会回滚,导致事务B修改的值也没有了。
脏读
- 假设事务A更新了一行数据的值为A值,此时事务B去查询了一些这个数据的值,看到的是就是A值
- 现在事务B就会拿着刚刚查询到的A值做各种业务处理。
- 但是坑爹的事情发生了,事务A突然回滚了事务,导致它刚才更新的A值没了。此时那行数据的值回滚为NULL值
- 然后事务B紧接着再次查询那行数据的值,看到的就是NULL值了!
脏读,本质就是事务B去查询了事务A修改过的数据,但是此时事务A还没有提交,所以事务A随时会回滚导致事务B再次查询就读不到刚才事务A修改的数据了
小结
- 无论是脏读还是脏写,都是因为一个事务去更新或者查询了另外一个还没提交的事务更新后的数据
- 因为另外一个事务还没提交,所以它随时可以会反悔,那么必然导致你更新的数据没了,或者之前查询的数据没了。这就是脏写和脏读两种坑爹场景。
一次事务多次查询一条数据读到的都是不同的值,这就是不可重复读
- 假设我们有一个事务A开启了,在这个事务A里会多次会一条数据进行查询。 然后,另外有两个事务,事务B和事务C,都是对一条数据进行更新的。
- 然后我们假设一个前提,就是比如说事务B更新数据之后,如果还没提交,那么事务A是读不到的,必须要事务B提交之后,它修改的值才能被事务A给读取到。也就是说,避免了脏读的发生。
但是没有脏读就万事大吉了吗?绝对不是,还有另外一个问题,叫做不可重复读。
- 假设缓存页里一条数据原来的值是A值,此时事务A开启之后,第一次查询这条数据,读取到的就是A指,如下图:
- 接着事务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执行期间多次查到的值都不一样,都是别的提交过的事务修改过的值,那么此时你就可以认为,数据库有问题,这个问题就是“不可重复读”的问题!
幻读是什么奇葩问题
在了解幻读以及 MySQL 是如何解决幻读这个问题前,我们需要知道,什么是当前读、什么是快照读。
- 快照读:读取快照中的数据,不需要进行加锁。看到快照这两个字,各位肯定马上就想到 MVCC 了,是这样,MVCC 作用于读取已提交和可重复读(默认)这两个隔离级别,这俩隔离级别下的普通 select 操作就是快照读
- 当前读:读取的是最新版本的数据, 并且对读取的记录加锁, 阻塞其他事务同时改动相同记录,避免出现安全问题。
除了读取已提交和可重复读这俩隔离级别下的普通 select 操作,其余操作都是当前读
所谓幻读,即一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行。
在可重复读隔离级别下,普通的查询是快照读,当前事务是不会看到别的事务插入的数据的。因此,幻读问题在 “当前读” 下才会出现。
如下:
- 事务A,先发送一条SQL语句,里面有一个条件,要查询一批数据出来,比如"select * from table where id > 10"
- 然后,它一开始查询出来了2条数据。如下图:
- 这个时候,事务B往表里面插入了几条数据,而且事务B还提交了。如下图:
- 接着事务A此时第二次查询,再次按照之前的一模一样的条件执行“select * from table where id>10”这条SQL语句,由于其它事务插入了几条数据,导致这次它查询出来了4条数据,如下图所示:
幻读就是一个事务用一样的SQL多次查询,结果每次查询都会发现查到了一些之前没看到的数据
产生幻读的原因是,行锁只能锁住行,但是新插入记录这个动作,操作的是锁住的行之间的 “间隙”。因此,为了解决幻读问题,InnoDB 只好引入新的锁,也就是间隙锁 (Gap Lock)。
总结
- 脏写就是两个事务每提交的状况下,都修改同一条数据,结果一个事务回滚了,把另一个事务修改的值也撤销了。所谓脏写就是两个事务每提交状态下修改同一个值
- 脏读就是事务A修改了一条数据的值,结果还没有提交,事务B就读到了A修改的值,然后A回滚了,事务B再次读,就读不到了。也就是事务读到了修改之后没有提交的值
- 不可重复读这个问题,简单来说,就是一个事务多次查询一条数据,结果每次读到的值都不一样,这个过程中可能别的事务会修改这条数据的值,而且修改值之后事务都提交了,结果导致人家每次查到的值都不一样,都查到了提交事务修改过的值,这就是所谓的不可重复读。
- 幻读就是一个事务用一样的SQL多次查询,结果每次查询都会发现查到了一些之前没看到的数据
上面这四个问题都是因为业务系统会多线程并发执行,每个线程可能都会开启一个事务,每个事务都会执行增删改查操作。
然后数据库会并发执行多个事务,多个事务可能会并发的对缓存页里的同一批数据进行增删改查操作,于是这个并发增删改查同一批数据的问题,可能就会导致脏写、脏读、不可重复读、幻读这些问题。
所以这些问题的本质,就是数据库的多事务并发问题。为了解决多事务并发问题,数据库才设计了事务隔离机制、MVCC多版本隔离机制、锁机制,用一整套机制来解决多事务并发问题。