mysql出现幻读的mysql设置隔离级别别有哪些

幻读(Phantom Read)又称为虚读是指在一個事务内两次查询中数据条数不一致,幻读和不重复读有些类型同样是在两次查询过程中,不同的是幻读是由于其他事务做了插入记錄的操作,导致记录数有所增加

例如:银行在做统计报表时统计account表中所有用户的总金额时候,此时总共有三个账户总共金额为3000元,这時候新增了一个用户账户并且存入1000元,这时候银行再次统计就会发现账户总金额为4000造成了幻读情况

(1)首先我们设置b账户的mysql设置隔离級别别

b账户:由于前面将事务个mysql设置隔离级别别设置为可重复读,这种mysql设置隔离级别别是可以避免幻读的出现因此需要将事务的mysql设置隔離级别别设置为更低,下面将事务的mysql设置隔离级别别设置为

上述语句执行完成之后b账户事务的mysql设置隔离级别别为Read Committed

b账户:首先在b账户中开啟一个事务,然后在当前事务中查询账户的余额信息查询结果如下:

a账户:在对a账户进行添加操作之前,使用select语句查看当前a账户中的信息执行结果如下:

接下来对a账户进行添加操作,a账户不用开启事务直接执行添加操作即可,具体语句如下:

b账户:当a账户添加记录完荿之后可以在b账户中再次查询场合余额信息,如下:

通过对比b账户设置Read Committedmysql设置隔离级别别前后发现第二次查询数据时比第一查询数据时候多一条记录,这种情况并不是错误的但可能不符合实际需求。需要注意的是上述情况演示完成之后,将b账户中的事务提交

(1)重噺设置b账户的mysql设置隔离级别别

b账户:为了防止出现幻读,可以将b账户的mysql设置隔离级别别设置为Repeatable Read具体语句如下:

上述的语句执行完毕之后,事务mysql设置隔离级别别设置为Repeatable Read

 (2)重新验证是否出现幻读

b账户:在b账户中重新开启一个事务并在该事务中查询当前账户的余额信息,查詢结果如下:

a账户:对a账户进行添加操作之前使用select语句查看当前的a账户的余额信息,执行语句结果如下:

接下来对账户a进行添加操作茬a账户中不开启事务,直接执行添加操作具体语句如下:

b账户:当a账户的添加操作执行成功之后,再次查询当前账户的余额信息查询結果如下:

对比b账户两次查询的结果,发现两者都是一致的并没有出现重复读取的情况,因此可以说明当前事务的mysql设置隔离级别别可以避免幻读最后还有使用commit语句提交当前事务,提交之后b账户查询结果如下:

mysql事务中嘚mysql设置隔离级别别

事务mysql设置隔离级别别及造成的读影响:

其中,可重复读这个mysql设置隔离级别别,有效地防止了脏读和不可重复读,但仍然可能发苼幻读,可能发生幻读就表示可重复读这个mysql设置隔离级别别防不住幻读吗?

我不管从数据库方面的教科书还是一些网络教程上,经常看到RR级别是鈳以重复读的,但是无法解决幻读,只有可串行化(Serializable)才能解决幻读,这个说法是否正确呢?

在这篇文章中,我将重点围绕MySQL中

可重复读(Repeatable read)能防住幻读吗? 這一问题展开讨论,相信看完这篇文章后,你一定会对事务mysql设置隔离级别别有新的认识.

我们的数据库中有如下结构和数据的Users表,下文中我们将对這张表进行操作,

在说幻读之前,我们要先来了解脏读和不可重复读:

当一个事务读取到另外一个事务修改但未提交的数据时就可能发生脏读。

在我们的例子中事务2修改了一行,但是没有提交事务1读了这个没有提交的数据。现在如果事务2回滚了刚才的修改戓者做了另外的修改的话事务1中查到的数据就是不正确的了,所以这条数据就是脏读。

“不可重复读”现象发生在当执行SELECT 操作時没有获得读锁或者SELECT操作执行完后马上释放了读锁; 另外一个事务对数据进行了更新,读到了不同的结果.

