让建站和SEO变得简单

让不懂建站的用户快速建站,让会建站的提高建站效率!

幻读为什么会被 MySQL 单独拎出来不休?

发布日期:2022-05-15 16:35    点击次数:195

本文转载自微信公众号「飞天小牛肉」,作家飞天小牛肉 。转载本文请关系飞天小牛肉公众号。

所谓幻读,即一个事务在前后两次查询并吞个限度的本领,后一次查询看到了前一次查询莫得看到的行,这个回复测度大伙儿依然背烂了,然则它具体有什么后果呢?为什么会被 MySQL 单独拎出来不休呢?MySQL 又是如何不休的呢?

面前读和快照读

在了解幻读以及 MySQL 是如何不休幻读这个问题前,咱们需要澄清,什么是面前读、什么是快照读。

快照读:读取快照中的数据,不需要进行加锁。看到快照这两个字,诸君详情马上就预料 MVCC 了,是这么,MVCC 作用于读取已提交和可叠加读(默许)这两个隔断级别,这俩隔断级别下的平凡 select 操作即是快照读 面前读:读取的是最新版块的数据, 何况对读取的记载加锁, 窒碍其他事务同期改变沟通记载,幸免出现安全问题。

除了读取已提交和可叠加读这俩隔断级别下的平凡 select 操作,其余操作都是面前读:

select...lock in share mode (分享读锁) select...for update update, delete, insert 

举个例子,来直觉感受下快照读和面前读,以可叠加读隔断级别为例,假定咱们目下有一张 user 表,有如下的数据:

开启两个事务:

事务 1 先来个快照读望望:

没格外,很通俗。

事务 2 修改 age = 99,然后提交:

你猜,这本领,事务 1 快照读出来的数据应该是若干?

是的,仍然是旧数据:

那事务 1 “面前读” 出来的数据详情是最新的了:

幻读到底有什么问题

所谓幻读,即一个事务在前后两次查询并吞个限度的本领,后一次查询看到了前一次查询莫得看到的行。

在可叠加读隔断级别下,平凡的查询是快照读,面前事务是不会看到别的事务插入的数据的。因此,幻读问题在 “面前读” 下才会出现。

也有好多的著评释用 MVCC 来不休 "快照读" 下的幻读问题,我以为没必要纠结吧,甚而可能口试官更可能欢畅听到 MVCC 的道理。

不外,话说雅致,莫得 MVCC 机制,哪来快照读这个东西?

那么,幻读到底有什么问题?它具体有什么后果呢?

未几谎话,径直上例子:

咱们有一张表 user(id, username, age),依然有两条数据 (1, "Jack", 20), (2, "Tom", 18)

有如下三个事务:

尽人皆知,select for update 语句会加行锁,假定,戒备这里是假定奥!!!假定事务 1 的 select * from user where name = 'Jack' for update 只在 id = 1 的这一转上加行锁

不错看到,事务 1 实行了三次查询,都是要查出 name = "Jack" 的记载行。戒备咱们假定这里只在 name = 'Jack' 行上加行锁

第一次查询只复返了 id = 1 这一转 在第二次查询之前,事务 2 把 id = 2 这一转的 name 值改成了 "Jack",因此事务 1 第二次查询的杀青是 id = 1 和 id = 2 这两行 在第三次查询之前,事务 3 插入了一个 name = "Jack" 的新数据,因此事务 1 第三次查询的杀青是 id = 1、id = 2 以及 id = 3 这三行

显豁,第三次查询读到的 id = 3 这一转的局面,即是幻读

但其实从逻辑上来说,这似乎是莫得问题的。

因为这三个查询都是加了 for update,都是面前读。而面前读的法例,即是要能读到所有依然提交的记载的最新值,是以第二次查询和第三次查询即是应该看到事务 2 和事务 3 的操作成果。

那么,幻读到底有啥问题?

