MySQL学习(一)脏读、不可重复读、幻读(鸣人和佐助上学的故事)

文章目录

    • 脏读、不可重复读、幻读
      • 1. 脏读(事务可以读取未提交的数据)
      • 2. 不可重复读(两次执行同样的查询,可能会得到不一样的结果)
      • 3. 幻读(也是读取了提交的新事物,指增、删操作)
    • 不可重复读重点在于update,而幻读的重点在于insert和delete。
    • 最常见、常问的是 RC(读已提交)、RR(可重复读),一句话概括他俩的区别:RC读的是当前存在的最新版本的数据,RR读的是事务开启之前的最后一个版本状态的数据。
    • 事务
      • 事务是什么
      • 事务的 4 种特性 - ACID
        • 原子性(atomicity)
        • 一致性(consistency)
        • 隔离性(isolation)
        • 持久性(durability)
      • 事务的 4 种隔离级别(强弱级别逐渐递增)
        • 读未提交(read uncommitted)
        • 读已提交(read committed)
        • 可重复读(repeatable read)
        • 可串行化(serializable)
      • MySQL 查看及设置事务隔离级别
      • MVCC
    • 描述如有错误不足之处,各位看官请指正下哈~
    • 参考资源:

脏读、不可重复读、幻读

概念很简单,网上大都差不多,讲的也都很清楚了,就是之前网上看到的实际给的例子很少,感觉理解总是不太透彻,隔断时间可能记得就不太清楚了,今天想着自己亲自实验下,试下这几个效果,加深下理解,便于记忆。


1. 脏读(事务可以读取未提交的数据)

所谓的脏读,其实就是读到了别的事务回滚前的脏数据

比如事务B执行过程中修改了数据X,在未提交前,事务A读取了X,而事务B却回滚了,这样事务A就形成了脏读。

也就是说,当前事务读到的数据是别的事务想要修改但是没有修改成功的数据。

示例:先创建一个空表:

CREATE TABLE `account` (
  `id` int(11) PRIMARY KEY AUTO_INCREMENT,
  `name` varchar(50) DEFAULT NULL comment '账户名',
  `balance` decimal(20,2) DEFAULT NULL comment '余额'
) comment '账户表';

旋涡鸣人开学了,初始化一个账户,先给自己充值了100块钱:

INSERT INTO account(`name`, `balance`) values ('旋涡鸣人', 100);

MySQL学习(一)脏读、不可重复读、幻读(鸣人和佐助上学的故事)_第1张图片
开学了,学校通知每人的账户都至少需要 200 元,鸣人的爸爸和鸣人的妈妈同一时间查看鸣人的账户里充值(如果不够就充值),我们把事务隔离级别设置为读未提交看看脏读是怎么一回事:

开启两个命令行窗口,都设置隔离级别为读未提交(read uncommitted):
MySQL学习(一)脏读、不可重复读、幻读(鸣人和佐助上学的故事)_第2张图片
脏读:

时间顺序 爸爸充值事务 妈妈充值事务
1 开始事务
2 开始事务
3 查询鸣人账户余额为 100 元
4 充值 100 元,余额被更改为 200 元
5 查询账户余额为 200 元(脏读)
6 充值失败,事务回滚,余额变更为 100 元
7 爸爸看到钱够了,不再充值了!!
8 提交事务
备注 由于爸爸读到了未提交的 “脏数据”,导致鸣人去学校发现账户钱不够了。。。

妈妈充值事务:
MySQL学习(一)脏读、不可重复读、幻读(鸣人和佐助上学的故事)_第3张图片
爸爸充值事务,发生脏读了:
MySQL学习(一)脏读、不可重复读、幻读(鸣人和佐助上学的故事)_第4张图片
妈妈充值失败,事务回滚:
MySQL学习(一)脏读、不可重复读、幻读(鸣人和佐助上学的故事)_第5张图片
最后鸣人妈妈和爸爸一沟通,都以为爸爸完成充值了,但鸣人一查账户余额,发现钱不够,内心…

(解决方案:一个事务在读取数据的时候,禁止读取未提交的事务里数据变更)


2. 不可重复读(两次执行同样的查询,可能会得到不一样的结果)

事务A首先读取了一条数据,然后执行逻辑的时候,事务B将这条数据改变了,然后事务A再次读取的时候,发现数据不匹配了,就是所谓的不可重复读了。

