mysql幻读详解实例以及解决办法


Posted in MySQL onJune 16, 2022
脏读/不可重复读的概念都比较容易理解和掌握,这里不在讨论

事务隔离级别(tx_isolation)

mysql 有四级事务隔离级别 每个级别都有字符或数字编号

级别 symbol 描述
读未提交 READ-UNCOMMITTED 0 存在脏读、不可重复读、幻读的问题
读已提交 READ-COMMITTED 1 解决脏读的问题,存在不可重复读、幻读的问题
可重复读 REPEATABLE-READ 2 mysql 默认级别,解决脏读、不可重复读的问题,存在幻读的问题。使用 MMVC机制 实现可重复读
序列化 SERIALIZABLE 3 解决脏读、不可重复读、幻读,可保证事务安全,但完全串行执行,性能最低

我们可以通过以下命令 查看/设置 全局/会话 的事务隔离级别

mysql> SELECT @@global.tx_isolation, @@tx_isolation;
+-----------------------+------------------+
| @@global.tx_isolation | @@tx_isolation   |
+-----------------------+------------------+
| REPEATABLE-READ       | READ-UNCOMMITTED |
+-----------------------+------------------+
1 row in set (0.00 sec)

# 设定全局的隔离级别 设定会话 global 替换为 session 即可 把set语法温习一下
# SET [GLOABL] config_name = 'foobar';
# SET @@[session.|global.]config_name = 'foobar';
# SELECT @@[global.]config_name;

SET @@gloabl.tx_isolation = 0;
SET @@gloabl.tx_isolation = 'READ-UNCOMMITTED';

SET @@gloabl.tx_isolation = 1;
SET @@gloabl.tx_isolation = 'READ-COMMITTED';

SET @@gloabl.tx_isolation = 2;
SET @@gloabl.tx_isolation = 'REPEATABLE-READ';

SET @@gloabl.tx_isolation = 3;
SET @@gloabl.tx_isolation = 'SERIALIZABLE';

幻读

首先我们要搞明白何谓幻读,目前网上的众多解释幻读的博文个人感觉仔细设想一下就能找出推翻的例子,就像博文把 非阻塞IO 等同为 异步IO,然后好多文章都纷纷借用,其实这俩货是完全不同,非阻塞IO 是 同步IO 中的一种模式,并非 异步IO。错误的观点都被大众认同的 "正确化" 了,扯远了,回归主题。

幻读会在 RU / RC / RR 级别下出现,SERIALIZABLE 则杜绝了 幻读,但 RU / RC 下还会存在脏读、不可重复读,故我们就以 RR 级别来研究 幻读,排除其他干扰。

注意:RR 级别下存在幻读的可能,但也是可以使用对记录手动加 X锁 的方法消除幻读。SERIALIZABLE 正是对所有事务都加 X锁 才杜绝了 幻读,但很多场景下我们的业务 sql 并不会存在 幻读 的风险。SERIALIZABLE 的一刀切虽然事务绝对安全,但性能会有很多不必要的损失。故可以在 RR 下根据业务需求决定是否加锁,存在幻读风险我们加锁,不存在就不加锁,事务安全与性能兼备,这也是 RR 作为 mysql 默认隔是个事务离级别的原因,所以需要正确的理解 幻读。

幻读错误的理解:说幻读是 事务A 执行两次 select 操作得到不同的数据集,即 select 1 得到 10 条记录,select 2 得到 11 条记录。这其实并不是幻读,这是不可重复读的一种,只会在 R-U R-C 级别下出现,而在 mysql 默认的 RR 隔离级别是不会出现的。

这里给出我对幻读的比较白话的理解:

幻读,并不是说两次读取获取的结果集不同,幻读侧重的方面是某一次的 select 操作得到的结果所表征的数据状态无法支撑后续的业务操作。更为具体一些:select 某记录是否存在,不存在,准备插入此记录,但执行 insert 时发现此记录已存在,无法插入,此时就发生了幻读。

这里给出 mysql 幻读的比较形象的场景(借用我在知乎上的回答):

table users: id primary key

事务T1

mysql幻读详解实例以及解决办法


事务T2

mysql幻读详解实例以及解决办法