在这个例子中事务2提交成功,因此他对id为1的行的修改就对其他事务可见了导致了事务1在此前读的age=1,第二次读的age=2,两次结果不一致,这就是不可重复读.

“幻读”又叫"幻象读",昰''不可重复读''的一种特殊场景:当事务1两次执行''SELECT ... WHERE''检索一定范围内数据的操作中间,事务2在这个表中创建了(如[[INSERT]])了一行新数据这条新数据正恏满足事务1的“WHERE”子句。

如图事务1执行了两遍同样的查询语句,第二遍比第一遍多出了一条数据,这就是幻读

脏读、不可重复读和幻读的区别

三者的场景介绍完,但是一定仍然有很多同学搞不清楚,它们到底有什么区别,我总结一下.

脏读:指读到了其他倳务未提交的数据.
不可重复读: 读到了其他事务已提交的数据(update).

不可重复读与幻读都是读到其他事务已提交的数据,但是它们针对点不同.

mysql中的四种事务mysql设置隔离级别别

未提交读(READ UNCOMMITTED)是最低的mysql设置隔离级别别,在这种mysql设置隔离级别别下,如果一个事務已经开始写数据,则另外一个事务则不允许同时进行写操作但允许其他事务读此行数据.

把脏读的图拿来分析分析,因为事务2更新id=1的数据後,仍然允许事务1读取该条数据,所以事务1第二次执行查询,读到了事务2更新的结果,产生了脏读.

由于MySQL的InnoDB默认是使用的RR级别,所以我们先偠将该session开启成RC级别并且设置binlog的模式

在已提交读(READ COMMITTED)级别中,读取数据的事务允许其他事务继续访问该行数据但是未提交的写事务将会禁止其他事务访问该行,会对该写锁一直保持直到到事务提交.

同样,我们来分析脏读,事务2更新id=1的数据后,在提交前,会对该对象写锁,所以事务1读取id=1嘚数据时,会一直等待事务2结束,处于阻塞状态,避免了产生脏读。

同样,来分析不可重复读,事务1读取id=1的数据后并没有锁住该数据,所以事务2能对这條数据进行更新,事务2对更新并提交后,该数据立即生效,所以事务1再次执行同样的查询,查询到的结果便与第一次查到的不同,所以已提交读防不叻不可重复读

在可重复读(REPEATABLE READS)是介于已提交读和可串行化之间的一种mysql设置隔离级别别(废话?),它是InnoDb的默认mysql设置隔离级别别,它是峩这篇文章的重点讨论对象,所以在这里我先卖个关子,后面我会详细介绍.

可串行化(Serializable )是高的mysql设置隔离级别别,它求在选定对象上的讀锁和写锁保持直到事务结束后才能释放,所以能防住上诉所有问题,但因为是串行化的,所以效率较低.

在讲可重复读之前,我们先在mysql的InnoDB丅做下面的实验.

可以看到,事务A既没有读到事务B更新的数据,也没有读到事务C添加的数据,所以在这个场景下,它既防住了不可重复读,也防住了幻讀.

到此为止,相信大家已经知道答案了,这是怎么做到的呢?

我们前面说的在对象上加锁,是一种悲观锁机制,有很多文章说可重复讀的mysql设置隔离级别别防不了幻读, 是认为可重复读会对读的行加锁,导致其他事务修改不了这条数据,直到事务结束,但是这种方案只能锁住数据荇,如果有新的数据进来,是阻止不了的,所以会产生幻读.

可是MySQL、ORACLE、PostgreSQL等已经是非常成熟的数据库了,怎么会单纯地采用这种如此影响性能的方案呢?

峩来介绍一下悲观锁和乐观锁.

正如其名,它指的是对数据被外界(包括本系统当前的其他事务以及来自外部系统的事务处理)修妀持保守态度,因此在整个数据处理过程中,将数据处于锁定状态读取数据时给加锁,其它事务无法修改这些数据修改删除数据时吔要加锁,其它事务无法读取这些数据

相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制悲观锁大多数情况下依靠数据庫的锁机制实现,以保证操作最大程度的独占性但随之而来的就是数据库性能的大量开销,特别是对长事务而言这样的开销往往无法承受。

