一篇文章读懂 MySQL 事务中的四种隔离级别,以及脏读、不可重复读、幻读详解

1,241 阅读11分钟

一. 介绍

1.1 MySQL 事务的四种隔离级别

MySQL 中事务的隔离级别一共分为四种:

序列化(SERIALIZABLE)

可重复读(REPEATABLE READ)

提交读(READ COMMITTED)

未提交读(READ UNCOMMITTED)

1.2 含义

  1. SERIALIZABLE ----- 序列化

    当隔离级别为序列化时,用户之间通过一个接着一个的执行当前事务,不能并发执行。这种隔离级别提供了事务之间最大限度的隔离

  2. REPEATABLE READ ----- 可重复读

    这是 MySQL 数据库中的默认事务隔离级别。

    可重复读是指在同一个事务内,多次读取同一个数据,在可重复读的这一级别上,事务不会被看成一个序列。

    不过,当前正在执行事务的变化仍然不能被外部看到,也就是说,如果用户在当前事务对数据进行了更改,然后执行 SELECT 语句(显然此时查询的是更改之后的数据),同时在另一个事务中执行了同一条 SELECT 语句多次,结果总是与更改之前的数据是相同的。

    因为正在执行的事务所产生的数据变化不能被外部看到。

  3. READ COMMITTED ----- 提交读

    READ COMMITTED 隔离级别的安全性要比 REPEATABLE READ 隔离级别的安全性要差。

    READ COMMITTED 隔离级别的事务可以看到其他事务对数据的修改,也就是说,在事务处理期间,如果其他事务修改了相应的表,那么同一个事务的多个 SELECT 语句可能返回不同的结果。

  4. READ UNCOMMITTED ----- 未提交读

    READ UNCOMMITTED 提供了事务之间最小限度的隔离。

    该事务隔离界别除了容易产生幻读和不能重复读之后,处于该级别的事务还可以读到其他事务还没有提交的数据。

    如果这个事务使用其他事务不提交的变化作为计算基础,然后那些未提交的变化被它们的父事务撤销,这就导致了大量的数据变化了。

二. SQL 演示

2.1 查看隔离级别

  1. 在 MySQL 8 之前使用如下命令查看隔离级别

    # 这是 MySQL 8 之前查看隔离级别的方式
    SELECT @@GLOBAL.tx_isolation, @@tx_isolation;
    
  2. 在 MySQL 8 之后,查看事务的隔离级别则是(其实就是关键字变了,其他的都样):

    # 这是 MySQL 8 之后查看隔离级别的方式
    # GLOBAL 表示查看全局的隔离级别;后面的表示查看当前会话的隔离级别
    SELECT @@GLOBAL.transaction_isolation,@@transaction_isolation
    

    因为我用的是 MySQL 8 的,所以在这里只演示 8 的版本了。

    运行之后,就会看到如下效果:默认的隔离级别是 REPEATABLE-READ ,全局的隔离级别和当前会话的隔离级别都是这个。 查看MySQL的隔离级别

2.2 修改隔离级别

  1. 通过下面的这条命名,就可以修改隔离级别了。但需要注意的是,这里修改的是修改当前 session 隔离级别,不用修改全局隔离级别。

    #修改当前事务的隔离级别为 read uncommitted,在这种隔离级别中,可以读到别的事务未提交的数据
    SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
    

    当然,如果想要设置全局的,就可以像下面这样。这里只演示一次,后面就不作演示了。其实就是 session 当前会话global 全局 的区别罢了。

    # 这是设置全局的隔离级别
     SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
    
  2. 继续上面的,在把会话的隔离级别修改之后,就可以再次查看事务的隔离级别了,此时当前 session (也就是当前会话) 的隔离级别已经发生改变了 修改事务的隔离级别

  3. 注意,如果只修改当前会话的隔离级别,当我们换了一个会话的时候,隔离级别就又恢复到默认的隔离级别了,即 REPEATABLE-READ 。所以,我们测试时,只修改当前 session 的隔离级别就可以了。

2.3 脏读、不可重复读、幻读

在正式开始演示之前,先来大概的了解一下什么是脏读、不可重复读、幻读。

  • 脏读 :一个事务读取到另一个事务还没有提交的数据

  • 不可重复读 :在一个事务中多次读取同一个数据时,结果出现不一致

  • 幻读 :在一个事务中,使用相同的 SQL 两次读取,第二次读取到其他事务新插入的行

    不可重复读注重于数据的修改,而幻读注重于数据的插入

