接上篇 事务隔离级别和幻读 ,留了个坑,没想到竟然过了10天,时间不注意真的过的好快。顺便提下,图片链接是属于网站的,开发自己的图床迫在眉睫,万一哪天迁移就要做很多额外工作,一些概念或者思路用图片表达更直观清楚。
成都创新互联公司是一家专业提供海口企业网站建设,专注与成都网站设计、成都网站制作、外贸网站建设、H5建站、小程序制作等业务。10年已为海口众多企业、政府机构等服务。创新互联专业网站制作公司优惠进行中。
回到正题,之前提到一般情况下MySQL的InnoDB引擎在可重复读的情况下是没法保证不出现幻读的,但实际情况是MySQL可以通过加锁来防止幻读的出现,这种锁定通过Next-key机制来实现,是属于记录锁和间隙锁(Gap锁)的结合。
引申,行级别锁的三种算法:
举个存在唯一索引和辅助索引的例子做说明:
执行 select * from test where b = 3 for update
存在两个索引,分别加锁,唯一主键列a加record lock , 辅助索引列b加next-key lock (1,3) 以及给下一个值的区间(3,6)加gap锁;
因此在另一个事务里执行以下语句都会阻塞,具体分析:
第一个阻塞因为加了唯一索引的record lock a = 5;
第二个主键插入4,符合条件,但是根据辅助索引b 的范围, b = 2 在(1,3)中,同样阻塞;
第三个a =6 不在主键a锁定范围,b = 5 也不在辅助索引b 的范围(1,3)中,但在另一个gap锁范围(3,6)中,因此也阻塞;
这种锁定情形下,可以执行的包括类似语句:
insert的特殊情况
对于insert 会检查下一条记录是否被锁定,如上述例子有 select * from test where b = 3 for update 插入 insert into test select 2,2 会检测到b = 3 已经被锁定,而 insert into test select 2,0 可以执行;
[1]:《MySQL技术内幕:InnoDB存储引擎》-第六章:锁
幻读是指:在一个事务中,读取到了其他已经提交的事务插入的数据行。
MySQL在解决脏读、不可重复的读时候,使用了MVCC一致性视图,同时配合行锁来解决。
至于幻读的解决方式,MySQL引入了临键锁,通过间隙锁可以避免在两个行之间插入数据,从而避免了一个事务在读取的过程中,读取到其他事务插入的数据行。
一、定义
1、幻读MYSQL官方叫法是Phantom Rows,意为鬼影行或者幻影行,请看官方定义:
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.
翻译一下:
所谓的幻影行问题是指,在同一个事务中,同样的查询语句执行多次,得到了不同的行结果集。
例如,如果同一个SELECT语句执行了两次,第二次执行的时候比第一次执行时多出一行,则该行就是所谓的幻影行。
2、幻读与不可重复读的区别
从官方的定义来看,幻读的定义侧重于多条记录,就是记录条数的变化,而不可重复读侧重于单条记录数据的变化,这样区分原因在于解决幻读需要范围锁,解决不可重复读只需要单条记录加锁
二、InnoDB的REPEATABLE READ级别
InnoDB支持由SQL1992标准描述的所有四个事务隔离级别,默认隔离级别是 REPEATABLE READ。
1、快照读:
在RR模式下,第一次读取会建立快照,后续查询会读取快照。
这意味着,如果在同一事务中发出多个普通[ SELECT ](非锁定)语句,则这些 [ SELECT ]语句的结果也是一致的。
2、[locking reads](锁定读取,又叫当前读)
[ SELECT ]语句中使用 FOR UPDATE 或 FOR SHARE
3、行锁
在RR模式下,使用当前读以及 [ UPDATE ]和 [ DELETE ]语句会对数据记录加行锁,锁定范围取决于该语句使用的是具有唯一搜索条件的唯一索引还是范围类型搜索条件。
三、InnoDB的READ COMMITTED级别
1、在RC模式下,每次读取都会刷新快照,因此不能保证可重复读
2、在RC模式下,使用当前读以及 [ UPDATE ]和 [ DELETE ]语句会对数据记录加行锁,但是不会加范围锁,间隙锁定仅用于外键约束检查和重复键检查。
3、由于禁用了间隙锁定,因此可能会产生幻影行问题,因为其他会话可以在间隙中插入新行。
4、 对于[ UPDATE ]或 [ DELETE ]语句, InnoDB 仅对其更新或删除的行持有锁。MySQL评估 WHERE 条件后,将释放不匹配行的记录锁 。这大大降低了死锁的可能性,但是仍然可以发生。
5、对于[ UPDATE ]语句,如果某行已被锁定,则 InnoDB 执行“半一致”读取,将最新提交版本的数据返回给MySQL,以便MySQL可以确定该行是否符合 WHERE 条件。如果该行匹配(必须更新),则MySQL会再次读取该行,这一次 InnoDB 会将其锁定或等待获取锁。
6、注意
从MySQL 8.0.22开始,DML操作(增删改,通过联接列表或子查询)从MySQL授权表中读取数据,但不对其进行修改,无论隔离级别如何,都不会在MySQL授权表上获得读取锁。
有关更多信息,请参见 Grant Table Concurrency 。
四、乐观锁与悲观锁
1、乐观锁
在UPDATE的WHERE子句中加入版本信息来确定修改是否生效
使用乐观锁时仍然需要非常谨慎,因为RR是可重复读的,在UPDATE之前读取版本号,应该使用[当前读],不能使用[快照读]
2、悲观锁
在UPDATE执行前,SELECT后面加上FOR UPDATE来给记录加锁,保证记录在UPDATE前不被修改。SELECT ... FOR UPDATE是加上了X锁,也可以通过SELECT ... LOCK IN SHARE MODE加上S锁,来防止其他事务对该行的修改。
3、无论是乐观锁还是悲观锁,使用的思想都是一致的,那就是当前读。乐观锁利用当前读判断是否是最新版本,悲观锁利用当前读锁定行。
五、总结
1、RC级别没有范围锁一定会导致不可重复读和幻影行
2、RR级别安全性更高,实现可重复读的方式为快照,如果需要最新数据可以选择[当前读],因此RR级别是首选
3、不论RR还是RC级别,增、删、改的操作都会进行一次[当前读]操作,以此获取最新版本的数据,并检测是否有重复的索引。
4、RR级别下,当前事务如果未发生更新操作(增删改),快照版本会保持不变,多次查询读取的快照是同一个
5、RR级别下,当前事务如果发生更新(增删改),会刷新快照,会导致不可重复读和幻影行
6、RR级别下,使用当前读,会刷新快照,会导致不可重复读和幻影行
7、RR级别下,可以通过提交当前事务并在此之后发出新查询来为查询获取更新的快照。
8、RR级别可以部分解决不可重复读和幻读问题
9、其实问题的关键是你的业务逻辑需要可重复读还是最新数据
什么是幻读?
幻读指的是一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行。
首先快照读是不存在幻读的,只有当前读(实时读)才存在幻读的问题。
幻读有什么问题?
select ...for update语句就是将相应的数据行锁住,但是如果存在幻读,就把for update的语义破坏了。
如何解决幻读?
产生幻读的原因是,行锁只能锁住行,但是新插入记录这个动作,要更新的是记录之间的“间隙”。因此,为了解决幻读问题,InnoDB只好引入新的锁,也就是间隙锁(Gap Lock)。间隙锁和行锁合称 next-key lock , 每个next-key lock是前开后闭区间 。
总结
最近在网上看了不少mysql锁的文章,不少文章都提到InnoDB的RR隔离级别(Repeatable Read)无法解决幻读的问题。对此问题作者亲自做了一些实验,将实验结论记录在此。
本次实验的mysql版本为5.7.22 。
此篇文章的重点在于通过实验的形式解释清楚InnoDB的RR隔离级别是否解决了幻读问题。所以文中将不会对一些相关的概念进行解释,默认读者已经具备相关知识。如果读者对于以下的知识点不甚清楚,最好自行查阅相关资料,理解清楚之后再阅读接下来的实验内容,以免造成困惑。
进行此次实验需要具备的知识点(包括但不限于):
创建表结构:
创建两条数据:
最终的表数据如下:
打开两个终端,连上mysql,分别启动事务a和事务b。
在事务a和事务b上面分别执行如下命令:
查询出来的结果如下:
事务a:
事务b:
很明显事务b没有查询到事务a未提交的新插入数据。原因也很简单,因为 普通的select语句是快照读,而事务b启动时,它的快照数据就已经被版本锁定了 。
那么我们在事务b里面执行如下命令来看看执行结果:
执行完成之后我们发现事务b此时会block住,原因是 事务a的insert语句排它锁住了id为3的新插入数据,而事务b想请求所有行的共享锁,肯定是需要等待的。
那么此时事务b当前读id为1或2的数据(非事务a新插入数据)是否可行呢?
结论是可行的,因为 tmp_table 存在唯一键,且事务a的insert语句只是锁住了id为3的行。所以其他事务获取其他行的共享锁是可行的 。读者可以自行测试,这里就不做演示了。
事务a和事务b执行如下命令:
事务b打印的结果:
还是一样, 因为普通select是快照读,事务b还是读取到的是快照数据,所以不包含事务a提交之后的新数据 。
让我们在事务b下面使用共享锁查看当前版本数据:
结果如下:
可以查询到事务a已提交的新数据,所以此时使用当前读就产生了幻读 。
还有另一种情况也会产生幻读,并且只需要执行普通的select语句。下面请看演示。
在事务b下面执行如下两条语句:
第一条命令使用update更新了事务a已提交的新数据,第二条命令通过普通的select语句查看快照数据。
打印结果如下:
可以看到事务a已提交的新数据被事务b使用update语句更新了,并且通过普通的select语句给查询出来了,很显然,出现了幻读 。
所以说InnoDB的RR隔离级别没有或者解决了幻读问题都不太准确。应该说它并没有完全解决幻读的问题。
如果在同一个事务里面,只是总是执行普通的select快照读,是不会产生幻读的。
但是如果在这个事务里面通过当前读或者先更新然后快照读的形式来读取数据,就会产生幻读。
Phantom Rows
Innodb 中 RR 隔离级别能否防止幻读?