20 幻读是什么,幻读有什么问题?

例子:

CREATE TABLE `t20` (

`id` int(11) NOT NULL,

`c` int(11) DEFAULT NULL,

`d` int(11) DEFAULT NULL,

PRIMARY KEY (`id`),

KEY `c` (`c`)

) ENGINE=InnoDB;

insert into t20 values(0,0,0),(5,5,5),

(10,10,10),(15,15,15),(20,20,20),(25,25,25);

select * from t20;

幻读是什么?


SESSION A


SESSION B


SESSION C


T1


begin;

select * from t20 where d=5 for update;/*Q1*/

result:(5,5,5)


T2


update t20 set d=5 where id=0;


T3


select * from t20 where d=5 for update;/*Q2*/

result:(0,0,5),(5,5,5)


T4


insert into t20

values(1,1,5);


T5


select * from t20 where d=5 for update;/*Q3*/

result:(0,0,5),(1,1,5),(5,5,5)


T6


commit;

                                    假设只在id=5这一行加行锁

可以看到,session A里执行了3次查询,分别是Q1,Q2,Q3,他们的sql语句相同,都是select * from t20 where d=5 for update。这个语句应该很清楚了,

查所有d=5的行,而且使用的是当前读,并且加上写锁。现在,我们来看一下这三sql语句,分别回返回什么结果。

  1. Q1只返回id=5这一行;
  2. 在T2时刻,session B把id=0这一行的d值改成了5,因此T3时刻Q2查出来的是id=0和id=5的这两行。
  3. 在T4时刻,session C又插入了一行(1,1,5),因此T5时刻Q3查询出的是id=0,id=1,id=5这三行。

其中,Q3读到id=1这一行的现象,被称为”幻读”,也就是说,幻读是指在同一个事务中,前后两个相同的查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行

这里,需要对”幻读”做一个说明

  1. 在rr隔离级别下,普通的查询是快照读,是不会看到别的事务插入的数据的,因此,幻读在当前读才会出现
  2. 上面session B的修改结果,被session A之后的select 语句用”当前读”看到,不能称为幻读,幻读仅专指新插入的行

根据事务的可见性规则来分析,上面的三个查询都加了for update,都是当前读。而根据当前读的规则,就是要能读到所有已经提交的记录的最新值。

并且session B和session C的两条语句,执行后就会提交,所以Q2和Q3看到就应该是两个事务的操作结果。

但是,这是不是真的没有问题呢?

不,这里是有问题的

幻读有什么问题?

首先语义上的,SESSION A在T1时刻就声明了,我要把所有d=5的行锁住,不准别的进行读写操作,而实际上,这个语义被破坏了。


SESSION A


SESSION B


SESSION C


T1


begin;

select * from t20 where d=5 for update;/*Q1*/


T2


update t20 set d=5 where id=0;

update t20 set c=5 where id=0;


T3


select * from t20 where d=5 for update;/*Q2*/


T4


insert into

t20 values(1,1,5);

update t20 set c=5

where id=1;


T5


select * from t20 where d=5 for update;/*Q3*/


T6


commit;

SESSION B的第二条语句update t20 set c=5 where id=0的语义是”我把id=0,d=5的这一会的c值,改成了5”

由于在T1时刻,session A还只是给id=5这一行加了行锁,并没有给id=0这行加上锁,因此session B在T2时刻,是可以执行这两条update的语句,这样,就破坏了session A的Q1语句要锁住所有d=5的行的加锁声明。

其次,是数据一致性的问题

我们知道,锁的设计是为了保证数据的一致性,而这个一致性,不止是数据库内部数据状态在此刻的一致性,还包含了数据和日志在逻辑上的一致性。

为了说明这个问题,在session A的T1时刻加上一个更新语句:


SESSION A


SESSION B


SESSION C


T1


begin;

select * from t20 where d=5 for update;/*Q1*/

update t20 set d=100 where d=5;


