The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times. For example, if a SELECT is executed twice, but returns a row the second time that was not returned the first time, the row is a “phantom” row.

上面这句话摘自MySQL的官方手册。它只说明了读会读到上一次没有返回的记录,看起来是幻影一般。如果你理解到这里,那么恭喜你,你会遇到各种困惑。 其实幻读的现象远不止于此,更不仅仅只是两次「读」,第二次「读」来发现有幻觉。

MySQL的InnoDb存储引擎默认的隔离级别是REPEATABLE-READ,即可重复读。 那什么是「可重复读」呢,简单来说就是一个事务里的两个相同条件的查询查到的结果应该是一致的,即结果是「可以重复读到的」,所以就解决了「幻读」。

OK,听起来很简单,一个隔离级别就可以搞定了,但是内部的机制和原理并不简单,并且有些概念的作用可能大家并不知道具体解决了什么问题。

MVCC(Multi-Version Concurrency Control多版本并发控制):

MVCC每次更新操作都会复制一条新的记录,新纪录的创建时间为当前事务id

优势为读不加锁,读写不冲突

InnoDb存储引擎中,每行数据包含了一些隐藏字段 DATA_TRX_ID,DATA_ROLL_PTR,DB_ROW_ID,DELETE BIT

DATA_TRX_ID 字段记录了数据的创建和删除时间,这个时间指的是对数据进行操作的事务的id

DATA_ROLL_PTR 指向当前数据的undo log记录,回滚数据就是通过这个指针

DELETE BIT位用于标识该记录是否被删除,这里的不是真正的删除数据,而是标志出来的删除。真正意义的删除是在mysql进行数据的GC,清理历史版本数据的时候。

具体的DML:

INSERT:创建一条新数据,DB_TRX_ID中的创建时间为当前事务id,DB_ROLL_PT为NULL

UPDATE:将当前行的DB_TRX_ID中的删除时间设置为当前事务id,DELETE BIT设置为1

DELETE:复制了一行,新行的DB_TRX_ID中的创建时间为当前事务id,删除时间为空,DB_ROLL_PT指向了上一个版本的记录,事务提交后DB_ROLL_PT置为NULL

可知,为了提高并发度,InnoDb提供了这个「非锁定读」,即不需要等待访问行上的锁释放,读取行的一个快照即可。 既然是多版本读,那么肯定读不到隔壁事务的新插入数据了,所以解决了幻读。

Read Uncommitted每次都读取记录的最新版本,会出现脏读,未实现MVCC

Serializable对所有读操作都加锁,读写发生冲突,不会使用MVCC

(RR级别)InnoDb检查每行数据,确保它们符合两个标准:

只查找创建时间早于当前事务id的记录,这确保当前事务读取的行都是事务之前已经存在的,或者是由当前事务创建或修改的行

行的DELETE BIT为1时,查找删除时间晚于当前事务id的记录,确定了当前事务开始之前,行没有被删除

(RC级别)每次重新计算read view,read view的范围为InnoDb中最大的事务id,为避免脏读读取的是DB_ROLL_PT指向的记录

就这么简单吗? 其实幻读有很多种出现形式,简单的SELECT不加条件的查询在RR下肯定是读不到隔壁事务提交的数据的。但是仍然可能在执行INSERT/UPDATE时遇到幻读现象。因为SELECT 不加锁的快照读行为是无法限制其他事务对新增重合范围的数据的插入的。

所以还要引入第二个机制。

其实更多的幻读现象是通过写操作来发现的,如SELECT了3条数据,UPDATE的时候可能返回了4个成功结果,或者INSERT某条不在的数据时忽然报错说唯一索引冲突等。

首先来了解一下InnoDb的锁机制,InnoDB有三种行锁:

Record Lock:单个行记录上的锁

Gap Lock:间隙锁,锁定一个范围,但不包括记录本身。GAP锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况

Next-Key Lock:前两个锁的加和,锁定一个范围,并且锁定记录本身。对于行的查询,都是采用该方法,主要目的是解决幻读的问题

如果是带排他锁操作(除了INSERT/UPDATE/DELETE这种,还包括SELECT FOR UPDATE/LOCK IN SHARE MODE等),它们默认都在操作的记录上加了Next-Key Lock。只有使用了这里的操作后才会在相应的记录周围和记录本身加锁,即Record Lock + Gap Lock,所以会导致有冲突操作的事务阻塞进而超时失败。

隔离级别越高并发度越差,性能越差,虽然MySQL默认的是RR,但是如果业务不需要严格的没有幻读现象,是可以降低为RC的或修改配置innodb_locks_unsafe_for_binlog为1 来避免Gap Lock的。 注意有的时候MySQL会自动对Next-Key Lock进行优化,退化为只加Record Lock,不加Gap Lock,如相关条件字段为主键时直接加Record Lock。

