多个事务并发更新以及查询数据,为什么会有脏写和脏读的问题?

对于我们的业务系统去访问数据库而言,他往往都是多个线程并发执行多个事务的,对于数据库而言,他会有多个事务同时执行,可能这多个事务还会同时更新和查询同一条数据,所以这里会有一些问题需要数据库来解决。
每个事务都会执行各种增删改查的语句,把磁盘上的数据页加载到buffer pool的缓存页里来,然后更新缓存页,记录redo log和undo log,最终提交事务或者是回滚事务,多个事务会并发干上述一系列事情。如果多个事务要是对缓存页里的同一条数据同时进行更新或者查询,此时会产生哪些问题呢?
这里实际上会涉及到脏写、脏读、不可重复读、幻读,四种问题。
这个脏写的话,他的意思就是说有两个事务,事务A和事务B同时在更新一条数据,事务A先把他更新为A值,事务B紧接着就把他更新为B值。所谓脏写,就是我刚才明明写了一个数据值,结果过了一会儿却没了!真是莫名其妙。而他的本质就是事务B去修改了事务A修改过的值,但是此时事务A还没提交,所以事务A随时会回滚,导致事务B修改的值也没了,这就是脏写的定义。
接着我们继续看坑爹的脏读问题。假设事务A更新了一行数据的值为A值,此时事务B去查询了一下这行数据的值,看到的值是不是A值?现在事务B可能还挺high的,拿着刚才查询到的A值做各种业务处理。大家知道,每个事务都是业务系统发出的,所以业务系统里的事务B此时肯定会拿到刚查出来的A值在做一些业务处理。但是接着坑爹的事情发生了,事务A突然回滚了事务,导致他刚才更新的A值没了,此时那行数据的值回滚为NULL值!然后事务B紧接着此时再次查询那行数据的值,看到的居然此时是NULL值?事务B此时简直欲哭无泪。所以这就是坑爹的脏读,他的本质其实就是事务B去查询了事务A修改过的数据,但是此时事务A还没提交,所以事务A随时会回滚导致事务B再次查询就读不到刚才事务A修改的数据了!这就是脏读。
无论是脏写还是脏读,都是因为一个事务去更新或者查询了另外一个还没提交的事务更新过的数据。因为另外一个事务还没提交,所以他随时可能会反悔会回滚,那么必然导致你更新的数据就没了,或者你之前查询到的数据就没了,这就是脏写和脏读两种坑爹场景。

一个事务多次查询一条数据读到的都是不同的值,这就是不可重复读?

先来说说这个不可重复读的问题,这个问题是这样的:假设我们有一个事务A开启了,在这个事务A里会多次对一条数据进行查询,然后呢,另外有两个事务,一个是事务B,一个是事务C,他们俩都是对一条数据进行更新的。
然后我们假设一个前提,就是比如说事务B更新数据之后,如果还没提交,那么事务A是读不到的,必须要事务B提交之后,他修改的值才能被事务A给读取到,其实这种情况下,就是我们首先避免了脏读的发生。因为脏读的意思就是事务A可以读到事务B修改过还没提交的数据,此时事务B一旦回滚,事务A再次读就读不到了,那么此时就会发生脏读问题。
此时会有另外一个问题,叫做不可重复读。假设缓存页里一条数据原来的值是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执行期间多次查到的值都不一样,都是别的提交过的事务修改过的值,那么此时你就可以认为,数据库有问题,这个问题就是“不可重复读”的问题!

听起来很恐怖的数据库幻读,到底是个什么奇葩问题?

简单来说,你一个事务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多版本隔离机制、锁机制,用一整套机制来解决多事务并发问题,接下来,我们将要深入讲解这些机制,让大家彻底能够理解数据库内部的执行原理。

SQL标准中对事务的4个隔离级别,都是如何规定的呢?

在SQL标准中规定了4种事务隔离级别,就是说多个事务并发运行的时候,互相是如何隔离的,从而避免一些事务并发问题。
这4种级别包括了:
read uncommitted(读未提交),read committed(读已提交),repeatable read(可重复读),serializable(串行化)
不同的隔离级别是可以避免不同的事务并发问题的。
第一个read uncommitted隔离级别,是不允许发生脏写的。也就是说,不可能两个事务在没提交的情况下去更新同一行数据的值,但是在这种隔离级别下,可能发生脏读,不可重复读,幻读。
第二个是read committed隔离级别,这个级别下,不会发生脏写和脏读,也就是说,人家事务没提交的情况下修改的值,你是绝对读不到的!但是呢,可能会发生不可重复读和幻读问题,因为一旦人家事务修改了值然后提交了,你事务是会读到的,所以可能你多次读到的值是不同的!
第三个是REPEATABLE READ隔离级别,就是可重复读级别。这个级别下,不会发生脏写、脏读和不可重复读的问题,因为你一个事务多次查询一个数据的值,哪怕别的事务修改了这个值还提交了,没用,你不会读到人家提交事务修改过的值,你事务一旦开始,多次查询一个值,会一直读到同一个值!但是他还是会发生幻读的。
最后一个隔离就别,就是serializable级别,这种级别,根本就不允许你多个事务并发执行,只能串行起来执行,先执行事务A提交,然后执行事务B提交,接着执行事务C提交,所以此时你根本不可能有幻读的问题,因为事务压根儿都不并发执行!但是这种级别一般除非脑子坏了,否则更不可能设置了,因为多个事务串行,那数据库很可能一秒并发就只有几十了,性能会极差的。

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

要注意的一点是,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;
假设你在开发业务系统的时候,比如用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级别,偶尔可能也是有这种需求的。