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

脏读和幻读不好理解!

时间:2023-03-18 12:12:27 科技观察

本文转载自微信公众号《味觉小姐姐》,作者02号狗,转载请联系味觉小姐姐公众号。脏读、幻读、不可重复读、当前读、快照读,这些名词常常让人头晕目眩。因为一般人的大脑主线是单线程的,无法同时处理多个事务。要深刻记住,我们必须举几个例子。看完这篇文章,你会豁然开朗,不由得走上走下。但在此之前,我们需要看看当前的数据库隔离级别是多少。比如MySQL。select@@tx_isolation;MySQL包含4个隔离级别,当然隔离的是数据。要修改隔离级别,可以使用以下SQL语句。setsessiontransactionisolationlevelreaduncommitted;setsessiontransactionisolationlevelreadcommitted;setsessiontransactionisolationlevelrepeatableread;setsessiontransactionisolationlevelserializable;ok,我们来创建一个小测试表,看看在并发环境下的神奇效果。创建表`xjjdog_tx`(`id`INT(11)NOTNULL,`name`VARCHAR(50)NOTNULLCOLLATE'utf8_general_ci',`money`BIGINT(20)NOTNULLDEFAULT'0',PRIMARYKEY(`id`)USINGBTREE)COLLATE='utf8_general_ci'ENGINE=InnoDB;INSERTINTO`xjjdog_tx`(`id`,`name`,`money`)VALUES(2,'xjjdog1',100);INSERTINTO`xjjdog_tx`(`id`,`name`,`money`)VALUES(1,'xjjdog0',100);1.脏读脏读就是读取脏数据。什么是脏数据?它是另一个事务尚未提交的数据。在readuncommitted隔离级别下,会出现脏读。比如下面这个时序事务A:setsessiontransactionisolationlevelreaduncommitted;事务B:setsessiontransactionisolationlevelreaduncommitted;事务A:STARTTRANSACTION;事务B:STARTTRANSACTION;事务A:UPDATExjjdog_txSETmoney=money+100WHERENAME='xjjdog0';事务B:UPDATExjjdog_txSETmoney=money+100WHERENAME='xjjdog0';交易A:回滚;交易B:COMMIT;事务B:SELECT*FROMxjjdog_tx;在这个场景中,钱的原值是100,分别在两个session中进行加100的操作,然后其中一个session回滚事务。结果查询后发现,money的值还是100不变,也就是其中一个加100的操作被覆盖了。所以脏读的发生有几个条件。高并发场景下,一个事务A开始后,结束前,另一个事务参与事务A涉及的数据行读写事务隔离级别,事务隔离级别是最低的readuncommitted。你用完数据后,事务A返回Roll,你之前拿到的数据已经没有解了。你只需要将隔离级别设置为高于readuncommitted。2.不可重复读设置隔离级别为readcommitted可以避免脏读,这个其实很好理解。脏读的根本原因是事务执行过程中有其他操作干扰。这种隔离级别要求事务A提交后,修改后的值可以被事务B读取到,所以脏读是不可能的。基本上就此结束。但是readcommitted会造成不可重复读。顾名思义,在一个事务周期内,读取一个值会产生两个结果。不可重复的阅读证明世界并不总是围绕着你转。在你的交易执行过程中,还会有无数其他交易在执行,如果你的交易比这些交易持续的时间更长,那么你可能会读到两个或更多的值。让我告诉你一个故事。从前,有一棵桃树,结了十二个桃子。有一只猴子叫xjjdog,它想吃身上的桃子,可是桃子还没有熟。第二天去看的时候发现少了一个桃子,一共有11个桃子。经过仔细询问,原来是猴子A先吃了其中一个桃子。第二天去看的时候,桃子少了一个,变成了10个,原来一个被贪吃的猴子B吃了。就这样,桃子的数量一天比一天少,只有只剩下最后两个了,桃子还没有熟。桃子不摘,你就没了。xjjdog摘了最后2个桃子正要吃,结果一只猴子X跳出来说我已经盯着这些桃子看了1年了……在这个故事里,猴子A,Bxjjdog的交易时长是1天;xjjdog的交易时间是到桃子成熟为止;猴子X的持续时间更长,可能是一年。他们并不总是一天能看到12个桃子。今天的桃子可能被其他猴子吃了(交易),导致观察结果不同。这就是不可重复读的概念。有时候,即使读到的值一样,也不能证明没有问题。例如,某财务公司挪用2亿元炒股,月底又归还2亿元。虽然最终金额是一样的,但是由于你的对账周期长,你是不会发现这个差异的。如何解决不可重复读?首先,让我们看看不可重复读取是否是一个问题。一些系统需要这样的逻辑。每次在事务中读取不同的值,这是可以容忍的。但是如果你想让桃子成熟之前的数量在你的控制之下,那么不可重复读就是一个问题。一个很好的办法就是xjjdog一直站在桃树下。当其他猴子要摘桃子时,赶走它们。这种方法可行,但是在数据库中效率很低,属于可序列化级别的做法。MySQL有一个默认的事务隔离级别叫repeatableread,它使用的是MVCC方式(innodb),比较轻量级。3.可重复读这是MVCC(Multi-VersionConcurrencyControl)的功劳,它具有三个特点。每一行数据都有一个版本,每次更新数据都会更新版本。修改的时候复制一份,随意修改当前版本,在事务之间没有干扰的情况下比较版本号。如果提交成功,原始记录将被覆盖。如果失败了,rollbackMVCCInnoDB中的实现主要是为了提高数据库的并发性能,更好的处理读写冲突,这样即使有读写冲突的时候,也可以实现非锁定和非阻塞并发读取。它的实现也有3个关键技术:3个隐式字段:DB_TRX_ID,最近修改它的交易ID;DB_ROLL_PTR,回滚指针,指向上一个版本;为这条记录读视图生成版本变更链表:快照读操作时生成的读视图。除了使用上述附加信息外,它还会维护一个活动事务ID集。一切的关键在于快照二字。例如,事务A对某条记录进行快照读取,在快照读取的瞬间,产生一个ReadView。此时事务B和C还没有提交,而事务D和E,在ReadView创建的那一刻,commit完成之前,那么这个ReadView是读不到B和C的修改的。可惜的是可重复读只能解决快照读的不可重复读,快照读的时机也会影响读的准确性。考虑以下两种情况。在以下情况下,读取500。事务A事务B开启事务开启事务快照读取(无影响)查询量为500快照读取查询量为500更新量为400提交事务选择快照读取量为500selectlockinsharemodecurrentreadamountis400以下情况阅读是400。事务A事务B开启事务开启事务快照读取(无影响)查询量为500更新量为400提交事务选择快照读取量为400选择锁在共享模式当前读取量为400(表格来自[SnailMann]的博客).4.幻读幻读,这个词本身就很迷幻。幻读将发生在RU、RC和RR级别。举个最简单的例子。让您选择记录是否存在,然后计划稍后插入。如果记录不存在,那么你执行插入操作,但是当你真正执行插入操作时,结果是错误的。记录已经存在。这是幻读。首先,确认当前的可重复读取级别。如果不是,请修改它。SELECT@@tx_isolation#setsessiontransactionisolationlevelrepeatableread让我们来看看这个超自然的过程。有5个步骤,我已经为你标记了。让我们一一介绍。事务A使用begin开始一个事务,然后查询id为3的记录,此时不存在。但是由于snapshotread为id为3的record打开了一个readview,所以这个事务从头到尾都无法读取id为3的record。很好,这就是我们需要的不可重复读。接下来事务B插入一条id为3的记录,提交成功。事务A此时也想插入这条记录,于是执行同样的插入操作,结果数据库报错,显示这条记录已经存在。事务A一头雾水,想看看这条记录是什么,但是再次执行select语句时,找不到这条记录。但是在其他交易中,可以看到这条记录,因为已经正确提交了,这就是幻读。5、如何解决幻读幻读是错误的吗?在大多数情况下,它是正确的,但错误有点奇怪。为了防止幻读,需要开启FORUPDATE等高强度锁,实际情况下很少用到。为什么上面的操作,insert可以报错,select却找不到数据?这就不得不提到数据库读取的两种模式:快照读取:普通的select操作是从read视图中读取数据,read可能是历史数据的当前读取:insert,update,delete,select..forupdate,这种操作总是读取最新的数据。对于当前读,你读到的行和行之间的间隙会被锁定,直到事务提交后才会释放,其他事务无法修改,所以不会出现不可重复读或幻读。所以insert能找到冲突,而normalselect不能。解决幻读需要加X锁。上面这种情况,可以在事务A中执行:SELECT*FROMxjjdog_txWHEREid=3FORUPDATE这样做的时候,即使id为3的记录不存在,也会创建一个锁(可能会根据是否存在在后台添加记录行X锁或下一键锁间隙x锁)。6.总结这里是一个简短的总结。脏读是指一个事务读取了另一个事务还没有提交的记录。回滚其他事务时会出现问题。不可重复读是指在同一个事务中,多次读取可能导致结果不一致。这是因为其他事务在事务执行过程中修改了这些记录。MySQL默认是可重复读的,但是会出现幻读。幻读是由快照读取和当前读取之间的差异引起的。解决幻读,需要加锁(X锁,Gap锁等),比如forupdate,直到事务结束,全部改为currentreads。自然是没有问题的。所谓最高级的serializable,无非是当前读。可想而知,它在高并发环境下是高效的。所以几乎没用。作者简介:品味小姐姐(xjjdog),一个不允许程序员走弯路的公众号。专注于基础架构和Linux。十年架构,每天百亿流量,与你探讨高并发世界,给你不一样的滋味。我的个人微信xjjdog0,欢迎加好友进一步交流。