面试官:MySQL的可重复读级别能解决幻读问题吗?

引言

以前在深刻了解数据库理论的时候,了解到事务的不一样隔离级别可能存在的问题。为了更好的理解因此在MySQL数据库中测试复现这些问题。关于脏读和不可重复读在相应的隔离级别下都很容易的复现了。面试

可是对于幻读,我发如今可重复读的隔离级别下没有出现,当时想到难道是MySQL对幻读作了什么处理?sql

测试:数据库

建立一张测试用的表dept:并发

CREATETABLE`dept`(`id`int(11)NOTNULLAUTO_INCREMENT,`name`varchar(20)DEFAULTNULL,PRIMARYKEY(`id`))ENGINE=InnoDBAUTO_INCREMENT=12DEFAULTCHARSET=utf8insertintodept(name)values("后勤部")
面试官:MySQL的可重复读级别能解决幻读问题吗?

 

根据上面的流程执行,预期来讲应该是事务1的第一条select查询出一条数据,第二个select查询出两条数据(包含事务2提交的数据)。性能

可是在实际测试中发现第二条select实际上也只查询处理一条数据。这是可是根据数据库理论的可重复读的实现(排他锁和共享锁)这是不该该的状况。测试

在了解实际缘由前咱们先复习下事务的相关理论。atom

数据库原理理论

事务

事务(Transaction),通常是指要作的或所作的事情。在计算机术语中是指访问并可能更新数据库中各类数据项的一个程序执行单元(unit)。事务由事务开始(begin transaction)和事务结束(end transaction)之间执行的全体操做组成。spa

在关系数据库中,一个事务能够是一组SQL语句或整个程序。code

为何要有事务

一个数据库事务一般包含对数据库进行读或写的一个操做序列。它的存在包含有如下两个目的:orm

  • 为数据库操做提供了一个从失败中恢复到正常状态的方法,同时提供了数据库在异常状态下仍能保持一致性的方法。
  • 当多个应用程序在并发访问数据库时,能够在这些应用程序之间提供一个隔离方法,保证彼此的操做互相干扰。

事务特性

事务具备4个特性:原子性、一致性、隔离性、持久性。这四个属性一般称为 ACID 特性。

  • 原子性(atomicity):一个事务应该是一个不可分割的工做单位,事务中包括的操做要么都成功,要么都不成功。
  • 一致性(consistency):事务必须是使数据库从一个一致性状态变到另外一个一致性状态。一致性与原子性是密切相关的。
  • 隔离性(isolation):一个事务的执行不能被其余事务干扰。即一个事务内部的操做及使用的数据在事务未提交前对并发的其余事务是隔离的,并发执行的各个事务之间不能互相影响。
  • 持久性(durability):一个事务一旦成功提交,它对数据库中数据的改变就应该是永久性的。接下来的其余操做或故障不该该对其有任何影响。

事务之间的几个特性并非一组同等的概念:

若是在任什么时候刻都只有一个事务,那么其自然是具备隔离性的,这时只要保证原子性就能具备一致性。

若是存在并发的状况下,就须要保证原子性和隔离性才能保证一致性。

数据库并发事务中存在的问题

若是不考虑事务的隔离性,会发生如下几种问题:

脏读

脏读是指在一个事务处理过程里读取了另外一个未提交的事务中的数据。当一个事务正在屡次修改某个数据,而在这个事务中这屡次的修改都还未提交,这时一个并发的事务来访问该数据,就会形成两个事务获得的数据不一致。

不可重复读

不可重复读是指在对于数据库中的某条数据,一个事务范围内屡次查询返回不一样的数据值(这里不一样是指某一条或多条数据的内容先后不一致,但数据条数相同),这是因为在查询间隔,该事务须要用到的数据被另外一个事务修改并提交了。

不可重复读和脏读的区别是,脏读是某一事务读取了另外一个事务未提交的脏数据,而不可重复读则是读取了其余事务提交的数据。须要注意的是在某些状况下不可重复读并非问题。

幻读