也就是说,当前事务先进行了一次数据读取,然后再次读取到的数据是别的事务修改成功的数据,导致两次读取到的数据不匹配,也就照应了不可重复读的语义。

示例:

经过上一乌龙事件,吸取教训,鸣人老爸直接给鸣人账号充值了 9900 元,现在鸣人有了 10000 元,在学校里花钱很随意啦,某一天(还剩5000元),妈妈怕鸣人钱不够花了,想查下鸣人还有多少钱,这时候爸爸又给鸣人充值 5000 元,于是就发生了不可重复读事件:

时间顺序 爸爸充值事务 妈妈查询事务
1 开始事务
2 第一次查询,鸣人账户还剩 5000 元
3 开始事务
4 妈妈接了个电话
5 再次充值 5000 元
6 提交事务
7 妈妈想确认一次,第二次查询,账户余额为 10000 元
备注 按正确逻辑,妈妈在一次查询事务里前后两次读取到的数据应该一致

妈妈查询事务,第一次查询,账户余额 5000 元:
MySQL学习(一)脏读、不可重复读、幻读(鸣人和佐助上学的故事)_第6张图片

爸爸充值5000元,提交事务,账户余额更新为 10000 元:MySQL学习(一)脏读、不可重复读、幻读(鸣人和佐助上学的故事)_第7张图片

妈妈再次查询余额,发现和第一次读的数据不一致了:
MySQL学习(一)脏读、不可重复读、幻读(鸣人和佐助上学的故事)_第8张图片

(解决方案:一个事务在读数据的时候,禁止其他任何事务对该数据进行修改)


3. 幻读(也是读取了提交的新事物,指增、删操作)

高性能mysql第三版对幻读的解释是:“所谓幻读,指的是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行”。

幻行怎么理解? 一开始没搞明白,看了网上好多说是事务A前后两次查询的总条数会因为中间有事务B新增了一条数据导致查询询结果不一致。自己试验了一下不是这样子的,两次查询条数一模一样!只是在事务A插入事务B已插入的数据,会提示该行数据已存在,但插入之前明明是没查到的,这个就是幻影行。

当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次对该范围的数据进行插入记录时,会提示数据已存在,即产生了所谓的幻影行

幻读:当前只有鸣人一个账户的数据,假设每个人的账户数据都是唯一的。

时间顺序 A事务 B事务
1 开始事务
2 第一次查询,仅有鸣人一个账户的数据
3 开始事务
4 第一次查询,仅有鸣人一个账户的数据
5 插入宇智波·佐助账户数据
6 提交事务
7 第二次查询,还是只有鸣人一个账户的数据
8 插入宇智波·佐助的账户数据,发现插入失败!提示佐助账户已存在。。。
备注 按正常逻辑,查询了只有鸣人一个账户,再插入佐助的账户数据肯定是可以的,但这里提示重复插入了,这条数据就是所谓的幻影行。 提交当前事务,再次查询,发现会有俩账户数据。

人陆续来了,现在有两个账户了:
MySQL学习(一)脏读、不可重复读、幻读(鸣人和佐助上学的故事)_第9张图片
开启两个会话窗口,设置隔离级别为可重复读。

事务A:查询账户总数:
MySQL学习(一)脏读、不可重复读、幻读(鸣人和佐助上学的故事)_第10张图片

事务B:新增一条数据:
MySQL学习(一)脏读、不可重复读、幻读(鸣人和佐助上学的故事)_第11张图片

事务 A 第二次查询总数,发现 id 为 4 的记录明明查不到,插入却发现有些数据已经存在了,之前的检测获取的数据如同鬼影一般,即为幻读 :
MySQL学习(一)脏读、不可重复读、幻读(鸣人和佐助上学的故事)_第12张图片

(解决方案:一个事务加上表级锁,只要有任何东西操作这个表,就上锁,禁止读写并发)

But,因为表级锁性能太低下了,InnoDB 存储引擎并没采取这种方案,而是通过 间隙锁 + 多版本并发控制(MVCC -> Multiversion Concurrency Control)解决了幻读的问题。