领先是语义上的。事务 1 在第一次查询的本领就声明了,我要把所有 name = "Jack" 的行锁住,拒却别的事务对 name = "Jack" 的行进行读写操作。

然则,履行上,这个语义被碎裂了,举个例子,我再旧事务 2 里加一条 SQL 语句(黄色框框):

事务 2 的第二条语句的道理是 "把 id = 2 这一转的 age 值改成了 40",这行的 name 值是 "Jack"。

而在这之前,事务 1 仅仅给 id = 5 的这一转加了行锁,并莫得给 id = 2 这行加锁。是以,事务 2 是不错实行这条 update 语句的。

这么,事务 2 先将 id = 2 的 name 改为 Jack,然后再将 age 改为 40,碎裂了事务 1 对要把所有 "name = Jack 的行锁住" 的声明

其次,最进犯的是,是数据一致性的问题。

尽人皆知,加锁是为了保证数据的一致性,这个一致性,不仅包括数据的一致性,还包括数据和日记的一致性,举个例子:

给事务 1 再加上一条 SQL 语句(黄色框框)

我在上图中圈出了四个时刻, T1 T2 T3 和 T4,咱们来分析下经过这四个时刻的数据库现象:

经过 T1 时刻,id = 1 这一转形成 (1, Tom, 20),戒备这是在 T4 才负责提交的 经过 T2 时刻,id = 2 这一转形成 (2, Jack, 40) 经过 T3 时刻,表内部多了一转 (3, Jack, 30)

再来望望这本领 binlog 日记内部的内容,binlog 即是记载下咱们做了哪些操作嘛:

T2 时刻,事务 2 提交,写入了 2 条 update 语句;

update user set name = "Jack" where id = 2 update user set age = "40" where id = 2 /*(2, Jack, 40)*/ 

T3 时刻,事务 3 提交,写入了 1 条语句; 

insert into user values(3, "Jack", 30) /*(3, Jack, 30)*/ 

T4 时刻,事务 1 提交,binlog 中写入了 update user set name = "Tom" where name = "Jack" 这条语句

update user set name = "Tom" where name = "Jack" 

即是说,把所有 name = Jack 的行,都给我改成 name = "Tom"

这么,问题就来喽,binlog 一般都是用于备库同步主库的对吧,这个 binlog 一实行,那岂不是原先 (2, Jack, 40) 和 (3, Jack, 30) 这两行的 name 完全形成了 Tom。

也即是说,id = 2 和 id = 3 这两行,发生了数据不一致。

戒备!这个数据不一致到底是如何发生的?是假定事务 1 的 select * from user where name = 'Jack' for update 只在 id = 1 的这一转上加行锁导致的。

很显豁,分析到这里,咱们依然解析,只锁这一转是差异理的。那好办,让 select for update 把所有扫描到的行都给锁住不就行了?

这么,事务 2 在 T2 时刻就会被窒碍住,直到事务 1 在 T4 时刻 commit 开释锁。

由于 session A 把所有的行都加了写锁,是以 session B 在实行第一个 update 语句的本领就被锁住了。需要比及 T6 时刻 session A 提交以后,session B 才气链接实行。

But,这么看似没问题,是否确切没问题呢?

来看 binlog,实行序列是这么的:

事务 3: insert into user values(3, "Jack", 30) /*(3, Jack, 30)*/  事务 1: update user set name = "Tom" where name = "Jack"  事务 2: update user set name = "Jack" where id = 2 update user set age = "40" where id = 2 /*(2, Jack, 40)*/ 

不错看到,事务 2 的问题如实是不休了,Jack 保住了,仍然是 (2, Jack, 40)

然则!!!戒备事务 3,在数据库内部的杀青是 (3, "Jack", 30),而凭据 binlog 的实行杀青是 (3, Tom, 30),也即是说幻读的问题照旧莫得不休。

那为什么咱们依然把所有粗略扫描到的记载都加上了锁,照旧封闭不了 id = 3 这一转的插入和更新呢?

