如果多个事务要是对缓存页里的同一条数据同时进行更新或者查询,此时会产生哪些问题呢?
这里实际上会涉及到脏写、脏读、不可重复读、幻读,四种问题。
脏写
先看第一种问题,脏写
这个脏写的话,他的意思就是说有两个事务,事务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修改的值也没了,这就是脏写的定义。
InnoDB使用锁来保证不会有脏写情况的发生,也就是在第一个事务更新了某条记录后,就会给这条记录加锁,另一个事务再次更新时就需要等待第一个事务提交了,把锁释放之后才可以继续更新。
脏读
如果一个事务读到了另一个未提交事务修改过的数据,那就意味着发生了脏读。
此情况仅会发生在:读未提交的的隔离级别。
假设事务A更新了一行数据的值为A值,此时事务B去查询了一下这行数据的值,看到的值是不是A值?没错,此时如下图所示。
好,现在事务B可能还挺high的,拿着刚才查询到的A值做各种业务处理。大家知道,每个事务都是业务系统发出的,所以业务系统里的事务B此时肯定会拿到刚查出来的A值在做一些业务处理。
但是接着坑爹的事情发生了,事务A突然回滚了事务,导致他刚才更新的A值没了,此时那行数据的值回滚为NULL值!
然后事务B紧接着此时再次查询那行数据的值,看到的居然此时是NULL值?事务B此时简直欲哭无泪,看下图:
所以这就是坑爹的脏读,他的本质其实就是事务B去查询了事务A修改过的数据,但是此时事务A还没提交,所以事务A随时会回滚导致事务B再次查询就读不到刚才事务A修改的数据了!这就是脏读。
其实一句话总结,无论是脏写还是脏读,都是因为一个事务去更新或者查询了另外一个还没提交的事务更新过的数据。
因为另外一个事务还没提交,所以他随时可能会反悔会回滚,那么必然导致你更新的数据就没了,或者你之前查询到的数据就没了,这就是脏写和脏读两种坑爹场景。
不可重复读
多个事务并发执行时候,对MySQL的缓存页里的同一行数据同时进行更新或者查询的时候,可能发生的脏写和脏读的问题,之所以会发生脏写和脏读,最关键的,其实是因为你一个事务写或者查的是人家事务还没提交的时候更新过的数据,所以人家事务随时会反悔回滚,导致你这里有问题。
如果一个事务只能读到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交后,该事务都能查询得到最新值,那就意味着发生了不可重复读。
不可重复读仅会发生在:读未提交、读提交的隔离级别。
不可重复读隔离级别使用了mvcc解决了不可重复读和幻读(解决部分幻读)。
假设我们有一个事务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执行期间多次查到的值都不一样,都是别的提交过的事务修改过的值,那么此时你就可以认为,数据库有问题,这个问题就是“不可重复读”的问题!
数据库幻读
今天我们来最后讲一种数据库的并发问题,就是听着有点恐怖的幻读问题,幻读听起来很恐怖,搞的跟邪恶的巫师给你搞什么魔法一样,是不是?
其实没那么恐怖的,我们今天给大家来讲讲。
一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为幻读。幻读是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,比如这种修改涉及到表中的“全部数据行”。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入“一行新数据”。那么,以后就会发生操作第一个事务的用户发现表中还存在没有修改的数据行,就好象发生了幻觉一样.一般解决幻读的方法是增加范围锁RangeS,锁定检索范围为只读,这样就避免了幻读。
此情况会回发生在:读未提交、读提交、可重复读的隔离级别.
简单来说,你一个事务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多次查询,结果每次查询都会发现查到了一些之前没看到过的数据
注意,幻读特指的是你查询到了之前查询没看到过的数据!此时就说你是幻读了。
Session A中的事务先根据条件number > 0这个条件查询表hero,得到了name列值为'刘备'的记录;
之后Session B中提交了一个隐式事务,该事务向表hero中插入了一条新记录;
之后Session A中的事务再根据相同的条件number > 0查询表hero,得到的结果集中包含Session B中的事务新插入的那条记录,这种现象也、被称之为幻读。
有的同学会有疑问,那如果Session B中是删除了一些符合number > 0的记录而不是插入新记录,那Session A中之后再根据number > 0的条件读取的记录变少了,这种现象算不算幻读呢?
明确说一下,这种现象不属于幻读,幻读强调的是一个事务按照某个相同条件多次读取记录时,后读取时读到了之前没有读到的记录。
那对于先前已经读到的记录,之后又读取不到这种情况,算啥呢?其实这相当于对每一条记录都发生了不可重复读的现象。
幻读只是重点强调了读取到了之前读取没有获取到的记录。
举例说明读提交和可重复读
其中“读提交”和“可重复读”比较难理解,所以我用一个例子说明这几种隔离级别。假设数据表T中只有一列,其中一行的值为1,下面是按照时间顺序执行两个事务的行为。
mysql> create table T(c int) engine=InnoDB;
insert into T(c) values(1); 我们来看看在不同的隔离级别下,事务A会有哪些不同的返回结果,也就是图里面V1、V2、V3的返回值分别是什么。
· 若隔离级别是读未提交,则V1=2 V2=2 V3=2,这时候事务B虽然还没有提交,但是结果可以被A看到。因此,V2、V3也都是2。
· 若隔离级别是读提交,V1=1 V2=2 V3=2,事务B的更新在提交后才能被A看到。所以, V3的值也是2。
· 若隔离级别是可重复读,V1=1 V2=1 V3=2,之所以V2还是1,遵循的就是这个要求:事务在执行期间看到的数据前后必须是一致的。
· 若隔离级别是串行化,V1=1 V2=1 V3=2,则在事务B执行“将1改成2”的时候,会被锁住。直到事务A提交后,事务B才可以继续执行。所以从A的角度看, V1、V2值是1,V3的值是2。
在实现上,数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。在“可重复读”隔离级别下,这个视图是在事务启动时创建的,整个事务存在期间都用这个视图。在“读提交”隔离级别下,这个视图是在每个SQL语句开始执行的时候创建的。这里需要注意的是,“读未提交”隔离级别下直接返回记录上的最新值,没有视图概念;而“串行化”隔离级别下直接用加锁的方式来避免并行访问。
我们可以看到在不同的隔离级别下,数据库行为是有所不同的。Oracle数据库的默认隔离级别其实就是“读提交”,因此对于一些从Oracle迁移到MySQL的应用,为保证数据库隔离级别的一致,你一定要记得将MySQL的隔离级别设置为“读提交”。
总结来说,存在即合理,哪个隔离级别都有它自己的使用场景,你要根据自己的业务情况来定。我想你可能会问那什么时候需要“可重复读”的场景呢?我们来看一个数据校对逻辑的案例。
假设你在管理一个个人银行账户表。一个表存了每个月月底的余额,一个表存了账单明细。这时候你要做数据校对,也就是判断上个月的余额和当前余额的差额,是否与本月的账单明细一致。你一定希望在校对过程中,即使有用户发生了一笔新的交易,也不影响你的校对结果。
这时候使用“可重复读”隔离级别就很方便。事务启动时的视图可以认为是静态的,不受其他事务更新的影响。
MysqlRR级别下是否解决了幻读?
MVCC加上间隙锁的方式 (1)在快照读读情况下,mysql通过mvcc来避免幻读。 (2)在当前读读情况下,mysql通过next-key来避免幻读。锁住某个条件下的数据不能更改。
快照读解决了幻读
答:可重复读的隔离级别是能解决幻读的。
首先,只有在当前读的前提下才会出现幻读,快照读是不存在幻读的,因为同一个事务每一次快照读都是一致的。
当前读解决幻读
执行当前读的时候会有next-key lock : 即 record lock + gap lock
快照读转当前读
事务一
begin ;
select * from example_copy1 where id =10000; //1.没有数据
UPDATE example_copy1 set dr = 0 where id =10000;//3.更新数据
select * from example_copy1 where id =10000;//4.有数据
commit;
事务二:执行时机是在事务一第一条查询语句执行完
begin ;
insert into example_copy1(id) values(10000);//2.插入数据
commit;
很明显可重复读的隔离级别没有办法彻底的解决幻读的问题,应该避免上面的写法。
避免由快照读转为当前读。