一篇文章读懂 MySQL 事务中的四种隔离级别,以及脏读、不可重复读、幻读详解
一. 介绍
1.1 MySQL 事务的四种隔离级别
MySQL 中事务的隔离级别一共分为四种:
序列化(SERIALIZABLE)
可重复读(REPEATABLE READ)
提交读(READ COMMITTED)
未提交读(READ UNCOMMITTED)
1.2 含义
-
SERIALIZABLE
----- 序列化当隔离级别为序列化时,用户之间通过一个接着一个的执行当前事务,不能并发执行。这种隔离级别提供了事务之间最大限度的隔离
-
REPEATABLE READ
----- 可重复读这是 MySQL 数据库中的默认事务隔离级别。
可重复读是指在同一个事务内,多次读取同一个数据,在可重复读的这一级别上,事务不会被看成一个序列。
不过,当前正在执行事务的变化仍然不能被外部看到,也就是说,如果用户在当前事务对数据进行了更改,然后执行
SELECT
语句(显然此时查询的是更改之后的数据),同时在另一个事务中执行了同一条SELECT
语句多次,结果总是与更改之前的数据是相同的。因为正在执行的事务所产生的数据变化不能被外部看到。
-
READ COMMITTED
----- 提交读READ COMMITTED
隔离级别的安全性要比REPEATABLE READ
隔离级别的安全性要差。READ COMMITTED
隔离级别的事务可以看到其他事务对数据的修改,也就是说,在事务处理期间,如果其他事务修改了相应的表,那么同一个事务的多个SELECT
语句可能返回不同的结果。 -
READ UNCOMMITTED
----- 未提交读READ UNCOMMITTED
提供了事务之间最小限度的隔离。该事务隔离界别除了容易产生幻读和不能重复读之后,处于该级别的事务还可以读到其他事务还没有提交的数据。
如果这个事务使用其他事务不提交的变化作为计算基础,然后那些未提交的变化被它们的父事务撤销,这就导致了大量的数据变化了。
二. SQL 演示
2.1 查看隔离级别
-
在 MySQL 8 之前使用如下命令查看隔离级别
# 这是 MySQL 8 之前查看隔离级别的方式 SELECT @@GLOBAL.tx_isolation, @@tx_isolation;
-
在 MySQL 8 之后,查看事务的隔离级别则是(其实就是关键字变了,其他的都样):
# 这是 MySQL 8 之后查看隔离级别的方式 # GLOBAL 表示查看全局的隔离级别;后面的表示查看当前会话的隔离级别 SELECT @@GLOBAL.transaction_isolation,@@transaction_isolation
因为我用的是 MySQL 8 的,所以在这里只演示 8 的版本了。
运行之后,就会看到如下效果:默认的隔离级别是
REPEATABLE-READ
,全局的隔离级别和当前会话的隔离级别都是这个。
2.2 修改隔离级别
-
通过下面的这条命名,就可以修改隔离级别了。但需要注意的是,这里修改的是修改当前 session 隔离级别,不用修改全局隔离级别。
#修改当前事务的隔离级别为 read uncommitted,在这种隔离级别中,可以读到别的事务未提交的数据 SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
当然,如果想要设置全局的,就可以像下面这样。这里只演示一次,后面就不作演示了。其实就是
session 当前会话
与global 全局
的区别罢了。# 这是设置全局的隔离级别 SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
-
继续上面的,在把会话的隔离级别修改之后,就可以再次查看事务的隔离级别了,此时当前 session (也就是当前会话) 的隔离级别已经发生改变了
-
注意,如果只修改当前会话的隔离级别,当我们换了一个会话的时候,隔离级别就又恢复到默认的隔离级别了,即
REPEATABLE-READ
。所以,我们测试时,只修改当前 session 的隔离级别就可以了。
2.3 脏读、不可重复读、幻读
在正式开始演示之前,先来大概的了解一下什么是脏读、不可重复读、幻读。
-
脏读 :一个事务读取到另一个事务还没有提交的数据
-
不可重复读 :在一个事务中多次读取同一个数据时,结果出现不一致
-
幻读 :在一个事务中,使用相同的 SQL 两次读取,第二次读取到其他事务新插入的行
不可重复读注重于数据的修改,而幻读注重于数据的插入
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
未提交读:READ UNCOMMITTED | 有 | 有 | 有 |
提交读:READ COMMITTED | 无 | 有 | 有 |
可重复读(默认):REPEATABLE READ | 无 | 无 | 有 |
序列化:SERIALIZABLE | 无 | 无 | 无 |
2.4 READ UNCOMMITTED ----- 未提交读
READ UNCOMMITTED 是最低的隔离级别,这种级别中存在着 脏读 、不可重复读 、幻读 的问题。
-
首先,在数据库中先创建一个简单的表,用于测试。表很简单,就两个字段,如下:
-
接着,假设这里有两个用户,一个是 peng,一个是 qiqi,两个用户分别有 200 元。
-
一会儿的场景是,在一个事务中对两个用户的 money 字段进行数据上的操作;然后在另一个事务中也对该字段的数据进行操作。在此基础上,分别观察两者之间的区别。
注意 : 我这里测试用的是 Navicat 。在 Navicat 中不同的查询窗口就是不同的会话;如果使用的是 SQLyog 的话,不同的查询窗口对应的是同一个会话,所以 SQLyog 需要再开启一个新的连接,注意!是新的连接,在新的连接中进行查询操作。
2.4.1 脏读
-
首先打开两个 SQL 操作窗口,分别假设为 会话A 和 会话B。在 会话A 中输入几条 SQL(注意,这里输入完成后,先不用执行)
# 开启事务A START TRANSACTION; # 场景:qiqi 给 peng 转钱 UPDATE bank SET money=money - 50 WHERE username = 'qiqi'; UPDATE bank SET money=money + 50 WHERE username = 'peng'; # 提交事务 COMMIT;
-
在 会话B 中修改 MySQL 中默认的事务隔离级别为:READ UNCOMMITTED
# 把事务的隔离级别修改为:READ UNCOMMITTED SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
-
接着在 会话B 中输入以下 SQL ,然后运行第一行,开启事务B(注意,这里只需要执行一条语句,开启事务B即可)
# 开启事务B START TRANSACTION; # 查询表 bank SELECT * FROM bank; # 提交事务 COMMIT;
-
然后回到 会话A 中开启事务,并执行前两条 SQL 语句,给 peng 用户转账 50 元(注意,这里是执行前三条 SQL,执行后不提交)
此时就实现了在两个不同的会话中,去测试事务的隔离级别的不同了。后面的其他几个隔离级别也是在此基础上去测试的。
-
然后再回到 会话B 中,执行第二条语句,查询表 bank,结果如下:
你会发现,会话A 中的事务还没有结束,是还没有提交的,但是在 会话B 中已经能够查询到 bank 表的变化了。这就是 脏读。
2.4.2 不可重复读
-
有了上面演示的铺垫之后,这里就稍微的简略一些了。首先,将 peng 和 qiqi 的 money 字段的值都恢复成原来的 200。
-
在 会话B 中输入以下 SQL ,并且执行前两条:开启事务以及查询
# 开启事务B START TRANSACTION; # 查询表 bank,并且 name = peng 的内容 SELECT * FROM bank where username = 'peng'; # 提交事务 COMMIT;
执行之后的结果:
-
接着,在 会话A 中输入以下 SQL,并且执行并提交。
# 开启事务A START TRANSACTION; # 给 peng 增加 50 UPDATE bank SET money=money + 50 WHERE username = 'peng'; # 提交事务 COMMIT;
-
然后回到 会话B,再次执行第二条 SQL 语句,查询结果:
会话A 是已经提交的,而会话B 是还没有提交的状态,还在事务中的 会话B 读取到了已经修改并提交的数据,即前后两次查询,结果不一致,这就是 不可重复读
脏读 与 不可重复读 的区别:脏读是看到了其他事务未提交的数据,而不可重复读是看到了其他事务已经提交了的数据(由于 会话B 也是在事务中的,因此有可能并不想看到其他事务已经提交的数据)
2.4.3 幻读
幻读 与 不可重复读 非常像
-
在 会话A 中输入以下 SQL
# 开启事务A START TRANSACTION; # 添加一条记录 INSERT INTO bank(username,money) VALUES('zhangsan',300); # 提交事务 COMMIT;
-
接着在 会话B 中输入以下 SQL ,并且执行前两条:开始事务并查询
# 开启事务B START TRANSACTION; # 查询 bank 表 SELECT * FROM bank; # 删除 bank 表中 name = zhangsan 的字段 DELETE FROM bank WHERE username = 'zhangsan' # 提交事务 COMMIT;
查询的结果如下:
-
此时表中是只有 peng 和 qiqi 的,接着执行 会话A 中的前条 SQL:开启事务和添加一条记录。
-
然后再次执行一次 会话B 中的第二条 SQL 语句,查询 bank 表(注意!不要又一次执行
START TRANSACTION
开启事务了,只需要执行第二条 SQL 查询即可)。因为 脏读 的原因,此时是可以查询到添加之后的记录的: -
接着执行 会话B 的第三条 SQL 语句,即删除 name = zhangsan 的字段,这个时候删除就会出现问题。虽然在 会话B 中能够查询到 zhangsan,但是该条记录是还没有提交的,是因为的 脏读 的原因才看到的,所以是无法删除的。此时就产生了幻觉,明明有 zhangsan,却无法删除,这个就是 幻读
2.5 READ COMMITTED ----- 提交读
和 READ UNCOMMITTED 相比,READ COMMITTED 主要解决了 脏读 的问题,对于 不可重复读 和 幻读 则没有解决
-
将事务的隔离级别该为
READ COMMITTED
之后,重复上面的操作,可以发现,关于 脏读 的问题已经不在存在了,而 不可重复读 依然还是存在的。 -
至于 幻读 ,在上面的测试中的幻读例子,由于 脏读 在这里已经解决了,所以上面的例子不舍用在当前的测试中。
-
我们来看一个新的例子。首先需要把 会话B 的隔离级别改为
READ COMMITTED
# 隔离级别修改为 READ COMMITTED SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED
-
在 会话A 中输入以下 SQL(这里说一下,这里的数据都是手动恢复成最初的数据,即 peng 和 qiqi 都为 200):
# 开启事务A START TRANSACTION; # 添加一条记录 INSERT INTO bank(username,money) VALUES('zhangsan',300); # 提交事务 COMMIT;
-
接着在 会话B 中输入以下 SQL,并且运行前两条 SQL,这里的查询结果与前面的是一样的,所以这里就不啰嗦了。
# 开启事务B START TRANSACTION; # 查询 bank 表 SELECT * FROM bank; # 这里也添加一条记录 INSERT INTO bank(username,money) VALUES('zhangsan',300) # 提交事务 COMMIT;
-
接着在 会话A 中执行前两条 SQL 语句:开启事务以及添加一条记录。注意,只运行前两条 SQL,不提交!
-
然后在 会话B 中执行第二条 SQL ,即继续查询。此时因为已经不存在 脏读 问题,所以这里的查询是查不到 会话A 中添加的数据的。
-
接着在 会话B 中执行第三条 SQL 语句,也添加一条记录。由于 username 字段是唯一的,因为这里也无法插入。此时就会产生幻觉,明明没有 zhangsan 这个用户,却无法插入 zhangsan 。所以,这里也存在着 幻读
2.6 REPEATABLE READ ----- 可重复读
和 READ COMMITTED 相比,REPEATABLE READ 进一步解决了不可重复读的问题,但是幻读则未解决。
- REPEATABLE READ 中关于幻读的测试与上面大同小异,不一样的是,在 会话A 中添加一条记录的时候,需要提交事务。
- 因为 REPEATABLE READ 已经解决了不可重复读的问题,因此在 会话A 中提交了事务,在 会话B 中的查询也是查不到数据的。但当 会话B 也添加一条记录的时候,就会出错,这里就出现了幻读现象!
- 注意,REPEATABLE READ 是 InnoDB 引擎的默认数据库事务隔离级别
2.7 SERIALIZABLE ----- 序列化
- SERIALIZABLE 提供了事务之间最大限度的隔离,在这种隔离级别中,事务一个接一个的按顺序执行,不会发生脏读、不可重复读以及幻读的问题,最安全。
- 如果设置的事务的隔离级别为 SERIALIZABLE ,那么此时开启其他事务时,就会进入阻塞状态,必须等上一个事务提交之后,其他事务才能开启。
- 因为,在上一个事务还没有提交之前,后面的事务就会进入等待,在这样的情况下,前面的 脏读 、不可重复读 、幻读 等问题都不会发生。
转载自:https://juejin.cn/post/7117223873942126629