隔离级别脏读不可重复读幻读
未提交读:READ UNCOMMITTED
提交读:READ COMMITTED
可重复读(默认):REPEATABLE READ
序列化:SERIALIZABLE

2.4 READ UNCOMMITTED ----- 未提交读

READ UNCOMMITTED 是最低的隔离级别,这种级别中存在着 脏读不可重复读幻读 的问题。

  1. 首先,在数据库中先创建一个简单的表,用于测试。表很简单,就两个字段,如下: 创建表

  2. 接着,假设这里有两个用户,一个是 peng,一个是 qiqi,两个用户分别有 200 元。

  3. 一会儿的场景是,在一个事务中对两个用户的 money 字段进行数据上的操作;然后在另一个事务中也对该字段的数据进行操作。在此基础上,分别观察两者之间的区别。

注意 : 我这里测试用的是 Navicat 。在 Navicat 中不同的查询窗口就是不同的会话;如果使用的是 SQLyog 的话,不同的查询窗口对应的是同一个会话,所以 SQLyog 需要再开启一个新的连接,注意!是新的连接,在新的连接中进行查询操作。

2.4.1 脏读

  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;
    
  2. 在 会话B 中修改 MySQL 中默认的事务隔离级别为:READ UNCOMMITTED

    # 把事务的隔离级别修改为:READ UNCOMMITTED
    SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
    
  3. 接着在 会话B 中输入以下 SQL ,然后运行第一行,开启事务B(注意,这里只需要执行一条语句,开启事务B即可)

    # 开启事务B
    START TRANSACTION;
    # 查询表 bank
    SELECT * FROM bank;
    # 提交事务
    COMMIT;
    
  4. 然后回到 会话A 中开启事务,并执行前两条 SQL 语句,给 peng 用户转账 50 元(注意,这里是执行前三条 SQL,执行后不提交)

    此时就实现了在两个不同的会话中,去测试事务的隔离级别的不同了。后面的其他几个隔离级别也是在此基础上去测试的。

  5. 然后再回到 会话B 中,执行第二条语句,查询表 bank,结果如下: 脏读的结果

    你会发现,会话A 中的事务还没有结束,是还没有提交的,但是在 会话B 中已经能够查询到 bank 表的变化了。这就是 脏读

2.4.2 不可重复读

  1. 有了上面演示的铺垫之后,这里就稍微的简略一些了。首先,将 peng 和 qiqi 的 money 字段的值都恢复成原来的 200。

  2. 在 会话B 中输入以下 SQL ,并且执行前两条:开启事务以及查询

    # 开启事务B
    START TRANSACTION;
    # 查询表 bank,并且 name = peng 的内容
    SELECT * FROM bank where username = 'peng';
    # 提交事务
    COMMIT;
    

    执行之后的结果: 不可重复读1

  3. 接着,在 会话A 中输入以下 SQL,并且执行并提交。

    # 开启事务A
    START TRANSACTION;
    # 给 peng 增加 50
    UPDATE bank SET money=money + 50 WHERE username = 'peng';
    # 提交事务
    COMMIT;
    
  4. 然后回到 会话B,再次执行第二条 SQL 语句,查询结果: 不可重复读2

    会话A 是已经提交的,而会话B 是还没有提交的状态,还在事务中的 会话B 读取到了已经修改并提交的数据,即前后两次查询,结果不一致,这就是 不可重复读

脏读 与 不可重复读 的区别:脏读是看到了其他事务未提交的数据,而不可重复读是看到了其他事务已经提交了的数据(由于 会话B 也是在事务中的,因此有可能并不想看到其他事务已经提交的数据)

2.4.3 幻读

