TrumanWong

MySQL InnoDB事务隔离级别总结

TrumanWong
5/25/2023

本文基于MySQL 8.0.26版本。

概述

SQL标准定义的四个隔离级别为:

  • READ UNCOMMITTED:读未提交
  • READ COMMITTED:读已提交
  • REPEATABLE READ:可重读
  • SERIALIZABLE:串行化

如下所示,InnoDB存储引擎默认支持的隔离级别是REPEATABLE READ

mysql> show variables like 'transaction_isolation';
+-----------------------+-----------------+
| Variable_name         | Value           |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+
1 row in set (0.00 sec)

与标准SQL不同的是,InnoDB存储引擎在REPEATABLE READ事务隔离级别下,使用Next-Key Lock锁的算法,因此避免幻读的产生。这与其他数据库系统(如Microsoft SQL Server数据库)是不同的。所以说,InnoDB存储引擎在默认的REPEATABLE READ的事务隔离级别下已经能完全保证事务的隔离性要求,即达到SQL标准的SERIALIZABLE隔离级别。

隔离级别越低,事务请求的锁越少或保持锁的时间就越短。这也是为什么大多数数据库系统默认的事务隔离级别是READ COMMITTED

隔离级别

REPEATABLE READ

首先总结可重读的特性,如下图所示:

