当前位置: 首页 > 科技观察

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

时间:2023-03-21 22:13:05 科技观察

简介在深入了解数据库理论的时候,我了解了不同隔离级别的事务可能存在的问题。为了更好地理解,在MySQL数据库中测试和重现这些问题。脏读和不可重复读在相应的隔离级别下很容易重现。但是对于幻读,我发现在repeatableread的隔离级别下并没有出现。当时我就想,MySQL到底对幻读做了什么?测试:创建测试表dept:CREATETABLE`dept`(`id`int(11)NOTNULLAUTO_INCREMENT,`name`varchar(20)DEFAULTNULL,PRIMARYKEY(`id`))ENGINE=InnoDBAUTO_INCREMENT=12DEFAULTCHARSET=utf8insertintodept(name)values(“后勤部”)按上述流程执行。预计事务1的第一次select会查询到一条数据,第二次select会查询到两条数据(包括事务2提交的数据)。但是在实际测试中发现,第二个select实际上只查询和处理了一条数据。这是但是按照数据库理论中repeatableread的实现(独占锁和共享锁)应该不是这样的。在了解实际原因之前,我们先来回顾一下交易的相关理论。数据库原理理论事务(Transaction)一般是指要做或要做的事情。在计算机术语中,它是指访问并可能更新数据库中各种数据项的程序执行单元(单元)。事务由开始事务和结束事务之间执行的所有操作组成。在关系数据库中,事务可以是一组SQL语句或整个程序。为什么使用事务数据库事务通常由一系列读取或写入数据库的操作组成。它的存在有以下两个目的:为数据库操作提供了一种从故障中恢复到正常状态的方法,同时为数据库在异常状态下保持一致性提供了一种方法。当多个应用程序并发访问数据库时,可以在这些应用程序之间提供一种隔离的方法来保证彼此的操作不会相互干扰。事务特性事务有四个特性:原子性、一致性、隔离性和持久性。这四种特性通常被称为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加任何锁。即读写锁之间的关系可以概括为:多读单写事务的隔离级别,在事务中有以下隔离级别:读未提交(ReadUncommitted)解决了丢失更新的问题。如果一个事务已经开始写操作,则不允许其他事务同时写,但允许其他事务读取这一行数据。这种隔离级别可以通过“独占写锁”来实现,即事务需要修改一些数据必须对这些数据加X锁,读数据不需要加S锁。ReadCommitted(已提交读)解决了脏读问题。读取数据的事务允许其他事务继续访问数据行,但未提交的写事务将阻止其他事务访问该行。这可以通过“瞬时共享读锁”和“独占写锁”来实现,即事务需要修改一些数据,必须对这些数据加X锁。读取数据时,需要加S锁。当数据读取完成后,不等待事务结束,立即释放S锁。可重复读(RepeatableRead)禁止不可重复读和脏读,但有时会出现幻像数据。读取数据的事务不允许写入事务(但允许读取事务),并且写入事务不允许任何其他事务。Mysql默认使用这个隔离级别。这可以通过“共享读锁”和“独占写锁”来实现,即事务需要修改某些数据必须对这些数据加X锁,读数据时需要加S锁。当数据读取完成后,并不会立即释放S锁,而是等到事务结束后再释放。Serializable解决了幻读问题。提供严格的事务隔离。它要求事务串行执行,事务只能一个接一个地执行,不能并发执行。仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制来保证新插入的数据不会被刚刚执行过查询操作的事务访问到。隔离级别在MySQL中的实现以上内容解释了一些数据库理论概念,但是在MySQL、ORACLE等数据库中,出于性能的考虑,并没有完全按照上面介绍的理论来实现。MVCC多版本并发控制(Multi-VersionConcurrencyControl,MVCC)是MySQL中基于乐观锁理论实现隔离级别的一种方式,用于实现读提交和可重复读隔离级别。实现(隔离级别是可重复读)在讲如何实现之前先介绍两个概念:系统版本号:一个递增的数字,每启动一个新的事务,系统版本号就会自动递增。交易版本号:交易开始时的系统版本号。在MySQL中,表中每条数据后面都加了两个字段:创建版本号:创建一行数据时,指定当前系统版本号作为创建版本号删除版本号:删除一行数据时,使用当前系统版本号SELECTselect作为删除版本号赋值时读取数据的规则是:创建版本号<=当前事务版本号,删除版本号为空或>当前事务版本号。创建版本号<=当前事务版本号保证抓取的数据不会有后面启动的事务中创建的数据。这就是为什么在最初的例子中我们不会发现后面添加的数据的原因。INSERT插入是将当前系统版本号赋给创建版本号字段。UPDATE插入一条新记录,将当前事务版本号保存为行创建版本号,将当前事务版本号保存到原删除行。其实这里的update是通过delete和insert实现的。当DELETE被删除时,当前系统版本号被分配给删除版本号字段,它标识该行数据将在该事务中被删除,即使数据在提交时实际上没有被删除。选择规则后打开的数据将不会被查询。MVCC真的能解决幻读吗?从我们一开始的测试例子和上面的理论支持来看,似乎MySQL中的MVCC已经解决了幻读的问题。由于这次连载阅读好像没什么意义,所以我们带着疑问继续测试。预测数据:根据上面的结果,我们的预期结果是:idname1财务部2研发部但实际上我们的经验是:我们希望得到的结果只是第一条数据的部门是改为Finance,结果两个数据都修改了。这个结果告诉我们,在MySQL可重复读隔离级别中,并没有完全解决幻读问题,而是解决了读取数据时的幻读问题。但是在修改操作中仍然存在幻读的问题,也就是说MVCC对于幻读的解决并不彻底。以上情况出现在snapshotread和currentread中。我们需要知道为什么会这样。查阅了一些资料,发现在RR层面,通过MVCC机制,虽然可以重复读取数据,但是我们读取的数据可能是历史数据,而不是数据库中的最新数据。这种读取历史数据的方式称为快照读取,读取最新版本数据库的方式称为当前读取。selectsnapshotread当执行一个select操作时,InnoDB默认会进行一次snapshotread,并会记录这次select的结果,然后select时返回这次snapshot的数据,即使提交了其他事务也不影响当前选中的数据,实现可重复读取。snapshot是在第一次执行select的时候产生的,也就是说,假设当A开始事务,然后没有进行任何操作,此时B插入了一条数据,然后commit,而此时此时A执行select,那么返回的数据中就会有B添加的那条数据,之后有没有其他事务commit都无所谓,因为snapshot已经生成了,后面的selection都是根据快照。当前读取使用当前读取模式进行修改数据的操作(更新、插入、删除)。执行这些操作时,会读取到最新的记录,甚至可以查询其他事务提交的数据。假设您要更新一条记录,但该数据已被删除并在另一个事务中提交。更新的话会有冲突,所以更新的时候需要知道最新的数据。也正是因为如此,才导致了我们上面测试的情况。select当前读需要手动加锁:select*fromtablewhere?lockinsharemode;select*fromtablewhere?forupdate;有一个问题说明,在测试的时候,一开始以为使用begin语句是为了启动一个事务,所以上面第二次在测试中,因为事务1先启动,所以在事务中发现了事务2的新数据1、当时认为这与之前MVCC中的select规则不一致,于是做了如下测试:SELECT*FROMinformation_schema.INNODB_TRX//用于查询当前正在执行的事务,可以看到如果你只是执行开始语句,你实际上并没有开始一个事务。接下来,在begin之后添加一条select语句:所以你要明白,在对数据进行增删改查之后,才真正开始了一个事务。如何解决幻读显然,可重复读的隔离级别是没有办法彻底解决幻读问题的。如果我们项目中需要解决幻读,有两种方式:使用序列化读的隔离级别MVCC+next-key锁:next-key锁由记录锁(indexlocks)和间隙锁(gaplocks,它不仅锁定了每次需要使用的数据,还锁定了这些数据附近的数据)。其实很多项目都是不会用到上面两种方式的,序列化读取的性能太差了,而实际上幻读在很多情况下我们是完全可以接受的。