概述
MySQL 的事务提供了 ACID 四个特性,其中隔离性是较复杂的一个特性。SQL 标准定义了四种隔离级别,每一种隔离级别都规定了事务中修改对于其他事务的可见性。一般来说,较低的隔离通常可以带来更高的并发。四种隔离级别分别是: 未提交读(READ UNCOMMITTED),已提交读(READ COMMITTED)/不可重复读(NONREPEATABLE READ),可重复读(REPEATABLE READ),可串行化(SERIALIZABLE)。下面的说明仅对 InnoDB 引擎保证准确。
四种隔离级别
关于四种隔离级别,在《高性能 MySQL> 中已经有了很好的阐述,这里简单地陈述出来。
未提交读
在未提交读级别,事务中的修改,即使没有提交,对于其他事务也都是可见的。事务可以读取未提交的数据,这也称为脏读(Dirty Read)。很明显,未提交读等同于未做任何的事务隔离,因此是最低的隔离级别。一般情况下,也没有什么可用的场景。
已提交读
已提交读是相对于未提交读来说的,主要是实现了在事务中未提交的修改,对于其他事务是不可见的。但是这种隔离级别会造成一个问题,在一次事务中多次读取会出现不一样的结果,所以也称为不可重复读。想要更轻松的理解不可重复读,可以看下面的例子:
事务A期间,事务B提交了一次更新,这会导致事务A中两次查询 id 为 1 的数据,第一次 score 是 89,第二次 score 是 29。我们也可以用 sql 语句来验证一下:
mysql> show create table score;
+-------+--------------------------------------------------------------------+
| Table | Create Table |
+-------+--------------------------------------------------------------------+
| score | CREATE TABLE `score` ( |
| | `id` int(11) NOT NULL AUTO_INCREMENT, |
| | `name` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL, |
| | `score` int(11) NOT NULL, |
| | PRIMARY KEY (`id`) |
| | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci |
+-------+--------------------------------------------------------------------+
终端 A :
mysql> set autocommit=0;
mysql> set session transaction isolation level read committed;
mysql> begin;
mysql> select * from score where id = 1;
+----+------+-------+
| id | name | score |
+----+------+-------+
| 1 | Bob | 89 |
+----+------+-------+
终端 B :
mysql> set autocommit=0;
mysql> set session transaction isolation level read committed;
mysql> begin;
mysql> update score set score=29 where id = 1;
mysql> commit;
终端 A :
mysql> select * from score where id = 1;
+----+------+-------+
| id | name | score |
+----+------+-------+
| 1 | Bob | 29 |
+----+------+-------+
mysql> commit;
可重复读
可重复读是 MySQL 的默认隔离界别,保证了在同一个事务中多次读取同样的记录结果是一致的。但是在理论上,可重复读隔离级别还是无法解决另外一个幻读(Phantom Read)的问题。所谓幻读,指的是当某个事务在读取某个范围内的记录时,会产生换行(Phantom Row)。InnoDB 通过多版本控制(MVCC,Multiversion Concurreny Control)解决了幻读的问题。但是对于可重复读,有一个让我比较不确定的场景:
按道理说,在事务 A 内 score 应该是一致的,事务 B 提交的 score 是不可见的,也就是 89,所以 id 为 1 的数据应该是:
+----+------+-------+
| id | name | score |
+----+------+-------+
| 1 | Alice| 29 |
+----+------+-------+
事实真的如此吗?我们用 MySQL 验证一下:
终端 A:
mysql> set session transaction isolation level repeatable read;
mysql> begin;
mysql> select * from score where id = 1;
终端 B:
mysql> set session transaction isolation level repeatable read;
mysql> begin;
mysql> update score set score=29 where id = 1;
mysql> commit
终端 A:
mysql> update score set name='Alice' where score=89 and id = 1;
mysql> commit;
mysql> select * from score where id = 1;
+----+------+-------+
| id | name | score |
+----+------+-------+
| 1 | Bob | 29 |
+----+------+-------+
发现我的想法是错的。所以如何正确理解这里所说的可重复读呢?应该是仅指在 SELECT 的时候,因为 MySQL 事务的可重复读隔离级别下,会使用 MVCC,此时 SELECT 只查找版本早于当前事务版本的数据行,所以保证了一次事务内的可重复读。而 INSERT、DELETE、UPDATE 均会使用当前系统版本号的最新数据。
可串行化
可串行化是最高的隔离级别。它通过强制事务串行执行,避免了前面说的幻读问题。简单来说,SERIALIZABLE 会在读取的每一行数据都加锁,所以可能导致大量的超时和锁争用问题。实际应用中也很少用到这个隔离级别。