T2


update t20 set d=5 where id=0;

update t20 set c=5 where id=0;


T3


select * from t20 where d=5 for update;/*Q2*/


T4


insert into

t20 values(1,1,5);

update t20 set c=5

where id=1;


T5


select * from t20 where d=5 for update;/*Q3*/


T6


commit;

-update的加锁语义和select ... for update是一致的,所以这时候加上这条update语句很合理。Session A声明说”要给d=5的语句加锁锁”,就是为了要更新数据,新加的这条update就是把它认为加上了锁的这一行的d值修改成100.

现在,我们来分析一下图3执行完成后,数据库是什么结果

1 经过T1时刻,id=5这一行变成(5,5,100),当然这个结果最终在T6时刻正是提交的

2 经过T2时刻,id=0这一行变成(0,5,5)

3 经过T4时刻,表里面多了一行(1,5,5)

4 其他行跟这个执行序列无关,保持不变。

这时候看数据也没啥问题,我们看看这个时候的binlog内容。

1 T2时刻,session B事务提交了,写入了两条语句

2 T4时刻,session C事务提交了,写入了两条语句

3 T6时刻,session A事务提交,写入了update t set d=100 where d=5这条语句。放一起的话。

update t set d=5 where id=0; /*(0,0,5)*/

update t set c=5 where id=0; /*(0,5,5)*/

insert into t values(1,1,5); /*(1,1,5)*/

update t set c=5 where id=1; /*(1,5,5)*/

update t set d=100 where d=5;/* 所有 d=5 的行,d 改成 100*/

这个时候应该出问题了,这个语句序列,不论是拿到备库去执行,还是以后用binlog来恢复,这3行的结果,都变成了(0,5,100),(1,5,100)和(5,5,100)。

也就是id=0和id=1这两行,发生了数据不一致,这个问题很严重,是不行的。

到这里,在想一想,这个数据不一致是怎么引入的

分析知道,这是我们假设”select * from t20 where d=5 for update这条语句只给d=5这一行,也就是id=5的这一行加锁”导致的。

所以我们认为,上面的设计是不合理的。

那怎么改呢?我们把扫描过程中碰到的行,都加上写锁,再来看看执行效果


SESSION A


SESSION B


SESSION C


T1


begin;

select * from t20 where d=5 for update;/*Q1*/

update t20 set d=100 where d=5;


T2


update t20 set d=5 where id=0;

(blocked)

update t20 set c=5 where id=0;


T3


select * from t20 where d=5 for update;/*Q2*/


T4


insert into

t20 values(1,1,5);

update t20 set c=5

where id=1;


T5


select * from t20 where d=5 for update;/*Q3*/


T6


commit;

由于session A把所有的行都加了写锁,所以session B在执行第一个udpate语句的时候就被锁住了,需要等到T6时刻session A提交以后,session B才能继续执行。

在binlog里面,执行序列是这样的

insert into t values(1,1,5); /*(1,1,5)*/

update t set c=5 where id=1; /*(1,5,5)*/

update t set d=100 where d=5;/* 所有 d=5 的行,d 改成 100*/

update t set d=5 where id=0; /*(0,0,5)*/

update t set c=5 where id=0; /*(0,5,5)*/

可以看到,按照日志顺序执行,id=0这一行的最终结果是(0,5,5),所以,id=0这一行的问题解决了。

但同时你也可以看到,id=1这一行,在数据库里面的结果是(1,5,5),而根据binlog的执行结果是(1,5,100),也就说幻读的问题还没有解决。

原因很简单,在T3时刻,我们给所有行加锁的时候,id=1这一行还不存在,不存在也就加不上锁。

也就说,即使把所有的记录都加上锁,还是阻止不了新插入的记录,这也是为什么”幻读”会被单独拿出来解决的问题。

到这里,其实我们刚说明完文章的标题,幻读的定义和幻读有什么问题

