当前位置:首页 >时尚 >脏读、幻读,要想搞懂不容易! 并不能一次性处理多个事务

脏读、幻读,要想搞懂不容易! 并不能一次性处理多个事务

2024-06-28 11:44:54 [百科] 来源:避面尹邢网

脏读、脏读幻读,幻读要想搞懂不容易!

作者:小姐姐养的想搞狗02号 数据库 MySQL 脏读、幻读、容易不可重复读、脏读当前读、幻读快照读,想搞这些名词经常搞的容易让人头晕。因为一般人大脑的脏读主线就是单线程的,并不能一次性处理多个事务。幻读

[[394503]]

本文转载自微信公众号「小姐姐味道」,想搞作者小姐姐养的容易狗02号。转载本文请联系小姐姐味道公众号。脏读   

脏读、幻读,要想搞懂不容易! 并不能一次性处理多个事务

脏读、幻读幻读、想搞不可重复读、当前读、快照读,这些名词经常搞的让人头晕。因为一般人大脑的主线就是单线程的,并不能一次性处理多个事务。

脏读、幻读,要想搞懂不容易! 并不能一次性处理多个事务

要想记忆深刻,我们得借助几个实例。读完本文,你一定会豁然开朗,忍不住三连走起。

脏读、幻读,要想搞懂不容易! 并不能一次性处理多个事务

但在这之前,我们需要看一下当前的数据库隔离级别,到底是什么。比如MySQL。

  1. select @@tx_isolation; 

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

  1. set session transaction isolation level read uncommitted; 
  2. set session transaction isolation level read committed; 
  3. set session transaction isolation level repeatable read; 
  4. set session transaction isolation level serializable; 

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

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

1. 脏读

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

  1. 事务 A:set session transaction isolation level read uncommitted; 
  2. 事务 B:set session transaction isolation level read uncommitted; 
  3. 事务 A:START TRANSACTION ; 
  4. 事务 B:START TRANSACTION ; 
  5. 事务 A:UPDATE xjjdog_tx SET money=money+100 WHERE NAME='xjjdog0'; 
  6. 事务 B:UPDATE xjjdog_tx SET money=money+100 WHERE NAME='xjjdog0'; 
  7. 事务 A:ROLLBACK ; 
  8. 事务 B:COMMIT ; 
  9. 事务 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. 3个隐式字段:DB_TRX_ID,最近修改它的事务ID;DB_ROLL_PTR,回滚指针,指向上一个版本;DB_ROW_ID,隐藏主键
  2. undo日志:的对同一记录的修改,会生成针对此记录的版本变更链表
  3. 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一条记录是否存在然后打算进行后续插入时,如果这条记录不存在,然后你执行了插入操作,但在实际执行插入操作的时候,结果却报错了,这条记录已经存在了,这就是幻读。

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

  1. SELECT @@tx_isolation 
  2. # set session transaction isolation level repeatable read 

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

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

  1. 事务A使用begin开启一个事务,然后查询id为3的记录,此时不存在。但由于快照读开启了一个针对于id为3的记录的read view,所以在这个事务自始至终都不能够读到为3的记录。很好,这就是我们不可重复读所需要的
  2. 接下来,事务B插入了一条id为3的记录,并提交成功
  3. 事务A此时也想插入这条记录,于是执行了相同的插入操作,结果数据库报错,显示这条记录已经存在
  4. 事务A此时一脸懵逼,想看一下这条记录到底是啥,但当它再次执行select语句的时候,却查不到这条记录
  5. 但在其他事务中,是可以看到这条记录的,因为它已经正确提交

这就是幻读。

5. 如何解决幻读

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

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

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

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

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

  1. SELECT * FROM xjjdog_tx WHERE id=3 FOR UPDATE 

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

6. 总结

下面简单总结一下。

脏读,就是一个事务读取到另一个事务还没有提交的记录。当其他事务发生回滚的时候,就会出现问题。

不可重复读,意思是在同一个事务里,读多次可能会获得不一致的结果。这是因为在事务执行期间,有别的事务修改了这些记录。

MySQL默认是可重复读,但会发生幻读的情况。幻读是由于快照读和当前读的差别产生的。

要想解决幻读,就需要加锁(X锁,Gap锁等),比如for update,全部改成当前读直到事务结束,自然没有问题。

所谓的最高级别serializable,不过是全部搞成了当前读而已,在高并发的环境下效率,可想而知。所以几乎没有用的。

作者简介:小姐姐味道 (xjjdog),一个不允许程序员走弯路的公众号。聚焦基础架构和Linux。十年架构,日百亿流量,与你探讨高并发世界,给你不一样的味道。我的个人微信xjjdog0,欢迎添加好友,进一步交流。

 

责任编辑:武晓燕 来源: 小姐姐味道 脏读MySQL幻读

(责任编辑:综合)

    推荐文章
    热点阅读