幻读是事务非独立执行时发生的一种现象。例如事务T1对一个表中全部的行的某个数据项作了从“1”修改成“2”的操做,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值仍是为“1”而且提交给数据库。

而操做事务T1的用户若是再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像产生幻觉同样,这就是发生了幻读。

幻读和不可重复读都是读取了另外一条已经提交的事务(这点就脏读不一样),所不一样的是不可重复读可能发生在update,delete操做中,而幻读发生在insert操做中。

排他锁,共享锁

排它锁(Exclusive),又称为X 锁,写锁。

共享锁(Shared),又称为S 锁,读锁。

读写锁之间有如下的关系:

  • 一个事务对数据对象O加了 S 锁,能够对 O进行读取操做,可是不能进行更新操做。加锁期间其它事务能对O 加 S 锁,可是不能加 X 锁。
  • 一个事务对数据对象 O 加了 X 锁,就能够对 O 进行读取和更新。加锁期间其它事务不能对 O 加任何锁。

即读写锁之间的关系能够归纳为:多读单写

事务的隔离级别

在事务中存在如下几种隔离级别:

读未提交(Read Uncommitted)

解决更新丢失问题。若是一个事务已经开始写操做,那么其余事务则不容许同时进行写操做,但容许其余事务读此行数据。该隔离级别能够经过“排他写锁”实现,即事务须要对某些数据进行修改必须对这些数据加 X 锁,读数据不须要加 S 锁。

读已提交(Read Committed)

解决了脏读问题。读取数据的事务容许其余事务继续访问该行数据,可是未提交的写事务将会禁止其余事务访问该行。这能够经过“瞬间共享读锁”和“排他写锁”实现, 即事务须要对某些数据进行修改必须对这些数据加 X 锁,读数据时须要加上 S 锁,当数据读取完成后马上释放 S 锁,不用等到事务结束。

可重复读取(Repeatable Read)

禁止不可重复读取和脏读取,可是有时可能出现幻读数据。读取数据的事务将会禁止写事务(但容许读事务),写事务则禁止任何其余事务。

Mysql默认使用该隔离级别。这能够经过“共享读锁”和“排他写锁”实现,即事务须要对某些数据进行修改必须对这些数据加 X 锁,读数据时须要加上 S 锁,当数据读取完成并不马上释放 S 锁,而是等到事务结束后再释放。

串行化(Serializable)

解决了幻读的问题的。提供严格的事务隔离。它要求事务序列化执行,事务只能一个接着一个地执行,不能并发执行。仅仅经过“行级锁”是没法实现事务序列化的,必须经过其余机制保证新插入的数据不会被刚执行查询操做的事务访问到。

MySQL中的隔离级别的实现

上面的内容解释了一些数据库理论的概念,可是在MySQL、ORACLE这样的数据库中,为了性能的考虑并非彻底按照上面介绍的理论来实现的。

MVCC

多版本并发控制(Multi-Version Concurrency Control, MVCC)是MySQL中基于乐观锁理论实现隔离级别的方式,用于实现读已提交和可重复读取隔离级别的实现。

实现(隔离级别为可重复读)

在说到如何实现前先引入两个概念:

  • 系统版本号:一个递增的数字,每开始一个新的事务,系统版本号就会自动递增。
  • 事务版本号:事务开始时的系统版本号。

在MySQL中,会在表中每一条数据后面添加两个字段:

  • 建立版本号:建立一行数据时,将当前系统版本号做为建立版本号赋值
  • 删除版本号:删除一行数据时,将当前系统版本号做为删除版本号赋值

SELECT

select时读取数据的规则为:建立版本号<=当前事务版本号,删除版本号为空或>当前事务版本号。

建立版本号<=当前事务版本号保证取出的数据不会有后启动的事务中建立的数据。这也是为何在开始的示例中咱们不会查出后来添加的数据的缘由

删除版本号为空或>当前事务版本号保证了至少在该事务开启以前数据没有被删除,是应该被查出来的数据。

INSERT

insert是将当前的系统版本号赋值给建立版本号字段。

UPDATE