在innodb,我们看是怎么解决幻读问题的

如何解决幻读?

现在知道,产生幻读的原因是,行锁只能锁住行,但是新插入记录这个动作,要更新的是记录之间的”间隙”,因此,为了解决幻读问题,innodb引入了新的锁,也即是间隙锁(gap lock)。

顾名思义,间隙锁,锁的就是两个值之间的空隙。

这样,当执行select * from t20 where d=5 for update的时候,就不止给数据库已有的6行记录加上了行锁,还同时加了7个间隙锁,这样就确保无法在插入新的记录。

也就是说这时候,在一行扫描的过程中,不仅将给行上加了行锁,还给行两边的空隙,也加上了间隙锁。

现在你知道了,数据行是可以加上锁的实体,数据行之间的间隙,也是可以加上锁的实体。但是间隙锁跟我们碰到过的锁都不太一样。

比如行锁,分读锁和写锁


读锁


写锁


读锁


兼容


冲突


写锁


冲突


冲突

也就说,跟行锁冲突关系的是”另外一个行锁”

但是间隙锁不一样,跟间隙锁存在冲突关系的,是往这个间隙中插入一个记录这个操作间隙锁之间是不存在冲突关系的。


SESSION A


SESSION B


begin;

select * from t20 where c=7 lock in share mode;


begin;

select * from t20 where c=7 for update;

这里session B并不会被锁住,因为表t20里并没有c=7这个记录,因此session A加的是间隙锁(5,10),而session B也是在这个间隙加的间隙锁,它们的共同目标,即:保护这个间隙,不允许插入值,但,它们之间是不冲突的

间隙锁和行锁合称为next- key lock,每个next-key lock是前开后闭区间。也就是说,我们的表t20初始化后,如果用select * from t20 for update,要把整个表所有记录锁起来,

就形成了7个next-key lock,分别是(-∞,0],(0,5],(5,10],(10,15],(15,20],(20,25],(25,+supernum].

间隙锁和next-key lock的引入,帮我们解决了幻读的问题,但同时也带来了一些困扰


SESSION A


SESSION B


begin;

select * from t20 where id=9 for update;


begin;

select * from t20 where id=9 for update;


insert into t20 values (9,9,9); (blocked)


insert into t20 values(9,9,9);

ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

 

LATEST DETECTED DEADLOCK
------------------------
2019-01-04 17:03:20 7f662466a700
*** (1) TRANSACTION:
TRANSACTION 5580608, ACTIVE 17 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 376, 2 row lock(s)
MySQL thread id 18034, OS thread handle 0x7f6623fd0700, query id 637155 127.0.0.1 system update
insert into t20 values (9,9,9)
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 1276 page no 3 n bits 80 index `PRIMARY` of table `test`.`t20` trx id 5580608 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 4 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
 0: len 4; hex 8000000a; asc     ;;
 1: len 6; hex 00000053c93a; asc    S :;;
 2: len 7; hex 6e000002620570; asc n   b p;;
 3: len 4; hex 8000000a; asc     ;;
 4: len 4; hex 8000000a; asc     ;;

*** (2) TRANSACTION:
TRANSACTION 5580596, ACTIVE 20 sec inserting
mysql tables in use 1, locked 1
3 lock struct(s), heap size 376, 2 row lock(s)
MySQL thread id 18046, OS thread handle 0x7f662466a700, query id 637171 127.0.0.1 system update
insert into t20 values (9,9,9)
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 1276 page no 3 n bits 80 index `PRIMARY` of table `test`.`t20` trx id 5580596 lock_mode X locks gap before rec
Record lock, heap no 4 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
 0: len 4; hex 8000000a; asc     ;;
 1: len 6; hex 00000053c93a; asc    S :;;
 2: len 7; hex 6e000002620570; asc n   b p;;
 3: len 4; hex 8000000a; asc     ;;
 4: len 4; hex 8000000a; asc     ;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 1276 page no 3 n bits 80 index `PRIMARY` of table `test`.`t20` trx id 5580596 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 4 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
 0: len 4; hex 8000000a; asc     ;;
 1: len 6; hex 00000053c93a; asc    S :;;
 2: len 7; hex 6e000002620570; asc n   b p;;
 3: len 4; hex 8000000a; asc     ;;
 4: len 4; hex 8000000a; asc     ;;