凡是在REPEATABLE-READ中执行的语句均不会遇到幻读现象。

这个显然是错误的。REPEATABLE-READ只是有机制可以用来防止幻读的发生,但如果你没有「使用」或「激活」它相关机制,你仍然会遇到幻读现象。

REPEATABLE-READ肯定不会读到隔壁事务已经提交的数据,即使某个数据已经由隔壁事务提交,当前事务插入不会报错,否则就是发生了幻读。

简单来说前半句话是对的,后半句有什么问题呢?可REPEATABLE-READ中如何「读」是我们自己来写SELECT 的,如果不加锁则属于快照读,当前事务读不到的数据并不一定是不存在的,如果已经存在对应的数据,那么当前事务尝试插入的时候是可能会失败的。 而插入失败的原因可能是因为主键冲突导致数据库报异常,跟隔离级别无直接关系。任何隔离级别下插入已经存在的数据都会报错。

一句话,看不到并不代表没有,并不代表可以自以为然的插入无忧。

REPEATABLE-READ的事务里查不到的数据一定是不存在的,所以我可以放心插入,100%成功。

这个观点也是错的,查不到只能说明当前事务里读不到,并不代表此时其他事务没有插入这样的数据。 如何保证判断某个数据不存在以后其他事务也不会插入成功?答案是上Next-Key Lock。不上锁是无法阻止其他事务插入的。

SELECT * FROM table1 WHERE id >100

上面这个语句在事务里判断后如果不存在数据是无法保证其他事务插入符合条件的数据的,需要加锁

SELECT * FROM table1 WHERE id >100 FOR UPDATE;

此时如果有隔壁事务尝试插入大于100的id的数据则会等待当前事务释放锁,直到超时后中断当前事务。

(waiting for lock … then timeout) ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

说了这么多,有一点要注意,就是这个Next-Key Lock一定是在REPEATABLE-READ下才有,READ-COMMITTED是不存在的。

To prevent phantoms, InnoDB uses an algorithm called next-key locking that combines index-row locking with gap locking. You can use next-key locking to implement a uniqueness check in your application: If you read your data in share mode and do not see a duplicate for a row you are going to insert, then you can safely insert your row and know that the next-key lock set on the successor of your row during the read prevents anyone meanwhile inserting a duplicate for your row. Thus, the next-key locking enables you to “lock” the nonexistence of something in your table.

即InnoDb在REPEATABLE-READ下提供Next-Key Lock机制,但是需要业务自己去加锁,如果不加锁,只是简单的SELECT查询,是无法限制并行事务的插入的。

凡是REPEATABLE-READ中的读都无法读取最新的数据。

这个观点也是错误的,虽然我们读取的记录都是可重复读取的,但是如果你想读取最新的记录可以用加锁的方式读。

If you want to see the “freshest” state of the database, you should use either the READ COMMITTED isolation level or a locking read:

以下任意一种均:

SELECT * FROM table1 LOCK IN SHARE MODE;

SELECT * FROM table1 FOR UPDATE;

但这里要说明的是这样做跟SERIALIZABLE没有什么区别,即读也加了锁,性能大打折扣。

如果使用了当前读加了锁,但是锁的行并不存在则不会阻止隔壁事务插入符合条件的数据。

其实记录存在与否和事务加锁成功与否无关,如SELECT * FROM user WHERE id = 5 FOR UPDATE,此时id=5的记录不存在,隔壁事务仍然无法插入记录(假设当前自增的主键id已经是4了)。因为锁定的是索引,故记录实体存在与否没关系。

MySQL中的幻读只有在读的时候才会发生,读这里特指SELECT操作。

其实INSERT也是隐式的读取,只不过是在MySQL的机制中读取的,插入数据也是要先读取一下有没有主键冲突才能决定是否执行插入的。 不可重复读测试「读-读」,而幻读侧重「读-写」,用写来证实读的是幻影。为啥幻读不是侧重「读-读」呢?因为MVCC保证了一个事务是不可能读到另外一个事务的新插入数据的,所以这种场景下不会发生幻读。

转载:http://www.yangchengec.cn/shiyong/566.html