插入一条新记录,保存当前事务版本号为行建立版本号,同时保存当前事务版本号到原来删除的行,实际上这里的更新是经过delete和insert实现的。

DELETE

删除时将当前的系统版本号赋值给删除版本号字段,标识该行数据在那一个事务中会被删除,即便实际上在位commit时该数据没有被删除。根据select的规则后开启的数据也不会查询到该数据。

MVCC真的解决了幻读?

从最开始咱们的测试示例和上面的理论支持来看貌似在MySQL中经过MVCC就解决了幻读的问题,那既然这样串行化读貌似就没啥意义了,带着疑问继续测试。

测试前数据:

面试官:MySQL的可重复读级别能解决幻读问题吗?

 

面试官:MySQL的可重复读级别能解决幻读问题吗?

 

根据上面的结果咱们指望的结果是这样的:

idname1财务部2研发部

可是实际上咱们的通过是:

面试官:MySQL的可重复读级别能解决幻读问题吗?

 

原本咱们但愿获得的结果只是第一条数据的部门改成财务,可是结果确实两条数据都被修改了。

这种结果告诉咱们其实在MySQL可重复读的隔离级别中并非彻底解决了幻读的问题,而是解决了读数据状况下的幻读问题。而对于修改的操做依旧存在幻读问题,就是说MVCC对于幻读的解决时不完全的。

快照读和当前读

出现了上面的状况咱们须要知道为何会出现这种状况。在查阅了一些资料后发如今RR级别中,经过MVCC机制,虽然让数据变得可重复读,但咱们读到的数据多是历史数据,不是数据库最新的数据。

这种读取历史数据的方式,咱们叫它快照读 (snapshot read),而读取数据库最新版本数据的方式,叫当前读 (current read)。

select 快照读

当执行select操做是innodb默认会执行快照读,会记录下此次select后的结果,以后select 的时候就会返回此次快照的数据,即便其余事务提交了不会影响当前select的数据,这就实现了可重复读了。

快照的生成当在第一次执行select的时候,也就是说假设当A开启了事务,而后没有执行任何操做,这时候B insert了一条数据而后commit,这时候A执行 select,那么返回的数据中就会有B添加的那条数据。

以后不管再有其余事务commit都没有关系,由于快照已经生成了,后面的select都是根据快照来的。

当前读

对于会对数据修改的操做(update、insert、delete)都是采用当前读的模式。在执行这几个操做时会读取最新的记录,即便是别的事务提交的数据也能够查询到。

假设要update一条记录,可是在另外一个事务中已经delete掉这条数据而且commit了,若是update就会产生冲突,因此在update的时候须要知道最新的数据。也正是由于这样因此才致使上面咱们测试的那种状况。

select的当前读须要手动的加锁:

select*fromtablewhere?lockinsharemode;select*fromtablewhere?forupdate;

有个问题说明下

在测试过程当中最开始我觉得使用begin语句就是开始一个事务了,因此在上面第二次测试中由于先开始的事务1,结果在事务1中却查到了事务2新增的数据,当时认为这和前面MVCC中的select的规则不一致了,因此作了以下测试:

面试官:MySQL的可重复读级别能解决幻读问题吗?

 

SELECT*FROMinformation_schema.INNODB_TRX//用于查询当前正在执行中的事务

能够看到若是只是执行begin语句实际上并无开启一个事务。

下面在begin后添加一条select语句:

面试官:MySQL的可重复读级别能解决幻读问题吗?

 

因此要明白其实是对数据进行了增删改查等操做后才开启了一个事务。

如何解决幻读

很明显可重复读的隔离级别没有办法完全的解决幻读的问题,若是咱们的项目中须要解决幻读的话也有两个办法:

  • 使用串行化读的隔离级别
  • MVCC+next-key locks:next-key locks由record locks(索引加锁) 和 gap locks(间隙锁,每次锁住的不光是须要使用的数据,还会锁住这些数据附近的数据)

实际上不少的项目中是不会使用到上面的两种方法的,串行化读的性能太差,并且其实幻读不少时候是咱们彻底能够接受的。

相关文章
相关标签/搜索