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 相关文章推荐
left join、inner join、right join的区别
Apr 05 MySQL
解读MySQL的客户端和服务端协议
May 10 MySQL
MySql存储过程之逻辑判断和条件控制
May 26 MySQL
MySQL一些常用高级SQL语句
Jul 03 MySQL
MySQL 服务和数据库管理
Nov 11 MySQL
mysql中整数数据类型tinyint详解
Dec 06 MySQL
MySQL数据库如何给表设置约束详解
Mar 13 MySQL
mysql 子查询的使用
Apr 28 MySQL
MySQL中EXPLAIN语句及用法
May 20 MySQL
mysql数据库隔离级别详解
Jun 16 MySQL
MySQL count(*)统计总数问题汇总
Sep 23 MySQL
mysql序号rownum行号实现方式
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
php实现分页工具类分享
2014/01/09 PHP
PHP+Ajax实现无刷新分页实例详解(附demo源码下载)
2016/04/07 PHP
thinkPHP统计排行与分页显示功能示例
2016/12/02 PHP
javascript smipleChart 简单图标类
2011/01/12 Javascript
JavaScript代码轻松实现网页内容禁止复制(代码简单)
2015/10/23 Javascript
js获取元素的外链样式的简单实现方法
2016/06/06 Javascript
浅谈JSON.stringify()和JOSN.parse()方法的不同
2016/08/29 Javascript
Bootstrap基本样式学习笔记之标签(5)
2016/12/07 Javascript
jQuery读取XML文件的方法示例
2017/02/03 Javascript
js鼠标跟随运动效果
2017/03/11 Javascript
jQuery实现 RadioButton做必选校验功能
2017/06/15 jQuery
运用jQuery写的验证表单(实例讲解)
2017/07/06 jQuery
关于vue.extend和vue.component的区别浅析
2017/08/16 Javascript
Vue实现用户自定义字段显示数据的方法
2018/08/28 Javascript
浅谈Node框架接入ELK实践总结
2019/02/22 Javascript
vue element自定义表单验证请求后端接口验证
2019/12/11 Javascript
vue 限制input只能输入正数的操作
2020/08/05 Javascript
[01:03:18]DOTA2-DPC中国联赛 正赛 RNG vs Dynasty BO3 第一场 1月29日
2021/03/11 DOTA
python图像处理之镜像实现方法
2015/05/30 Python
Python下载网络小说实例代码
2018/02/03 Python
Python 互换字典的键值对实例
2019/02/12 Python
Python实现栈和队列的简单操作方法示例
2019/11/29 Python
Python连接HDFS实现文件上传下载及Pandas转换文本文件到CSV操作
2020/06/06 Python
基于Python实现体育彩票选号器功能代码实例
2020/09/16 Python
python 日志模块logging的使用场景及示例
2021/01/04 Python
html5 offlline 缓存使用示例
2013/06/24 HTML / CSS
马来西亚最大的在线隐形眼镜商店:MrLens
2019/03/27 全球购物
小学标准化建设汇报材料
2014/08/16 职场文书
学习十八大的心得体会
2014/09/12 职场文书
介绍信样本
2015/01/31 职场文书
研究生学习计划书应该怎么写?
2019/09/10 职场文书
JavaScript继承的三种方法实例
2021/05/12 Javascript
一文搞懂Python Sklearn库使用
2021/08/23 Python
分享一个vue实现的记事本功能案例
2022/04/11 Vue.js
Go语言grpc和protobuf
2022/04/13 Golang
el-table-column 内容不自动换行的解决方法
2022/08/14 Vue.js