不可重复读的重点是修改:同样的条件, 你读取过的数据, 再次读取出来发现值不一样了;
幻读的重点在于 事务A第一次读取数据,事务B对同一个表进行了insert,事务A第二次读取数据没读到,重新插入一次,提示已存在!这时是幻读。

不可重复读重点在于update,而幻读的重点在于insert和delete。

想要把脏读、不可重复读、幻读弄的更清楚之前,需要先学习一波事务,在事务不同的隔离级别下会产生不同的问题,上面简单介绍了这几个xx读的概念,只需要了解概念的童靴扫描一下看看概念就可以啦。如果想了解更深入一点,继续往下看吧!
ps:本来事务也想详细写写的,后来发现写完上边几个概念就占用了好大一块,太长啦,事务就先简单罗列了下概念,后面单独再写个~

最常见、常问的是 RC(读已提交)、RR(可重复读),一句话概括他俩的区别:RC读的是当前存在的最新版本的数据,RR读的是事务开启之前的最后一个版本状态的数据。

MySQL学习(一)脏读、不可重复读、幻读(鸣人和佐助上学的故事)_第13张图片

事务

事务是什么

事务就是一组原子性的 SQL 查询,或者说一个独立的工作单元。如果数据库引擎能够成功地对数据库应用该组查询的全部语句,那么就执行该组查询。如果其中有任何一条语句因为崩溃或其他原因无法执行,那么所有的语句都不会执行。通俗的说,事务内的语句,要么全部执行成功,要么全部执行失败。

事务的 4 种特性 - ACID

原子性、隔离性、持久性是手段,一致性是目的。

原子性(atomicity)

事务是一个完整的操作。事务的各步操作是不可分的(原子的);要么都执行,要么都不执行。

一致性(consistency)

当事务完成时,数据必须处于一致状态。

隔离性(isolation)

对数据进行修改的所有并发事务是彼此隔离的,这表明事务必须是独立的,它不应以任何方式依赖于或影响其他事务。

持久性(durability)

事务完成后,它对数据库的修改被永久保持,事务日志能够保持事务的永久性。

事务的 4 种隔离级别(强弱级别逐渐递增)

隔离级别越弱,出现的问题越多!打个比方,如果腾讯文档同一时间允许多人同时读取、同时修改,和同一时间只允许一个人读取、修改,哪个会出现更多问题呢 ?

读未提交(read uncommitted)

在一个事务中,可以读取到其他事务未提交的数据变化。

读已提交(read committed)

在一个事务中,可以读取到其他事务已经提交的数据变化。

可重复读(repeatable read)

可重复读是 MySQL 的默认事务隔离级别。

在一个事务中,直到事务结束前,都可以反复读取到事务刚开始时看到的数据,并且一直不会发生变化。

可串行化(serializable)

最高的隔离级别,所有的事务串行执行,资源消耗最大。

简单来说,这个级别会在读取的每一行数据都加锁,所以可能导致大量的超时和锁争用的问题。

MySQL 查看及设置事务隔离级别

  1. 查看:select @@tx_isolation
  2. 设置:set session transaction isolation level 事务隔离级别

MVCC

MVCC 是行级锁的一个变种,但是它在很多情况下避免了加锁操作,因此开销更低。

MVCC 的实现,是通过保存数据在某个时间点的快照来实现的。也就是说,不管需要执行多长时间,每个事务看到的数据都是一致的。根据事务开始的时间不同,每个事务对同一张表、同一时刻看到的数据可能是不一样的。

描述如有错误不足之处,各位看官请指正下哈~

参考资源:

《高性能MySQL》
https://www.cnblogs.com/wangenxian/p/11014504.html (概念解释:脏读、不可重复读、幻读)
https://blog.csdn.net/xmh594603296/article/details/79676844
https://blog.csdn.net/qq_33591903/article/details/81672260 (快速理解脏读、不可重复读、幻读概念)脏读举例里我这边验证了感觉有一点点小问题,我试了在产生了脏读之后,再直接更新余额,会更新失败。。
上面这个链接,我感觉幻读解释的也不对呢。。。
自己实验后,发现和下边这个链接里幻读的含义是一致的:
https://www.cnblogs.com/xiaohanlin/p/8644749.html
https://baijiahao.baidu.com/s?id=1659659238415878074&wfr=spider&for=pc (幻读)

你可能感兴趣的:(MySQL)