*** WE ROLL BACK TRANSACTION (2)
------------

死锁分析:

1 session A执行select ... for update,由于id=9这一行并不存在,因此会加上间隙锁(5,10)

2 session B 执行select... for update,同样会加上间隙锁(5,10),间隙锁之间并不冲突,因此这个会执行成功。

3 session B试图插入一行(9,9,9),被session A的间隙锁挡住了,只好进入等待

4 session A试图插入一行(9,9,9),被session B的间隙挡住了。

至此,两个session进入互相等待状态,形成了死锁,当然,innodb的死锁检测马上发现这对死锁关系,让session A的insert语句报错返回了。

现在知道,间隙锁的引入,可能会导致同样的语句锁住更大的范围,这其实是影响并发的,其实,这还只是一个简单的例子。下一篇会有更多的例子

小结

提到全表扫描的加锁方式,我们发现即使给所有行加上行锁,仍然无法解决幻读的问题,因此引入了间隙锁的概念。

思考题


SESSION A


SESSION B


SESSION C


begin;

select * from t20 where c>=15 and c<=20 order by c desc for update;


insert into t20

values(11,11,11);


insert into t20 values(6,6,6);

这里的session B和session C的insert语句会进入到什么状态

原文地址:https://www.cnblogs.com/yhq1314/p/10221206.html

时间: 2024-10-27 08:30:16

20 幻读是什么,幻读有什么问题?的相关文章

数据库事务隔离级别--读未提交,读已提交,重复读,序列化

参考原文:https://my.oschina.net/bigdataer/blog/1976010 上一篇文章讲述了:数据库主从复制,那么新的问题数据库读写分离对事物是否有影响? 1. 名词 读未提交read-uncommited 读已提交read-commited 重复读repeatable-->可能产生主从数据不一致问题 串行化serializable-->特殊场景-秒杀使用,一般不用 数据库事务的隔离级别有4个,由低到高依次为Read uncommitted.Read committe

回文指的是一个字符串从前面读和从后面读都一 样,编写一个算法判断一个字符串是否为回文。

回文指的是一个字符串从前面读和从后面读都一 样,编写一个算法判断一个字符串是否为回文. 要求: 1)采用链栈实现算法: 2)从键盘输入一个字符串,输出判断结果. #include"stdio.h" #include"stdlib.h" typedef char ElemType; typedef struct stnode { ElemType data; struct stnode *next; }StNode, *LinkStack; int huiwen(ch

我读经典(6):读《文明之光》有感

什么是文明?如果突然问大家这个问题,那么你可能会不知如何回答,感觉这是一个只可意会但不可言传的概念. 在新书<文明之光>中,吴军老师为我们带来了答案.继<浪潮之巅>和<数学之美>之后,吴军老师又出力作<文明之光>,将世界各地的主要文明为我们一一呈现和讲解.整套书现在出了第一册和第二册,加起来一共有16章.在这16章中,作者基本按照时间的顺序,从最古老的古埃及文明讲起,一直到美苏在航天方面的竞争为止,为大家展示了一幅生动有趣的历史画卷.全书文笔流畅.语言优美.

如何实现共享文件夹可读,文件不可读不可拷贝

如何实现共享文件夹,可以列出文件,而文件本身不可读,不可拷贝 问题:如何实现共享文件夹可读,文件不可读不可拷贝? 实验环境:win7 旗舰版.局域网 步骤及说明: 右击[右下角网络图标],进入"打开网络和共享中心",进入"更改高级共享设置",开启"启用密码保护共享"(说明:我启用该设置是为了访问共享时需要输入账户密码) 创建访问共享所需要的账户,右键"我的计算机"进入"管理",在"本地用户和组&q

