什么是脏读与幻读

这篇文章主要讲解了“什么是脏读与幻读”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“什么是脏读与幻读”吧!

什么是脏读与幻读

select @@tx_isolation;

什么是脏读与幻读

MySQL就包含4种隔离级别,隔离的当然是数据。要修改隔离级别的话,可以使用下面的SQL语句。

set session transaction isolation level read uncommitted; set session transaction isolation level read committed; set session transaction isolation level repeatable read; set session transaction isolation level serializable;

ok,我们创建一张小小的测试表,来看一下并发环境下的魔幻效果。

CREATE TABLE `xjjdog_tx` (  `id` INT(11) NOT NULL,  `name` VARCHAR(50) NOT NULL COLLATE 'utf8_general_ci',  `money` BIGINT(20) NOT NULL DEFAULT '0',  PRIMARY KEY (`id`) USING BTREE ) COLLATE='utf8_general_ci' ENGINE=InnoDB ; INSERT INTO `xjjdog_tx` (`id`, `name`, `money`) VALUES (2, 'xjjdog1', 100); INSERT INTO `xjjdog_tx` (`id`, `name`, `money`) VALUES (1, 'xjjdog0', 100);

1. 脏读

脏读,意思就是读出了脏数据。啥叫脏数据?就是另外一个事务还没有提交的数据。在read  uncommitted隔离级别下,就会出现脏读。比如下面这个时序

事务 A:set session transaction isolation level read uncommitted; 事务 B:set session transaction isolation level read uncommitted; 事务 A:START TRANSACTION ; 事务 B:START TRANSACTION ; 事务 A:UPDATE xjjdog_tx SET money=money+100 WHERE NAME='xjjdog0'; 事务 B:UPDATE xjjdog_tx SET money=money+100 WHERE NAME='xjjdog0'; 事务 A:ROLLBACK ; 事务 B:COMMIT ; 事务 B:SELECT * FROM xjjdog_tx ;

在这个场景下,money的原始值为100,分别在两个session中进行了加100的操作,然后回滚了其中的一个session事务。结果,经过查询,发现money的值保持100不变。也就是其中一次加100的操作被覆盖掉了。

什么是脏读与幻读

所以脏读发生有几个条件。

  • 高并发场景,在一个事务A开始之后还没结束之前,有另外一个事务参与了事务A所涉及的数据行读写

  • 事务隔离级别处于最低的读未提交

  • 在你使用到这些数据之后,事务A回滚,造成你之前拿到的数据已经不再存在

解决方式,只需要设置成隔离级别比read uncommitted高即可。

2. 不可重复读

把隔离级别设置成read  committed即可避免脏读,这其实非常好理解。脏读产生的根本原因就是在事务的执行期间有别的操作乱入,这个隔离级别要求事务A提交之后,修改后的值,才能被事务B读到,所以脏读是不可能会发生的,从根本上杜绝了。

但read commited会发生不可重复读的情况。

顾名思义,就是在一个事务周期内,对于一个值的读取,产生了两个结果。

不可重复读,证明了世界并不是总围绕着你转的。在你的事务执行期间,会有无数的其他事务执行,如果你的事务持续时间超过了这些事务,那么你就可能读到两个或者更多的值。

让我来给你讲一个故事。

从前,有一颗桃树,长了12棵桃子。有一只猴子,叫做xjjdog,它想吃上面的桃子,但桃子还不熟。

第二天去看的时候,它发现桃子少了一个,变成了11个,经过仔细打听,原来是被猴子A抢先吃掉一个。

第二天去看的时候,桃子又少了一个,变成了10个,原来是被馋嘴的猴子B吃掉一个。

如此这般,桃子一天天少了下去,只剩下最后的2个了,但桃子还是没熟。

再不摘桃子就没了,xjjdog摘下了最后的2个桃子,正打算大快朵颐,结果跳出一只猴子X,说我盯着这些桃子已经1年了...

在这故事中,猴子A、B的事务持续周期是1天;xjjdog的事务持续周期是直到桃子成熟;猴子X的持续周期更长,可能是一年。它们每天看到的桃子,并不总是12个。今天的桃子,可能被其他的猴子(事务)给吃掉了,造成了观测的结果是不一样的,这就是不可重复读的概念。

有时候,即使读到的值是一样的,也不能证明没问题。比如有财务挪用了2亿去炒股,然后在月底把2亿还了回来,虽然最终的金额都是一致的,但由于你的对账周期长,就发现不了这种差异。

如何解决不可重复读呢?先要看一下不可重复读是不是问题。

有的系统,要求的就是这样的逻辑,每次在事务中读取到不一样的值,它是可以忍受的。但如果你想要在桃子成熟之前,桃子的数量都在你的掌控之中,那不可重复读就是一种问题。