TrumanWong

  1. 在会话1和会话2中同事开启2个事务
  2. 在事务1中修改players表的记录后(将id为1的name值修改为lionel messi
  3. 在事务2中看到的仍是事务1修改前的记录,即使事务1提交后,在事务2提交前,会话2中看到的数据依然没有任何变化。
  4. 不管事务1是否提交,在事务2没有提交之前,这条数据对事务2来说一直都是没有发生改变的,这条数据在事务2中是可以重复的被读到,所以,这种隔离级别被称为可重读REPEATABLE READ

一致性非锁定读

众所周知,事务的隔离性是由锁来实现的,当上图中的事务1执行更新语句时,事务1中对数据增加了写锁,但是在事务2中,依旧可以进行读操作,而写锁是排他锁,在事务1中已经添加了写锁的情况下,为什么事务2还可以读呢?这是因为InnoDB采用了一致性非锁定读机制,通过行多版本控制multi versioning的方式来读取当前执行时间数据库中行的数据。如果读取的行正在执行DELETEUPDATE操作,这时读取操作不会因此去等待行上锁的释放。相反地,InnoDB存储引擎会去读取行的一个快照数据。如下图所示:

TrumanWong

上图直观地展现了InnoDB存储引擎一致性的非锁定读。之所以称其为非锁定读,因为不需要等待访问的行上X锁的释放。快照数据是指该行的之前版本的数据,该实现是通过undo段来完成。而undo用来在事务中回滚数据,因此快照数据本身是没有额外的开销。此外,读取快照数据是不需要上锁的,因为没有事务需要对历史的数据进行修改操作。

可以看到,非锁定读机制极大地提高了数据库的并发性。在InnoDB存储引擎的默认设置下,这是默认的读取方式,即读取不会占用和等待表上的锁。但是在不同事务隔离级别下,读取的方式不同,并不是在每个事务隔离级别下都是采用非锁定的一致性读。此外,即使都是使用非锁定的一致性读,但是对于快照数据的定义也各不相同。

幻读

在可重读的隔离级别下,可能会出现“幻读”的问题。下面展示一个幻读的示例:

TrumanWong

从上图可以看出,在第5步开始,数据已经发生了改变,到第7步时,事务2还是无法看到数据的改变,但是当事务2更新数据以后,发现莫名其妙的多出了一条数据。在同一个事务中,执行两次同样的sql,第二次执行的sql会返回之前不存在的行,或者之前出现的数据不见了,这种现象被称之为幻读

注意:上例中第8步执行的update语句并没有指定任何条件,相当于更新表中的所有行的对应字段,如果你指定了条件,并且没有更新到”隐藏”的行,那么可能无法看到幻读现象。

SERIALIZABLE

不同的隔离级别,所引入的问题会有所不同,隔离性也有所不同。串行化SERIALIZABLE隔离级别就不会出现幻读的问题。接下来我们将探讨事务隔离级别设为串行化时,事务将如何工作。如下所示:

TrumanWong

从上图可以看到,当事务1插入一条数据后,事务2中的锁请求超时了。这是因为事务的隔离性是由锁来实现的,当我们使用串行化SERIALIZABLE的隔离级别时,由于事务1先对players表施加了写锁,所以当事务2对players表请求读锁时,会被阻塞,所以在步骤4出现请求锁超时的情况。

在步骤5-6中可以看到,趁着事务2中查询语句被阻塞的时候,在事务1被提交后,事务2中的语句已经查询出结果,从返回结果可以看到,这个查询语句被阻塞了4.82秒,当事务1中的写锁释放后,事务2才读出了数据。

从上述测试可以得知,当事务处于串行化隔离级别时,是不可能出现幻读的情况,因为如果另一个事务对表添加了写锁,那么在当前事务中是无法读取数据的,必须等另一个事务提交后,释放了对表的写锁,当前事务才能进行读操作,所以使用串行化的隔离级别不会出现幻读的情况。但是,当事务隔离级别设为串行化时,数据库失去了并发的能力,所以我们很少将隔离级别设为串行化,因为这种隔离级别过于严格。

READ COMMITTED

接下来我们来看读已提交READ COMMITTED:

TrumanWong

从上图可以看到,在事务1中修改players表中的数据,如步骤1、2所示,此时事务1未提交,事务2中无法看到事务1中的修改,而当事务1提交后,事务2中即可看到事务1中的修改,换句话说,事务2可以读到事务1提交后的修改,这种隔离级别被称为读已提交READ COMMITTED

在读已提交的隔离级别下,也会出现“幻读”的问题,示例如下:

TrumanWong

从上图可以看到,事务1插入数据,提交事务后,在事务2中执行两次相同的查询语句,第二次查出的数据多出一行,出现“幻读”的情况。

在读已提交的隔离级别下,除了会出现幻读的情况,还会出现不可重读的情况。不可重读表示不一定可重读。示例如下:

TrumanWong

可以看到,在同一个事务中,第7步查出的id为5的记录name变为了Haaland,所以,在事务2中想要再次读到Foden,就变成了“不可重读”。

不可重读幻读的表象都非常相似,都是在同一个事务中,并没有操作某些数据,可是这些数据却莫名的被改变了,或者突然多出了某些数据,又或者突然少了某些数据。幻读的重点在于莫名其妙的增加了或减少了某些数据,不可重读的重点在于莫名的情况下,数据被修改更新了。

READ UNCOMMITTED

示例如下:

TrumanWong

从上图可以看到,在读未提交的事务隔离级别下,一个事务可以读到另外一个事务中未提交的数据,也就是脏数据。如果读到了脏数据,则显然违反了事务的隔离性。

在不同的事务下,当前事务可以读到另外事务未提交的数据,这种现象称之为脏读,简单来说就是可以读到脏数据。

当事务隔离级别处于读未提交READ UNCOMMITTED时,会出现脏读的情况,也会出现不可重读、幻读的问题,其并发性能最强,但隔离性与安全性也是最差的。

总结

脏读、幻读、不可重读的区别

脏读:当前事务可以读到另外事务未提交的数据。

幻读:幻读和不可重读十分相似,幻读的侧重点在于新增和删除,在同一事务中,使用相同的查询语句,第二次查询时,莫名多出之前不存在的记录,或丢失此前存在的记录。

不可重读:不可重读的侧重点在于更新数据。在同一事务中,查询相同的记录时,同一条记录莫名的发生了改变。

不同隔离级别存在的问题

事物的隔离级别越高,隔离性越强,存在的问题越少,并发能力相应越弱:

隔离级别/对应问题 脏读 不可重读 幻读
读未提交READ UNCOMMITTED
读已提交READ COMMITTED ×
可重读REPEATABLE READ × ×
串行化SERIALIZABLE × × ×