我读经典(4):读《打造Facebook》有感

我第一次听说Facebook,是在用了国内的人人网很久之后.从上大学到研究生毕业,我几乎每天都要上人人网,发发日志.照片,看看其他同学有什么动向等等.个人感觉人人网使用起来非常的方便,拉近了人与人之间的距离,比QQ舒服多了. 某一天,我读到了一篇文章,主题是有关人人网的.文章里面说,人人网是中国的Facebook.这时,我便对Facebook产生了兴趣,接着上网去百度了一把,才知道它是全球最大的社交网络,于2004年2月4日正式上线,其主要创始人为美国哈佛大学的辍学生马克·扎克伯格(很有意思,比

数据库隔离级别,读已提交,读未提交

同样是后端开发,年薪50万和年薪20万的差距在哪里>>> 数据库事务的隔离级别有4个,由低到高依次为Read uncommitted.Read committed.Repeatable read.Serializable,这四个级别可以逐个解决脏读.不可重复读.幻读这几类问题. √: 可能出现    ×: 不会出现 事务的隔离级别 脏读 不可重复读 幻读 Read uncommitted √ √ √ Read committed--Sql Server , Oracle × √ √ Re

人生在世,不在于读多少,在于读什么?

有很多人在说中国人的阅读量很少,这话也对也不对.很多时候不是阅读量少,而是有质量的阅读不多.在竹简飘香的年代,能会有多少书?古代的老子.庄子.孔子读了多少书?可以说,在我们读完大学四年的时候,很多人的阅读量都超过了古代最牛的大学问家. 信息不是知识,知识不是智慧,选择越多也就越迷茫. 在纸张泛滥.信息充塞的年代,一个谎言的出现,需要几十万字去证明这个谎言的真假:一场婚礼的出现,纸媒报纸新媒体产生的文字垃圾超过了任何一部经典名著. 民国时期的文人,敢说能把唐以前的绝大部分书都读过,那是整整三千年的

我读经典(5):读《大话重构》迷你书有感

近期.我在一个QQ群里面看到有人在讨论一本书,叫做<大话重构>.在闲暇之余,我下载了该书的电子版,是一本迷你书,仅仅包括了4 章内容.读完这本迷你书,结合自身的工作.我想说一下自己对于重构的看法. 重构.是一把双刃剑,开发者不要轻易使用.举个样例来说,你如今正在从事某个行业的工作.但有人告诉你另外一个行业赚钱多并且快.于是你就非常纠结,究竟要不要改行呢?不改行吧,钱挣得少.改行吧,自己又是新手.对那个行业又不熟悉.这样的心理状态事实上就是开发者对于重构的态度,能够用"进退维谷&quo

python-----opencv读视频、循环读图片显示进度条

功能:opencv读视频,显示进度条,推动进度条快进.后退,按q退出.代码如下: import os import cv2 def nothing(emp): pass def jindu(name,video): cv2.namedWindow(name,0) cv2.resizeWindow(name, 800, 600) cap = cv2.VideoCapture(video) frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) loop_fl

读技术书籍一定要读最经典的

最近很多人问我读什么书,鉴于之前有空的时候也会读过一些书,特此整理一些分享给大家,如果有你读的很不错的书也欢迎留言推荐补充. 下面主要是还是针对 Java 工程师学习方向的相关书籍,我大致把进阶的书籍分为 Java 基础.数据库.网络.框架.架构.方法论和职场. 1, Java 基础 Java 基础主要还是围绕基础语法.多线程.JVM 和性能. 「基础」的书籍呢还是经典的<Java 编程思想>了,不过个人建议不必须那么早的看这本书,很多东西做项目多了,用的多了再回过头来看这本书效果会比较好.