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 MVCC机制原理详解
Apr 20 MySQL
MySQL的join buffer原理
Apr 29 MySQL
MySQL中distinct与group by之间的性能进行比较
May 26 MySQL
MySQL系列之七 MySQL存储引擎
Jul 02 MySQL
Mysql数据库中datetime、bigint、timestamp来表示时间选择,谁来存储时间效率最高
Aug 23 MySQL
MySQL定时备份数据库(全库备份)的实现
Sep 25 MySQL
彻底解决MySQL使用中文乱码的方法
Jan 22 MySQL
MySQL多表查询机制
Mar 17 MySQL
MySQL中IO问题的深入分析与优化
Apr 02 MySQL
Mysql索引失效 数据库表中有索引还是查询很慢
May 15 MySQL
MySQL数据库如何查看表占用空间大小
Jun 10 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
php上的memcache和memcached两个pecl库
2010/03/29 PHP
PHP对表单提交特殊字符的过滤和处理方法汇总
2014/02/18 PHP
浅谈ThinkPHP的URL重写
2014/11/25 PHP
PHP生成二维码与识别二维码的方法详解【附源码下载】
2019/03/07 PHP
PHP实现财务审核通过后返现金额到客户的功能
2019/07/04 PHP
兼容IE和Firefox的javascript获取iframe文档内容的函数
2011/08/15 Javascript
前台js调用后台方法示例
2013/12/02 Javascript
js统计录入文本框中字符的个数并加以限制不超过多少
2014/05/23 Javascript
jQuery功能函数详解
2015/02/01 Javascript
jQuery实现按钮只点击一次后就取消点击事件绑定的方法
2015/06/26 Javascript
jquery+html5时钟特效代码分享(可设置闹钟并且语音提醒)
2020/03/30 Javascript
jQuery简单实现仿京东商城的左侧菜单效果代码
2015/09/09 Javascript
深入剖析JavaScript中的函数currying柯里化
2016/04/29 Javascript
使用jQuery卸载全部事件的思路详解
2017/04/03 jQuery
浅谈node.js 命令行工具(cli)
2018/05/10 Javascript
教你搭建按需加载的Vue组件库(小结)
2019/07/29 Javascript
Vue仿微信app页面跳转动画效果
2019/08/21 Javascript
layui输入框中只允许输入整数的实现方法
2019/09/18 Javascript
vue更改数组中的值实例代码详解
2020/02/07 Javascript
vue随机验证码组件的封装实现
2020/02/19 Javascript
vue-axios同时请求多个接口 等所有接口全部加载完成再处理操作
2020/11/09 Javascript
[36:19]2018DOTA2亚洲邀请赛 小组赛 A组加赛 Newbee vs LGD
2018/04/03 DOTA
python批量添加zabbix Screens的两个脚本分享
2017/01/16 Python
通过Pandas读取大文件的实例
2018/06/07 Python
通过Python模块filecmp 对文件比较的实现方法
2018/06/29 Python
Python中实例化class的执行顺序示例详解
2018/10/14 Python
python3 批量获取对应端口服务的实例
2019/07/25 Python
python实现几种归一化方法(Normalization Method)
2019/07/31 Python
对tensorflow 中tile函数的使用详解
2020/02/07 Python
利用pipenv和pyenv管理多个相互独立的Python虚拟开发环境
2020/11/01 Python
在网上学习全世界最好的课程:Coursera
2017/11/07 全球购物
Shell编程面试题
2012/05/30 面试题
六十岁生日答谢词
2014/01/10 职场文书
人事部经理岗位职责
2014/03/07 职场文书
我的暑假生活作文(五年级)范文
2019/08/07 职场文书
小程序自定义轮播图圆点组件
2022/06/25 Javascript