很浅显。在咱们给所有行加锁的本领,id = 3 这一转还不存在,数据库扫描不到,也就诚然加不上锁了。

这亦然为什么幻读问题会被单独拿出来不休的原因,即使咱们把所有的的记载都加上锁,照旧封闭不了新插入的记载。

MySQL 如何不休幻读

目下你澄清了,产生幻读的原因是,行锁只可锁住行,然则新插入记载这个行动,操作的是锁住的行之间的 “症结”。因此,为了不休幻读问题,InnoDB 只好引入新的锁,也即是症结锁 (Gap Lock)。

这么,当你实行 select * from user where name = 'Jack' for update 的本领,就不啻是给数据库中已有的 n 个记载加上了行锁,还同期加了 n + 1 个症结锁(这两个合起来也成为 Next-Key Lock 临键锁)。也即是说,在数据库一转行扫描的流程中,不仅扫描到的行加上了行锁,还给行双方的闲逸也加上了锁。这么就确保了无法再插入新的记载。

这里多提一嘴,update、delete 语句用不上索引是很恐怖的。

对非索引字段进行 select .. for update、update 或者 delete 操作,由于莫得索引,走全表查询,就会对所有行记载 以及 所有间隔 都进行上锁。而关于索引字段进行上述操作,只好索引字段本人和隔邻的间隔会被加锁。

总结下 MySQL 不休幻读的妙技:

隔断级别:可叠加读

快照读 MVCC + 面前读 Next-Lock Key(只在可叠加读隔断级别下见效)

隔断级别:SERIALIZABLE

在这个隔断级别下,事务在读操作时,先加表级别的分享锁,直到事务杀青才开释;事务在写操作时,先加表级别的排它锁,直到事务杀青才开释。也即是说,串行化锁定了整张表,幻读不存在的

终末放上这道题的背诵版:

口试官:幻读有什么问题,MySQL 是如何不休幻读的

小牛肉:幻读即是一个事务在前后两次查询并吞个限度的本领,后一次查询看到了前一次查询莫得看到的行。

幻读的后果即是数据库中的数据和 binlog 的实行杀青会不一致,其原因就在于,咱们无法封闭新插入的数据。即是说,咱们在给扫描到的行加锁的本领,你等会要插入的行还不存在,也就没法对他进行加锁,那么这个新插入的数据,可能在主库中是这个表情,从库实行完 binlog 后其实是会被修改的。

这也即是为啥幻读会被单独拎出来不休的原因了。

幻读问题在 "面前读" 下才会出现。

所谓面前读即是,读取的是最新版块的数据, 何况对读取的记载加锁, 窒碍其他事务同期改变沟通记载,幸免出现安全问题。

与之对应的,快照读,读取的是快照中的数据,不需要进行加锁。读取已提交和可叠加读这俩隔断级别下的平凡 select 操作即是快照读。其实即是 MVCC 机制,或者说,在快照读下,给与 MVCC 机制不休幻读。

然后,关于面前读这种情况,前边咱们说,由于无法封闭新插入的数据,是以无法不休幻读问题,是以,咱们接头,不仅对扫描到的行进行加锁,还对行之间的症结进行加锁,这么就能根绝新数据的插入和更新。这个其实即是记载锁 Record Lock 和症结锁 Gap Lock,也被称为临键锁 Next-Lock Key。

 额临键锁只在可叠加读也即是 InnoDB 的默许隔断级别下见效。也不错给与更高的可串行化隔断级别,所有的操作都是串行实行的,不错径直根绝幻读问题。

 



热点资讯

会·评 | 政府工作报告透露哪些投资机会?券商

中新经纬3月7日电 2022年全国两会正在召开中。3月5日,第十三届全国人民代表大会第五次会议在人民大会堂举行开幕会,国务院总理李克强作政府工作报告。其中透露出A股哪些投资机会? 中信...

相关资讯