MySQL中事务隔离级别
最近在学习数据库的事务隔离级别。在这里整理一下。由于本人水平和写作能力有限,如文中有错误或者表达不清楚的地方,请多包涵。有任何意见或建议,欢迎留言。
我们都知道关系型数据库事务有 ACID
的原则,他们分别代表原子性(Atomicity),一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。我们今天重点了解一下隔离性。(注:本文所有依据都是根据MySQL 5.7.30)
1. 数据库事务并发会操作出现的问题
在详细讨论这几个隔离级别之前,我们有必要先了解一下多个事务并发操作时会出现的异常情况:
dirty read
(脏读。读取到其他事物未提交的数据,这种操作是非常危险的,因为其他事物未提交的数据可能会被回滚或者进一步更新,这种操作不遵循ACID原则。)non-repeatable read
(不可重复读。在查询数据时,同一个事务多次查询本应返回相同的数据,却出现了不一致的情况。MySQL认为,这种操作违背了数据库设计的ACID原则。在事务中,数据应该是一致的,具有可预测和稳定的关系。)phantom read
(幻读。在查询数据时,一个查询在一个事务中运行多次,在这期间有其他的事务插入了新行或更新行,导致多次运行的结果不一致。)
2. MySQL中提供的隔离级别
事务隔离的作用在于多个事务并发操作时,事务之间相互隔离,互不影响。MySQL中提供了全部四种(这四种隔离级别是在SQL 1992标准中定义的)隔离级别:READ-UNCOMMITTED
,READ-COMMITTED
,REPEATABLE-READ
和 SERIALIZABLE
。下面分别介绍一下每一种隔离级别的特点、实现方式以及可能会出现的异常情况。我们可以通过下面的代码查看并临时设置当前连接会话的隔离级别:
mysql> select @@session.tx_isolation; -- 查看当前会话的隔离级别 +------------------------+ | @@session.tx_isolation | +------------------------+ | READ-UNCOMMITTED | +------------------------+ 1 row in set (0.13 sec) mysql> set @@session.tx_isolation=‘READ-UNCOMMITTED‘; --设置隔离级别为 READ-UNCOMMITTED Query OK, 0 rows affected (0.10 sec)
READ-UNCOMMITTED
允许读取其他事务未提交的数据,我们可以通过下面的运行结果看到,事务1把用户的余额减少500,在未提交的情况下,事务2可以查询到数据的更新。这种情况是非常危险的,因为其他事务未提交的数据是不可靠的,如果当前事务拿到了这个数据进行使用,将导致不可预料的后果。所以在该级别下,会出现脏读。
既然该级别下,连其他事务未提交的数据都可以读取,那肯定已提交的数据也是可以读取的,所以也会出现不可重复度。
再看一下,如果在运行过程中,其他事务插入了数据的情况。发现同样也是可以读取的,所以在该级别下,也会出现幻读。
总结,在READ-UNCOMMITTED
级别下,读取的数据是不可靠并且不一致的,会出现脏读,不可重复度和幻读。READ-COMMITTED
先看一下是否能读取到其他事务未提交的数据,发现没有问题。不会出现脏读。
接着看看会不会出现不可重复读的问题呢。从下面的运行结果可以看出,不可重复读的问题,还是存在的。
幻读问题也同样存在。
总结,在READ-COMMITTED
级别下,脏读问题已经被解决,不可重复度和幻读依然存在。REPEATABLE-READ
在MySQL的InnoDB
引擎中,REPEATABLE-READ
是默认的隔离级别。我们分别测试一下上面的三个问题。首先是脏读,我们可以从下面的结果中看到,脏读不会出现。
从这个隔离级别的名字就可以看出,可重复读。那我们来测试一下会不会出现不可重复读的问题呢。下图中的结果表示,不会出现。
再看一下幻读,从下图中的结果可以看到,幻读问题也被解决了。
总结,在REPEATABLE-READ
级别下,脏读、不可重复度和幻读问题都已经被解决。SERIALIZABLE
在SERIALIZABLE
级别下,事务是按照顺序执行的。我们来测试一下是否会出现上面的几个问题,但在修改数据时,直接出错了
得到错误1205 - Lock wait timeout exceeded; try restarting transaction
,等待锁超时。这是因为我们开启了两个事务,从SERIALIZABLE
的字面意思可以看出,串行化,应该是说事务要一个一个的执行,两个事务不能同时执行,但为什么前面的查询语句没问题呢? 我们前面开启了两个事务,同时去查询一条数据是可以的。 这是因为,在该级别下,如果自动提交(autocommit
)是关闭的,InnoDB引擎会隐式的把所有的SELECT
语句转换为SELECT ... LOCK IN SHARE MODE
,我们写的查询语句被转换成了共享锁查询,所以两个查询语句是可以同时运行的,而修改会被阻塞。那么,又有另一个问题,如果自动提交是开启的,查询语句和修改语句可以同时执行吗? MySQL认为,如果自动提交是开启的,那么一条查询语句就在它自己的事务里,因此它肯定是只读的,所以它不会阻塞其他事务。
总结:由于在该模式下,一个(非自动提交的)事务的查询会被隐式的转换为共享锁查询,会阻塞其他事务修改该数据。所以不会出现脏读、不可重复读以及幻读。
3. MySQL中四个隔离级别和三个并发问题的关系
- | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
READ-UNCOMMITTED | ? | ? | ? |
READ-COMMITTED | ? | ? | ? |
REPEATABLE-READ | ? | ? | ? |
SERIALIZABLE | ? | ? | ? |
4. MySQL中事务隔离级别的实现原理
MySQL中提供了一种一致性非锁定读取(Consistent Nonlocking Reads
),一致性读取意味着InnoDB使用多版本控制,查询将指向某个时间点的数据库快照。查询会看到在该时间点之前已提交的更改,但不会看到该时间点之后的更改以及未提交的事务的更改。但是有一个例外情况,在该事务中如果对数据进行了更改,查询操作将可以查看到这些数据的最新版本(可能在该事务的时间点之后,被其他事务修改过),而其他数据保持时间点之前的旧版本,此时,在该事务中,可能会看到该表处于一种从未存在过的状态。
如果事务隔离级别是 REPEATABLE-READ
,那么同一事务中的所有一致性查询都将读取这样的查询第一次查询生成的快照。在进行第一次查询时,InnoDB会给当前事务一个时间点,事务根据这个时间点查询数据库。如果其他事务在该时间点之后进行了数据更新,则当前事务不会看到这些更新。可以通过提交当前事务,然后再发出新的查询,获得最新的快照。
如果事务隔离级别是 READ-COMMITTED
,那么事务中每一次查询,都将设置并读取自己最新的快照。
一致性读取,是InnoDB在 READ-COMMITTED
和 REPEATABLE-READ
级别下处理普通查询(没有加锁的查询)的默认模式。一致性读取不会对其访问的表设置任何锁,因此,在对表进行一致性读取时,其他事务可以自由的修改这些表。
下面我们分析一下 REPEATABLE-READ
和 READ-COMMITTED
的实现原理。由于 READ-UNCOMMITTED
和 READ-COMMITTED
的区别只是在于能否读取到其他事务未提交的更改,所以这两个级别是类似的。SERIALIZABLE
在前面已经说过了,所以这里不再赘述。
REPEATABLE-READ的实现原理
在该级别下,普通查询会使用一致性查询,同一个事务中的后续查询都将读取第一次查询时建立的快照。所以可以保证同一事物中的查询的一致性。
如果使用锁定查询(SELECT ... FOR UPDATE
或者 SELECT ... LOCK IN SHARE MODE
)和 UPDATE
语句、 DELETE
语句,锁定取决于该查询是否有唯一搜索条件的唯一索引,还是使用范围类型的搜索条件。
- 存在唯一索引
InnoDB只锁定找到的索引记录。 - 范围类型的条件
InnoDB锁定扫描到的索引范围。
READ-COMMITTED的实现原理
在该级别下,和 REPEATABLE-READ
一样,普通查询会使用一致性查询。不同的是,同一个事务中,所有的一致性查询都会设置并读取新的快照。这样可以让事务在每一次读取时,都可以查看到其他事物已经提交的更改。但同样也带来了不可重复读和幻读的问题。
使用锁定查询和 UPDATE
语句、DELETE
语句,锁定的范围和 REPEATABLE-READ
一致,不再赘述。
5. 其他问题
现在我们都知道在 READ-UNCOMMITTED
级别下,能够读取其他事务未提交的数据。那么能够对这些数据进行更改从而导致数据不一致吗?看下图的运行结果
这里我们得到了一个错误 1205 - Lock wait timeout exceeded; try restarting transaction
,从错误信息看,是在等待锁的过程中超时了。可以看出,在该级别下,MySQL也是使用了锁的。虽然在这种模式下可以读取到其他事务未提交的数据,但不允许并发修改同一条数据。READ-UNCOMMITTED
除了可以读取其他事务未提交的数据之外,其他的工作方式与 READ-COMMITTED
类似,而在 READ-COMMITTED
级别下,使用锁定查询、UPDATE
语句、DELETE
语句,都是会锁定数据的。
参考资料:
https://dev.mysql.com/doc/refman/5.7/en/glossary.html
https://dev.mysql.com/doc/refman/5.7/en/innodb-transaction-isolation-levels.html