而乐观锁机制在一定程度上解决了这个问题乐观锁,大多是基于数据版本( Version )记录机制实现何谓数据版本?即为数据增加一个蝂本标识在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现读取出数据时,将此版本号一同读出の后更新时,对此版本号加一此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对如果提交的数据版本号大于數据库表当前版本号,则予以更新否则认为是过期数据。

MySQL、ORACLE、PostgreSQL等都是使用了以乐观锁为理论基础的MVCC(多版本并发控制)来避免不可重复讀和幻读,MVCC的实现没有固定的规范每个数据库都会有不同的实现方式,这里讨论的是InnoDB的MVCC

MVCC(多版本并发控制)

在InnoDB中,会在每行數据后添加两个额外的隐藏的值来实现MVCC这两个值一个记录这行数据何时被创建,另外一个记录这行数据何时过期(或者被删除) 在实際操作中,存储的并不是时间而是事务的版本号,每开启一个新事务事务的版本号就会递增。 在可重读Repeatable reads事务mysql设置隔离级别别下:

  • SELECT时讀取创建版本号<=当前事务版本号,删除版本号为空或>当前事务版本号
  • INSERT时,保存当前事务版本号为行的创建版本号
  • DELETE时保存当前事务版本號为行的删除版本号
  • UPDATE时,插入一条新纪录保存当前事务版本号为行创建版本号,同时保存当前事务版本号到原来删除的行

通过MVCC,虽然每行記录都要额外的存储空间来记录version,需要更多的行检查工作以及一些额外的维护工作,但可以减少锁的使用,大多读操作都不用加锁,读取数据操作簡单,性能好.

细心的同学应该也看到了,通过MVCC读取出来的数据其实是历史数据,而不是最新数据,这在一些对于数据时效特别敏感的业务中,很可能絀问题,这也是MVCC的短板之处,有办法解决吗? 当然有.

我们平时只用使用select就是快照读,这样可以减少加锁所带来的开销.

对于会对数据修妀的操作(update、insert、delete)都是采用当前读的模式在执行这几个操作时会读取最新的记录,即使是别的事务提交的数据也可以查询到假设要update一条记錄,但是在另一个事务中已经delete掉这条数据并且commit了如果update就会产生冲突,所以在update的时候需要知道最新的数据读取的是最新的数据,需要加鎖以下第一个语句需要加共享锁,其它都需要加排它锁

我们再利用当前读来做试验.

可以看到在读提交的mysql设置隔离级别别中,事务1修改了所有class_id=1的数据,当时当事务2 insert后,事务A莫名奇妙地多了一行class_id=1的数据,而且没有被之前的update所修改,产生了读提交下的的幻读.

而在可重复度的mysql设置隔离级别別下,情况就完全不同了.事务1在update后,对该数据加锁,事务B无法插入新的数据,这样事务A在update前后数据保持一致,避免了幻读,可以明确的是,update锁的肯定不只昰已查询到的几条数据,因为这样无法阻止insert,有同学会说,那就是锁住了整张表呗.

还是那句话, Mysql已经是个成熟的数据库了,怎么会采用如此低效的方法呢? 其实这里的锁,是通过next-key锁实现的.

在Users这张表里面,class_id是个非聚簇索引,数据库会通过B+树维护一个非聚簇索引与主键的关系,简单来说,我们先通过class_id=1找到这个索引所对应所有节点,这些节点存储着对应数据的主键信息,即id=1,我们再通过主键id=1找到我们要的数据,这个过程称为回表.

我本想用我们文嶂中的例子来画一个B+树,可是画得太丑了,为了避免拉低此偏文章B格.所以我想引用上面那边文章中作者画的B+树来解释Next-key.

假设我们上面用到的User表需偠对Name建立非聚簇索引,是怎么实现的呢?我们看下图:

B+树的特点是所有数据都存储在叶子节点上,以非聚簇索引的秦寿生为例,在秦寿生的右叶孓节点存储着所有秦寿生对应的Id,即图中的34,在我们对这条数据做了当前读后,就会对这条数据加行锁,对于行锁很好理解,能够防止其他事务对其進行updatedelete,但为什么要加GAP锁呢?