别再误解MySQL和「幻读」了的更多相关文章

  1. 【Java面试】这应该是面试官最想听到的回答,Mysql如何解决幻读问题?

    "Mysql如何解决幻读问题" 一个工作了4年小伙伴,去一个美团面试,遇到了这样一个问题. 大家好,我是Mic,一个工作了14年的Java程序员 关于这个问题,面试官想考察什么?我 ...

  2. MySQL中的幻读,你真的理解吗?

    昨天接到阿里的电话面试,对方问了一个在MySQL当中,什么是幻读.当时一脸懵逼,凭着印象和对方胡扯了几句.面试结束后,赶紧去查资料,才发现之前对幻读的理解完全错误.下面,我们就聊聊幻读. 要说幻读,就 ...

  3. MYSQL如何解决幻读

    第一部分 首先要了解下mysql数据库的事务特征之一隔离级别: READ UNCOMMITTED(未提交读): 在READUNCOMMITTED级别,事务中的修改,即使没有提交,对其他事务也都是可见的 ...

  4. 面试官:MySQL的幻读是怎么被解决的?

    大家好,我是小林. 我之前写过一篇数据库事务的文章「 事务.事务隔离级别和MVCC」,这篇我说过什么是幻读. 在这里插入图片描述 然后前几天有位读者跟我说,我这个幻读例子不是已经被「可重复读」隔离级别 ...

  5. MySQL到底能否解决幻读问题

    先说结论,MySQL 存储引擎 InnoDB 在可重复读(RR)隔离级别下是解决了幻读问题的. 方法:是通过next-key lock在当前读事务开启时,1.给涉及到的行加写锁(行锁)防止写操作:2. ...

  6. 硬吃一个P0故障,「在线业务」应该如何调优HBase参数?

    1.背景 由于种种原因,最近将核心业务生产使用的HBase迁移到了云上的弹性MapReduce(EMR)集群上,并使用了EMR的HBase组件默认参数配置. 结果在流量高峰期出现了宿主机故障,挂掉了两 ...

  7. mysql系列:加深对脏读、脏写、可重复读、幻读的理解

    关于相关术语的专业解释,请自行百度了解,本文皆本人自己结合参考书和自己的理解所做的阐述,如有不严谨之处,还请多多指教. 事务有四种基本特性,叫ACID,它们分别是: Atomicity-原子性,Con ...

  8. MySQL 到底是怎么解决幻读的?

    ; 原理:将历史数据存一份快照,所以其他事务增加与删除数据,对于当前事务来说是不可见的. 2. next-key 锁 (当前读) next-key 锁包含两部分: 记录锁(行锁) 间隙锁 记录锁是加在 ...

  9. mysql 幻读

    幻读(Phantom Read) 是指当用户读取某一范围的数据行时,B事务在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影”行.InnoDB和Falcon存储引擎通 过多版本并发 ...

随机推荐

  1. CentOS 5.5 安装 64位 Oracle 10g

    参考官方文档(随着数据库文件一起下载) Oracle® DatabaseQuick Installation Guide 10gRelease 2 (10.2) for Linux x86-64 官方 ...

  2. 使用多个fixture和fixture直接互相调用

    使用多个fixture 如果用例需要用到多个fixture的返回数据,fixture也可以return一个元组.list或字典,然后从里面取出对应数据. # test_fixture4.py impo ...

  3. Tensorflow-gpu搭建CUDA 10.0与cuDNN等版本问题

    https://blog.csdn.net/weixin_42718092/article/details/85001140

  4. centos7 忘记root密码,如何进入单用户模式。

    init方法 1.centos7的grub2界面会有两个入口,正常系统入口和救援模式: 2.修改grub2引导 在正常系统入口上按下"e",会进入edit模式,搜寻ro那一行,以l ...

  5. JSON中文处理类实例

    $array = array( 'Name'=>'络恩', 'Age'=>24); $post=my_json_encode($array); // 这个函数是判断版本,如果是搞版本的则直 ...

  6. 学习Go语言(二)快速入门

    作为一名学习过多种编程语言的“老码农”,学习一门新的语言不能像“新手”一样,要快速入门. 无论面向过程,还是面向对象的编程语言:静态语言,动态语言,一般都包括: 标识符.变量(常量).运算符.表达式. ...

  7. 奉献pytorch 搭建 CNN 卷积神经网络训练图像识别的模型,配合numpy 和matplotlib 一起使用调用 cuda GPU进行加速训练

    1.Torch构建简单的模型 # coding:utf-8 import torch class Net(torch.nn.Module): def __init__(self,img_rgb=3,i ...

  8. ElasticSearch 7.3.0 查询、修改、删除 文档操作

    PUT chuyuan/_doc/ { "name":"xiaolin", , "sex":"F", "lov ...

  9. BZOJ 4857 反质数序列

    题面 奇数+奇数一定不是质数(1+1除外),偶数+偶数一定不是质数,质数只可能出现在偶数+奇数中 把所有的点排成两列,权值为奇数的点在左边,权值为偶数的在右边 如果左边的点x+右边的点y是质数,我们就 ...

  10. c语言小技巧:C语言学习笔记之位运算求余

    我们都知道,求一个数被另一个数整除的余数,可以用求余运算符”%“,但是,如果不 允许使用求余运算符,又该怎么办呢?下面介绍一种方法,是通过位运算来求余,但是注 意:该方法只对除数是2的N次方幂时才有效 ...