step1 T1: SELECT * FROM \`users\` WHERE \`id\` = 1;
step2 T2: INSERT INTO \`users\` VALUES (1, 'big cat');
step3 T1: INSERT INTO \`users\` VALUES (1, 'big cat');
step4 T1: SELECT * FROM \`users\` WHERE \`id\` = 1;

T1 :主事务,检测表中是否有 id 为 1 的记录,没有则插入,这是我们期望的正常业务逻辑。

T2 :干扰事务,目的在于扰乱 T1 的正常的事务执行。

在 RR 隔离级别下,step1、step2 是会正常执行的,step3 则会报错主键冲突,对于 T1 的业务来说是执行失败的,这里 T1 就是发生了幻读,因为 T1 在 step1 中读取的数据状态并不能支撑后续的业务操作,T1:“见鬼了,我刚才读到的结果应该可以支持我这样操作才对啊,为什么现在不可以”。T1 不敢相信的又执行了 step4,发现和 setp1 读取的结果是一样的(RR下的 MMVC机制)。此时,幻读无疑已经发生,T1 无论读取多少次,都查不到 id = 1 的记录,但它的确无法插入这条他通过读取来认定不存在的记录(此数据已被T2插入),对于 T1 来说,它幻读了。

其实 RR 也是可以避免幻读的,通过对 select 操作手动加 行X锁(SELECT ... FOR UPDATE 这也正是 SERIALIZABLE 隔离级别下会隐式为你做的事情),同时还需要知道,即便当前记录不存在,比如 id=1 是不存在的,当前事务也会获得一把记录锁(因为InnoDB的行锁锁定的是索引,故记录实体存在与否没关系,存在就加 行X锁,不存在就加 next-key lock间隙X锁),其他事务则无法插入此索引的记录,故杜绝了幻读。

在 SERIALIZABLE 隔离级别下,step1 执行时是会隐式的添加 行(X)锁 / gap(X)锁的,从而 step2 会被阻塞,step3 会正常执行,待 T1 提交后,T2 才能继续执行(主键冲突执行失败),对于 T1 来说业务是正确的,成功的阻塞扼杀了扰乱业务的T2,对于T1来说他前期读取的结果是可以支撑其后续业务的。

所以 mysql 的幻读并非什么读取两次返回结果集不同,而是事务在插入事先检测不存在的记录时,惊奇的发现这些数据已经存在了,之前的检测读获取到的数据如同鬼影一般。

这里要灵活的理解读取的意思,第一次select是读取,第二次的 insert 其实也属于隐式的读取,只不过是在 mysql 的机制中读取的,插入数据也是要先读取一下有没有主键冲突才能决定是否执行插入。

不可重复读侧重表达 读-读,幻读则是说 读-写,用写来证实读的是鬼影。

RR级别下防止幻读

RR级别下只要对 SELECT 操作也手动加行(X)锁即可类似 SERIALIZABLE 级别(它会对 SELECT 隐式加锁),即大家熟知的:

# 这里需要用 X锁, 用 LOCK IN SHARE MODE 拿到 S锁 后我们没办法做 写操作
SELECT `id` FROM `users` WHERE `id` = 1 FOR UPDATE;

如果 id = 1 的记录存在则会被加行(X)锁,如果不存在,则会加 next-lock key / gap 锁(范围行锁),即记录存在与否,mysql 都会对记录应该对应的索引加锁,其他事务是无法再获得做操作的。

这里我们就展示下 id = 1 的记录不存在的场景,FOR UPDATE 也会对此 “记录” 加锁,要明白,InnoDB 的行锁(gap锁是范围行锁,一样的)锁定的是记录所对应的索引,且聚簇索引同记录是直接关系在一起的。

mysql幻读详解实例以及解决办法

id = 1 的记录不存在,开始执行事务:
step1: T1 查询 id = 1 的记录并对其加 X锁
step2: T2 插入 id = 1 的记录,被阻塞
step3: T1 插入 id = 1 的记录,成功执行(T2 依然被阻塞中),T1 提交(T2 唤醒但主键冲突执行错误)
T1事务符合业务需求成功执行,T2干扰T1失败。

SERIALIZABLE级别杜绝幻读

在此级别下,我们便不需要对 SELECT 操作显式加锁,InnoDB会自动加锁,事务安全,但性能很低

mysql幻读详解实例以及解决办法

step1: T1 查询 id = 2 的记录,InnoDB 会隐式的对齐加 X锁
step2: T2 插入 id = 2 的记录,被阻塞
step3: T1 插入 id = 2 的记录,成功执行(T2 依然被阻塞中)
step4: T1 成功提交(T2 此时唤醒但主键冲突执行错误)
T1事务符合业务需求成功执行,T2干扰T1失败。

总结

RR 级别作为 mysql 事务默认隔离级别,是事务安全与性能的折中,可能也符合二八定律(20%的事务存在幻读的可能,80%的事务没有幻读的风险),我们在正确认识幻读后,便可以根据场景灵活的防止幻读的发生。

SERIALIZABLE 级别则是悲观的认为幻读时刻都会发生,故会自动的隐式的对事务所需资源加排它锁,其他事务访问此资源会被阻塞等待,故事务是安全的,但需要认真考虑性能。

InnoDB 的行锁锁定的是索引,而不是记录本身,这一点也需要有清晰的认识,故某索引相同的记录都会被加锁,会造成索引竞争,这就需要我们严格设计业务 sql,尽可能的使用主键或唯一索引对记录加锁。索引映射的记录如果存在,加行锁,如果不存在,则会加 next-key lock / gap 锁 / 间隙锁,故 InnoDB 可以实现事务对某记录的预先占用,如果记录存在,它就是本事务的,如果记录不存在,那它也将是本是无的,只要本是无还在,其他事务就别想占有它。

到此这篇关于mysql幻读详解的文章就介绍到这了,更多相关mysql幻读详解内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!


Tags in this post...

MySQL 相关文章推荐
MySQL基础(一)
Apr 05 MySQL
MYSQL(电话号码,身份证)数据脱敏的实现
May 28 MySQL
MySQL库表名大小写的选择
Jun 05 MySQL
MySQL高速缓存启动方法及参数详解(query_cache_size)
Jul 01 MySQL
MySQL一些常用高级SQL语句
Jul 03 MySQL
MySQL8.0升级的踩坑历险记
Nov 01 MySQL
SQL 聚合、分组和排序
Nov 11 MySQL
Mysql使用全文索引(FullText index)的实例代码
Apr 03 MySQL
数据分析数据库ClickHouse在大数据领域应用实践
Apr 03 MySQL
MySQL事务操作的四大特性以及并发事务问题
Apr 12 MySQL
MySQL运行报错:“Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggre”解决方法
Jun 14 MySQL
DQL数据查询语句使用示例
Dec 24 MySQL
mysql数据库隔离级别详解
Jun 16 #MySQL
MySQL解决Navicat设置默认字符串时的报错问题
Jun 16 #MySQL
MySQL数据库之内置函数和自定义函数 function
Jun 16 #MySQL
MySQL数据库之存储过程 procedure
Jun 16 #MySQL
MySQL8.0 Undo Tablespace管理详解
Jun 16 #MySQL
MySQL串行化隔离级别(间隙锁实现)
Jun 16 #MySQL
MySQL详解进行JDBC编程与增删改查方法
Jun 16 #MySQL
You might like
深入file_get_contents函数抓取内容失败的原因分析
2013/06/25 PHP
PHP错误Parse error: syntax error, unexpected end of file in test.php on line 12解决方法
2014/06/23 PHP
php绘制一个扇形的方法
2015/01/24 PHP
在WordPress中安装使用视频播放器插件Hana Flv Player
2016/01/04 PHP
ExtJS 学习专题(一) 如何应用ExtJS(附实例)
2010/03/11 Javascript
Underscore.js 的模板功能介绍与应用
2012/12/24 Javascript
js显示时间 js显示最后修改时间
2013/01/02 Javascript
setInterval计时器不准的问题解决方法
2014/05/08 Javascript
Javascript封装DOMContentLoaded事件实例
2014/06/12 Javascript
ECMAScript6中Set/WeakSet详解
2015/06/12 Javascript
jquery实现从数组移除指定的值
2015/06/24 Javascript
jquery实现点击查看更多内容控制段落文字展开折叠效果
2015/08/06 Javascript
JS+CSS实现美化的下拉列表框效果
2015/08/11 Javascript
jQuery Mobile弹出窗、弹出层知识汇总
2016/01/05 Javascript
html5+javascript实现简单上传的注意细节
2016/04/18 Javascript
jQuery Ajax 全局调用封装实例代码详解
2016/06/02 Javascript
jQuery DOM节点的遍历方法小结
2017/08/15 jQuery
jQuery选择器之层次选择器用法实例分析
2019/02/19 jQuery
详解vue-cli 脚手架 安装
2019/04/16 Javascript
js回溯法计算最佳旅行线路代码实例
2019/09/11 Javascript
Vue中图片Src使用变量的方法
2019/10/30 Javascript
Openlayers+EasyUI Tree动态实现图层控制
2020/09/28 Javascript
Python实现的连接mssql数据库操作示例
2018/08/17 Python
Python实现针对json中某个关键字段进行排序操作示例
2018/12/25 Python
python实现简单五子棋游戏
2019/06/18 Python
html5使用window.postMessage进行跨域实现数据交互的一次实战
2021/02/24 HTML / CSS
6号汽车旅馆预订:Motel 6
2018/02/11 全球购物
The Beach People美国:澳洲海滨奢华品牌
2018/07/05 全球购物
世界上最大的皮肤科医生拥有和经营的美容网站:LovelySkin
2021/01/03 全球购物
SOA面试题:如何在SOA中实现松耦合
2013/07/21 面试题
银行介绍信范文
2014/01/10 职场文书
简单租房协议书
2014/04/09 职场文书
运动会广播稿100字
2014/09/14 职场文书
贪污受贿检讨书范文
2014/11/19 职场文书
七年级地理教学计划
2015/01/22 职场文书
Linux服务器离线安装 nginx的详细步骤
2022/06/16 Servers