MySQL 8.0.26
版本。SQL
标准定义的四个隔离级别为:
如下所示,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
首先总结可重读的特性,如下图所示:
众所周知,事务的隔离性是由锁来实现的,当上图中的事务1执行更新语句时,事务1中对数据增加了写锁,但是在事务2中,依旧可以进行读操作,而写锁是排他锁,在事务1中已经添加了写锁的情况下,为什么事务2还可以读呢?这是因为InnoDB
采用了一致性非锁定读机制,通过行多版本控制multi versioning
的方式来读取当前执行时间数据库中行的数据。如果读取的行正在执行DELETE
或UPDATE
操作,这时读取操作不会因此去等待行上锁的释放。相反地,InnoDB
存储引擎会去读取行的一个快照数据。如下图所示:
上图直观地展现了InnoDB
存储引擎一致性的非锁定读。之所以称其为非锁定读,因为不需要等待访问的行上X锁的释放。快照数据是指该行的之前版本的数据,该实现是通过undo
段来完成。而undo
用来在事务中回滚数据,因此快照数据本身是没有额外的开销。此外,读取快照数据是不需要上锁的,因为没有事务需要对历史的数据进行修改操作。
可以看到,非锁定读机制极大地提高了数据库的并发性。在InnoDB
存储引擎的默认设置下,这是默认的读取方式,即读取不会占用和等待表上的锁。但是在不同事务隔离级别下,读取的方式不同,并不是在每个事务隔离级别下都是采用非锁定的一致性读。此外,即使都是使用非锁定的一致性读,但是对于快照数据的定义也各不相同。
在可重读的隔离级别下,可能会出现“幻读”的问题。下面展示一个幻读的示例:
从上图可以看出,在第5步开始,数据已经发生了改变,到第7步时,事务2还是无法看到数据的改变,但是当事务2更新数据以后,发现莫名其妙的多出了一条数据。在同一个事务中,执行两次同样的sql
,第二次执行的sql
会返回之前不存在的行,或者之前出现的数据不见了,这种现象被称之为幻读。
update
语句并没有指定任何条件,相当于更新表中的所有行的对应字段,如果你指定了条件,并且没有更新到”隐藏”的行,那么可能无法看到幻读现象。不同的隔离级别,所引入的问题会有所不同,隔离性也有所不同。串行化SERIALIZABLE
隔离级别就不会出现幻读的问题。接下来我们将探讨事务隔离级别设为串行化时,事务将如何工作。如下所示:
从上图可以看到,当事务1插入一条数据后,事务2中的锁请求超时了。这是因为事务的隔离性是由锁来实现的,当我们使用串行化SERIALIZABLE
的隔离级别时,由于事务1先对players
表施加了写锁,所以当事务2对players
表请求读锁时,会被阻塞,所以在步骤4出现请求锁超时的情况。
在步骤5-6中可以看到,趁着事务2中查询语句被阻塞的时候,在事务1被提交后,事务2中的语句已经查询出结果,从返回结果可以看到,这个查询语句被阻塞了4.82秒,当事务1中的写锁释放后,事务2才读出了数据。
从上述测试可以得知,当事务处于串行化隔离级别时,是不可能出现幻读的情况,因为如果另一个事务对表添加了写锁,那么在当前事务中是无法读取数据的,必须等另一个事务提交后,释放了对表的写锁,当前事务才能进行读操作,所以使用串行化的隔离级别不会出现幻读的情况。但是,当事务隔离级别设为串行化时,数据库失去了并发的能力,所以我们很少将隔离级别设为串行化,因为这种隔离级别过于严格。
接下来我们来看读已提交READ COMMITTED
:
从上图可以看到,在事务1中修改players
表中的数据,如步骤1、2所示,此时事务1未提交,事务2中无法看到事务1中的修改,而当事务1提交后,事务2中即可看到事务1中的修改,换句话说,事务2可以读到事务1提交后的修改,这种隔离级别被称为读已提交READ COMMITTED
。
在读已提交的隔离级别下,也会出现“幻读”的问题,示例如下:
从上图可以看到,事务1插入数据,提交事务后,在事务2中执行两次相同的查询语句,第二次查出的数据多出一行,出现“幻读”的情况。
在读已提交的隔离级别下,除了会出现幻读的情况,还会出现不可重读的情况。不可重读表示不一定可重读。示例如下:
可以看到,在同一个事务中,第7步查出的id
为5的记录name
变为了Haaland
,所以,在事务2中想要再次读到Foden
,就变成了“不可重读”。
不可重读与幻读的表象都非常相似,都是在同一个事务中,并没有操作某些数据,可是这些数据却莫名的被改变了,或者突然多出了某些数据,又或者突然少了某些数据。幻读的重点在于莫名其妙的增加了或减少了某些数据,不可重读的重点在于莫名的情况下,数据被修改更新了。
示例如下:
从上图可以看到,在读未提交的事务隔离级别下,一个事务可以读到另外一个事务中未提交的数据,也就是脏数据。如果读到了脏数据,则显然违反了事务的隔离性。
在不同的事务下,当前事务可以读到另外事务未提交的数据,这种现象称之为脏读,简单来说就是可以读到脏数据。
当事务隔离级别处于读未提交READ UNCOMMITTED
时,会出现脏读的情况,也会出现不可重读、幻读的问题,其并发性能最强,但隔离性与安全性也是最差的。
脏读:当前事务可以读到另外事务未提交的数据。
幻读:幻读和不可重读十分相似,幻读的侧重点在于新增和删除,在同一事务中,使用相同的查询语句,第二次查询时,莫名多出之前不存在的记录,或丢失此前存在的记录。
不可重读:不可重读的侧重点在于更新数据。在同一事务中,查询相同的记录时,同一条记录莫名的发生了改变。
事物的隔离级别越高,隔离性越强,存在的问题越少,并发能力相应越弱:
隔离级别/对应问题 | 脏读 | 不可重读 | 幻读 |
---|---|---|---|
读未提交READ UNCOMMITTED | √ | √ | √ |
读已提交READ COMMITTED | × | √ | √ |
可重读REPEATABLE READ | × | × | √ |
串行化SERIALIZABLE | × | × | × |