一种非常好的方式,就是xjjdog一直站在桃树地下。当有别的猴子想要摘桃,就把它赶走。这种方式可行,但在数据库中非常低效,这是serializable级别的做法。

MySQL有一个默认的事务隔离级别,叫做repeatable read,使用了MVCC的方式(innodb),要更轻量级一些。

3. 可重复读

这就是MVCC(Multi-Version Concurrency Control)的功劳了,它有三个特点。

每行数据都存在一个版本,每次数据更新时都更新该版本

修改时,拷贝一份,当前版本随意修改,事务之间无干扰

保存时比较版本号,如果成功commit覆盖原记录,失败则rollback

MVCC在InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。它的实现关键也有三项技术:

  1. 鸿蒙官方战略合作共建——HarmonyOS技术社区

  2. 3个隐式字段:DB_TRX_ID,最近修改它的事务ID;DB_ROLL_PTR,回滚指针,指向上一个版本;DB_ROW_ID,隐藏主键

  3. undo日志:的对同一记录的修改,会生成针对此记录的版本变更链表

  4. read view:快照读操作的时候,产生的读视图。除了使用上面的额外信息,它也会维护一个活跃的事务ID集合

一切的关键,就在于快照这两个字上面。

比如事务A对某个记录进行了快照读,那么在快照读的这一刻,就生成了一个Read  View。在这一刻,事务B和C,还没有commit,事务D和E,在建立ReadView那一刻之前,commit完成,那么这个Read  View,就不能够读到B和C的修改。

但可惜的是,可重复读,只能解决快照读的不可重复读,快照读的时机,也会影响读取的准确程度。请看下面两种情况。

下面这种情况读到的是500。

事务A 事务B
开启事务 开启事务
快照读(无影响)查询金额为500 快照读查询金额为500
更新金额为400  
提交事务  
  select 快照读金额为500
  select lock in share mode当前读金额为400

下面这种情况读到的是400。

事务A 事务B
开启事务 开启事务
快照读(无影响)查询金额为500  
更新金额为400  
提交事务  
  select 快照读金额为400
  select lock in share mode当前读金额为400
 

(表格来自[SnailMann]的博客)。

4. 幻读

幻读,这个词本身就非常的迷幻。在RU、RC、RR级别下,都会出现幻读。

拿一个最简单的例子来说。让你select一条记录是否存在然后打算进行后续插入时,如果这条记录不存在,然后你执行了插入操作,但在实际执行插入操作的时候,结果却报错了,这条记录已经存在了,这就是幻读。

首先,确认目前时可重复读级别。如果不是,则修改之。

SELECT @@tx_isolation # set session transaction isolation level repeatable read

让我们来看一下这个灵异过程。

什么是脏读与幻读

有5个步骤,我都给你标好了。下面一一介绍。

  1. 鸿蒙官方战略合作共建——HarmonyOS技术社区

  2. 事务A使用begin开启一个事务,然后查询id为3的记录,此时不存在。但由于快照读开启了一个针对于id为3的记录的read  view,所以在这个事务自始至终都不能够读到为3的记录。很好,这就是我们不可重复读所需要的

  3. 接下来,事务B插入了一条id为3的记录,并提交成功

  4. 事务A此时也想插入这条记录,于是执行了相同的插入操作,结果数据库报错,显示这条记录已经存在

  5. 事务A此时一脸懵逼,想看一下这条记录到底是啥,但当它再次执行select语句的时候,却查不到这条记录

  6. 但在其他事务中,是可以看到这条记录的,因为它已经正确提交

这就是幻读。

5. 如何解决幻读

幻读有错么?多数情况下没错,就是报错怪异了些。要防止幻读,需要开启FOR UPDATE这样高强度的锁定,实际情况是非常少用。

为什么上面的操作,insert能报错,但select却无法查到数据呢?这就不得不提一下数据库读的两种模式:

快照读:普通的select操作,是从read view中读取数据,读取的可能是历史数据

当前读:insert、update、delete、select..for update这种操作,读取的总是当前的最新数据

对于当前读,你读取的行,以及行的间隙都会被加锁,直到事务提交时才会释放,其他的事务无法进行修改,所以也不会出现不可重复读、幻读的情形。所以insert能够发现冲突,而普通select却不可以。要想解决幻读,就需要加X锁。在上面这种情况,就可以在事务A中执行:

SELECT * FROM xjjdog_tx WHERE id=3 FOR UPDATE

当这么做的时候,即使id为3的记录不存在,它也会创建锁(在背后可能根据记录的存在与否加行X锁或者next-key lock间隙x锁)。

感谢各位的阅读,以上就是“什么是脏读与幻读”的内容了,经过本文的学习后,相信大家对什么是脏读与幻读这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是亿速云,小编将为大家推送更多相关知识点的文章,欢迎关注!