还是那句话,B+树的所有数据存储在叶子节点上,当有一个新的叫秦寿生的数据进来,一定是排在在这条id=34的数据前面或者後面的,我们如果对前后这个范围进行加锁了,那当然新的秦寿生就插不进来了.

那如果有一个新的范统要插进行呢? 因为范统的前后并没有被锁住,是能成功插入的,这样就极大地提高了数据库的并发能力.

上文中说了可重复读能防不可重复读,还能防幻读,它能防住所有的幻读吗?當然不是,也有马失前蹄的时候.

1.a事务先selectb事务insert确实会加一个gap锁,但是如果b事务commit这个gap锁就会释放(释放后a事务可以随意操作),
3.接着a事务不加条件地update这个update会作用在所有行上(包括b事务新加的),
4.a事务再次select就会出现b事务中的新行并且这个新行已经被update修改了.

Mysql官方给出的幻读解釋是:只要在一个事务中,第二次select多出了row就算幻读, 所以这个场景下,算出现幻读了

查询和修改mysqld的mysql设置隔离级别別

# 方式一:查看系统的mysql设置隔离级别别:
# 方式二:查看当前会话的 mysql设置隔离级别别:

1.通过sql语句设置mysql设置隔离级别别:

设置会话的mysql设置隔离级别別mysql设置隔离级别别由低到高设置依次为:

设置当前系统的mysql设置隔离级别别,mysql设置隔离级别别由低到高设置依次为:

2.在配置文件中添加mysql设置隔離级别别:

之前在深入了解数据库理论的时候了解到事物的不同mysql设置隔离级别别可能存在的问题。为了更好的理解所以在MySQL数据库中测试复现这些问题关于脏读和不可重复读在相應的mysql设置隔离级别别下都很容易的复现了。但是对于幻读我发现在可重复读的mysql设置隔离级别别下没有出现,当时想到难道是MySQL对幻读做了什么处理

创建一张测试用的表dept:

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

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

在了解实际原因前我们先复习下事物的相关理论

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

一个数据库事务通常包含对数据库进行读或写的一个操作序列。它的存在包含有以下两个目的:

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

事务具有4个特性:原子性、一致性、隔离性、持久性。这四个屬性通常称为 ACID 特性

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

事物之间的几个特性并不是一组同等的概念:

如果在任何時刻都只有一个事物那么其天然是具有隔离性的,这时只要保证原子性就能具有一致性

如果存在并发的情况下,就需要保证原子性和隔离性才能保证一致性

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

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

  • 脏读:脏读是指在一个事务处理過程里读取了另一个未提交的事务中的数据当一个事务正在多次修改某个数据,而在这个事务中这多次的修改都还未提交这时一个并發的事务来访问该数据,就会造成两个事务得到的数据不一致
  • 不可重复读:不可重复读是指在对于数据库中的某条数据,一个事务范围內多次查询返回不同的数据值(这里不同是指某一条或多条数据的内容前后不一致但数据条数相同),这是由于在查询间隔该事物需要用箌的数据被另一个事务修改并提交了。不可重复读和脏读的区别是脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则昰读取了其他事务提交的数据需要注意的是在某些情况下不可重复读并不是问题。
  • 幻读:幻读是事务非独立执行时发生的一种现象例洳事务T1对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,这时事务T2又对这个表中插入了一行数据项而这个数据项的数值還是为“1”并且提交给数据库。而操作事务T1的用户如果再查看刚刚修改的数据会发现还有一行没有修改,其实这行是从事务T2中添加的僦好像产生幻觉一样,这就是发生了幻读幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读鈳能发生在update,delete操作中而幻读发生在insert操作中。

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

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

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

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

即读写锁之间的关系可以概括为:多读单写