幻读 与 不可重复读 非常像

  1. 在 会话A 中输入以下 SQL

    # 开启事务A
    START TRANSACTION;
    # 添加一条记录
    INSERT INTO bank(username,money) VALUES('zhangsan',300);
    # 提交事务
    COMMIT;
    
  2. 接着在 会话B 中输入以下 SQL ,并且执行前两条:开始事务并查询

    # 开启事务B
    START TRANSACTION;
    # 查询 bank 表
    SELECT * FROM bank;
    # 删除 bank 表中 name = zhangsan 的字段
    DELETE FROM bank WHERE username = 'zhangsan'
    # 提交事务
    COMMIT;
    

    查询的结果如下: 幻读1

  3. 此时表中是只有 peng 和 qiqi 的,接着执行 会话A 中的前条 SQL:开启事务和添加一条记录。

  4. 然后再次执行一次 会话B 中的第二条 SQL 语句,查询 bank 表(注意!不要又一次执行 START TRANSACTION 开启事务了,只需要执行第二条 SQL 查询即可)。因为 脏读 的原因,此时是可以查询到添加之后的记录的: 幻读2

  5. 接着执行 会话B 的第三条 SQL 语句,即删除 name = zhangsan 的字段,这个时候删除就会出现问题。虽然在 会话B 中能够查询到 zhangsan,但是该条记录是还没有提交的,是因为的 脏读 的原因才看到的,所以是无法删除的。此时就产生了幻觉,明明有 zhangsan,却无法删除,这个就是 幻读

2.5 READ COMMITTED ----- 提交读

和 READ UNCOMMITTED 相比,READ COMMITTED 主要解决了 脏读 的问题,对于 不可重复读幻读 则没有解决

  1. 将事务的隔离级别该为 READ COMMITTED 之后,重复上面的操作,可以发现,关于 脏读 的问题已经不在存在了,而 不可重复读 依然还是存在的。

  2. 至于 幻读 ,在上面的测试中的幻读例子,由于 脏读 在这里已经解决了,所以上面的例子不舍用在当前的测试中。

  3. 我们来看一个新的例子。首先需要把 会话B 的隔离级别改为 READ COMMITTED

    # 隔离级别修改为 READ COMMITTED
    SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED
    
  4. 在 会话A 中输入以下 SQL(这里说一下,这里的数据都是手动恢复成最初的数据,即 peng 和 qiqi 都为 200):

    # 开启事务A
    START TRANSACTION;
    # 添加一条记录
    INSERT INTO bank(username,money) VALUES('zhangsan',300);
    # 提交事务
    COMMIT;
    
  5. 接着在 会话B 中输入以下 SQL,并且运行前两条 SQL,这里的查询结果与前面的是一样的,所以这里就不啰嗦了。

    # 开启事务B
    START TRANSACTION;
    # 查询 bank 表
    SELECT * FROM bank;
    # 这里也添加一条记录
    INSERT INTO bank(username,money) VALUES('zhangsan',300)
    # 提交事务
    COMMIT;
    
  6. 接着在 会话A 中执行前两条 SQL 语句:开启事务以及添加一条记录。注意,只运行前两条 SQL,不提交!

  7. 然后在 会话B 中执行第二条 SQL ,即继续查询。此时因为已经不存在 脏读 问题,所以这里的查询是查不到 会话A 中添加的数据的。

  8. 接着在 会话B 中执行第三条 SQL 语句,也添加一条记录。由于 username 字段是唯一的,因为这里也无法插入。此时就会产生幻觉,明明没有 zhangsan 这个用户,却无法插入 zhangsan 。所以,这里也存在着 幻读

2.6 REPEATABLE READ ----- 可重复读

和 READ COMMITTED 相比,REPEATABLE READ 进一步解决了不可重复读的问题,但是幻读则未解决。

  1. REPEATABLE READ 中关于幻读的测试与上面大同小异,不一样的是,在 会话A 中添加一条记录的时候,需要提交事务。
  2. 因为 REPEATABLE READ 已经解决了不可重复读的问题,因此在 会话A 中提交了事务,在 会话B 中的查询也是查不到数据的。但当 会话B 也添加一条记录的时候,就会出错,这里就出现了幻读现象!
  3. 注意,REPEATABLE READ 是 InnoDB 引擎的默认数据库事务隔离级别

2.7 SERIALIZABLE ----- 序列化

  1. SERIALIZABLE 提供了事务之间最大限度的隔离,在这种隔离级别中,事务一个接一个的按顺序执行,不会发生脏读、不可重复读以及幻读的问题,最安全。
  2. 如果设置的事务的隔离级别为 SERIALIZABLE ,那么此时开启其他事务时,就会进入阻塞状态,必须等上一个事务提交之后,其他事务才能开启。
  3. 因为,在上一个事务还没有提交之前,后面的事务就会进入等待,在这样的情况下,前面的 脏读不可重复读幻读 等问题都不会发生。