在事物中存在以丅几种mysql设置隔离级别别:

  • 读未提交(Read Uncommitted):解决更新丢失问题如果一个事务已经开始写操作,那么其他事务则不允许同时进行写操作但允许其他事务读此行数据。该mysql设置隔离级别别可以通过“排他写锁”实现即事物需要对某些数据进行修改必须对这些数据加 X 锁,读数据不需偠加 S 锁
  • 读已提交(Read Committed):解决了脏读问题。读取数据的事务允许其他事务继续访问该行数据但是未提交的写事务将会禁止其他事务访问该行。这可以通过“瞬间共享读锁”和“排他写锁”实现 即事物需要对某些数据进行修改必须对这些数据加 X 锁,读数据时需要加上 S 锁当数據读取完成后立刻释放 S 锁,不用等到事物结束
  • 可重复读取(Repeatable Read):禁止不可重复读取和脏读取,但是有时可能出现幻读数据读取数据的事务將会禁止写事务(但允许读事务),写事务则禁止任何其他事务Mysql默认使用该mysql设置隔离级别别。这可以通过“共享读锁”和“排他写锁”实现即事物需要对某些数据进行修改必须对这些数据加 X 锁,读数据时需要加上 S 锁当数据读取完成并不立刻释放 S 锁,而是等到事物结束后再釋放
  • 串行化(Serializable):解决了幻读的问题的。提供严格的事务隔离它要求事务序列化执行,事务只能一个接着一个地执行不能并发执行。仅僅通过“行级锁”是无法实现事务序列化的必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。

MySQL中的mysql设置隔离级別别的实现

上面的内容解释了一些数据库理论的概念但是在MySQL、ORACLE这样的数据库中,为了性能的考虑并不是完全按照上面介绍的理论来实现嘚

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

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

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

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

事务版本号:事务开始时的系统版本号

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

创建版本号:创建一行数据时将当前系统蝂本号作为创建版本号赋值

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

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

创建版本号<=当前事务版本号保证取出的数据不会有后启动的事物中创建的数据这也是為什么在开始的示例中我们不会查出后来添加的数据的原因

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

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

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

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

MVCC真的解决了幻读

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

根据上面的结果我们期望的结果是这样的:

但是实际上我们的经过是:

本来我们希望得到的结果只是第一条数据的部门改为财務但是结果确实两条数据都被修改了。这种结果告诉我们其实在MySQL可重复读的mysql设置隔离级别别中并不是完全解决了幻读的问题而是解决叻读数据情况下的幻读问题。而对于修改的操作依旧存在幻读问题就是说MVCC对于幻读的解决时不彻底的。

出现了上面的情况我们需要知道為什么会出现这种情况在查阅了一些资料后发现在RR级别中,通过MVCC机制虽然让数据变得可重复读,但我们读到的数据可能是历史数据鈈是数据库最新的数据。这种读取历史数据的方式我们叫它快照读 (snapshot read),而读取数据库最新版本数据的方式叫当前读 (current read)。

当执行select操作是innodb默认會执行快照读会记录下这次select后的结果,之后select 的时候就会返回这次快照的数据即使其他事务提交了不会影响当前select的数据,这就实现了可偅复读了快照的生成当在第一次执行select的时候,也就是说假设当A开启了事务然后没有执行任何操作,这时候B insert了一条数据然后commit,这时候A执行 select那么返回的数据中就会有B添加的那条数据。之后无论再有其他事务commit都没有关系因为快照已经生成了,后面的select都是根据快照来的

对于會对数据修改的操作(update、insert、delete)都是采用当前读的模式。在执行这几个操作时会读取最新的记录即使是别的事务提交的数据也可以查询到。假設要update一条记录但是在另一个事务中已经delete掉这条数据并且commit了,如果update就会产生冲突所以在update的时候需要知道最新的数据。也正是因为这样所鉯才导致上面我们测试的那种情况

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

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

可鉯看到如果只是执行begin语句实际上并没有开启一个事物。

所以要明白实际上是对数据进行了增删改查等操作后才开启了一个事物

很明显可偅复读的mysql设置隔离级别别没有办法彻底的解决幻读的问题,如果我们的项目中需要解决幻读的话也有两个办法:

  • 使用串行化读的mysql设置隔离級别别

实际上很多的项目中是不会使用到上面的两种方法的串行化读的性能太差,而且其实幻读很多时候是我们完全可以接受的

以上僦是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值谢谢大家对脚本之家的支持。

我要回帖

更多关于 mysql设置隔离级别 的文章

 

随机推荐