跳转至

隔离级别

隔离级别简介

对于两个并发执行的事务,如果涉及到操作同一条记录的时候,可能会发生问题。因为并发操作会带来数据的不一致性,包括脏读、不可重复读、幻读等。数据库系统提供了隔离级别来让我们有针对性地选择事务的隔离级别,避免数据不一致的问题。

并发事务带来的问题:

  1. 脏读(Dirty read)
  2. 丢失修改(Lost to modify)
  3. 不可重复读(Unrepeatableread)
  4. 幻读(Phantom read)

SQL 标准定义了 4 种隔离级别,简介:

  • READ-UNCOMMITTED(读取未提交):最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
  • READ-COMMITTED(读取已提交):允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
  • REPEATABLE-READ(可重复读):对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
  • SERIALIZABLE(可串行化):最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。

分别对应可能出现的数据不一致的情况:

Isolation Level 脏读(Dirty Read) 不可重复读(Non Repeatable Read) 幻读(Phantom Read) Update Insert / Delete 备注
Read Uncommitted Yes Yes Yes Yes Yes
Read Committed - Yes Yes Yes Yes
Repeatable Read - - Yes Yes Yes Mysql 默认隔离界别,也可以防止幻读
Serializable - - - - - 所有事务按顺序进行,可以防止丢失修改

如何防止丢失修改:

  1. SERIALIZABLE:设置该隔离级别会使 CRUD 所有操作影响的行数据带上锁(其它隔离级别只有 update 操作才会上锁),其它事务必须等待锁释放才可继续执行
  2. 写锁:直接使用 update table set xx=xx+1
  3. 悲观锁:在查询语句后面加上 for update,适合 update 多于 select 操作
  4. 乐观锁:在表中维护 1 个 version 字段,每次更新 +1,如果该条记录 version 与查询时不一致则回滚当前操作避免丢失修改,适合 select 多于 update 操作

默认隔离级别

如果没有指定隔离级别,数据库就会使用默认的隔离级别。在 MySQL 中,如果使用 InnoDB,默认的隔离级别是 Repeatable Read。

MySQL8 中隔离级别的变量跟之前的版本不一样,之前是 tx_isolation,MySQL8 改成了 transaction_isolation。查看当前隔离级别的命令是

mysql> select @@global.transaction_isolation,@@transaction_isolation;

+--------------------------------+-------------------------+
| @@global.transaction_isolation | @@transaction_isolation |
+--------------------------------+-------------------------+
| REPEATABLE-READ                | REPEATABLE-READ         |
+--------------------------------+-------------------------+
1 row in set (0.01 sec)

mysql 的默认隔离级别是可重复读,但其实对高并发业务来说,可重复读并不是最合适的,最合适的是读提交,主要是因为 MySQL 5.0 之前,MySQL 的主从复制在度提交这个隔离级别下是有 bug 的。

修改全局隔离级别为读提交:

mysql> SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;

修改会话隔离级别:

mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

四种隔离级别

首先,我们准备好 students 表的数据,该表仅一行记录:

mysql> SELECT * FROM students;
+----+-------+
| id | name  |
+----+-------+
|  1 | Alice |
+----+-------+
1 row in set (0.00 sec)

创建数据库并建一张测试表:

CREATE DATABASE test_trans;
USE test_trans;

CREATE TABLE students (
  id INT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(50)
);

INSERT INTO students (name) VALUES ('Alice');

Read Uncommitted

Read Uncommitted 是隔离级别最低的一种事务级别。

在这种隔离级别下,一个事务会读到另一个事务更新后但未提交的数据,如果另一个事务回滚,那么当前事务读到的数据就是脏数据,这就是脏读(Dirty Read)。

分别开启两个 MySQL 客户端连接,按顺序依次执行事务 A 和事务 B:

时刻 事务A 事务B
1 SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
2 BEGIN; BEGIN;
3 SELECT * FROM students WHERE id = 1; -- Alice
4 UPDATE students SET name = 'Bob' WHERE id = 1;
5 SELECT * FROM students WHERE id = 1; -- Bob
6 ROLLBACK;
7 SELECT * FROM students WHERE id = 1; -- Alice
8 COMMIT;

当事务 A 执行完第 4 步时,它更新了 id=1 的记录,但并未提交,而事务 B 在第 5 步读取到的数据就是未提交的数据。

随后,事务 A 在第 6 步进行了回滚,事务 B 再次读取 id=1 的记录,发现和上一次读取到的数据不一致,这就是脏读。

可见,在 Read Uncommitted 隔离级别下,一个事务可能读取到另一个事务更新但未提交的数据,这个数据有可能是脏数据。

Read Committed

在 Read Committed 隔离级别下,一个事务可能会遇到不可重复读(Non Repeatable Read)的问题。

不可重复读是指,在一个事务内,多次读同一数据,在这个事务还没有结束时,如果另一个事务恰好修改了这个数据,那么,在第一个事务中,两次读取的数据就可能不一致。

分别开启两个 MySQL 客户端连接,按顺序依次执行事务 A 和事务 B:

时刻 事务A 事务B
1 SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
2 BEGIN; BEGIN;
3 SELECT * FROM students WHERE id = 1; -- Alice
4 UPDATE students SET name = 'Bob' WHERE id = 1;
5 SELECT * FROM students WHERE id = 1; -- Alice
6 COMMIT;
7 SELECT * FROM students WHERE id = 1; -- Bob
8 COMMIT;

当事务 B 第一次执行第 3 步的查询时,得到的结果是 Alice,随后,由于事务 A 在第 4 步更新了这条记录并提交,所以,事务 B 在第 7 步再次执行同样的查询时,得到的结果就变成了 Bob,因此,在Read Committed 隔离级别下,事务不可重复读同一条记录,因为很可能读到的结果不一致。

Repeatable Read(??)

在 Repeatable Read 隔离级别下,一个事务可能会遇到幻读(Phantom Read)的问题。

幻读是指,在一个事务中,第一次查询某条记录,发现没有,但是,当试图更新这条不存在的记录时,竟然能成功,并且,再次读取同一条记录,它就神奇地出现了。

分别开启两个 MySQL 客户端连接,按顺序依次执行事务 A 和事务 B:

时刻 事务A 事务B
1 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
2 BEGIN; BEGIN;
3 SELECT * FROM students WHERE id = 99; -- empty
4 INSERT INTO students (id, name) VALUES (99, 'Bob');
5 SELECT * FROM students WHERE id = 99; -- empty
6 COMMIT;
6 SELECT * FROM students WHERE id = 99; -- empty
7 UPDATE students SET name = 'Alice' WHERE id = 99; -- 1 row affected
8 SELECT * FROM students WHERE id = 99; -- Alice
9 COMMIT;

事务 B 在第 3 步第一次读取 id=99 的记录时,读到的记录为空,说明不存在 id=99 的记录。随后,事务 A 在第 4 步插入了一条 id=99 的记录并提交。

事务 B 在第 6 步再次读取 id=99 的记录时,读到的记录仍然为空,但是,事务 B 在第 7 步试图更新这条不存在的记录时,竟然成功了,并且,事务 B 在第 8 步再次读取 id=99 的记录时,记录出现了。

可见,幻读就是没有读到的记录,以为不存在,但其实是可以更新成功的,并且,更新成功后,再次读取,就出现了。

Serializable

Serializable 是最严格的隔离级别。在 Serializable 隔离级别下,所有事务按照次序依次执行,因此,脏读、不可重复读、幻读都不会出现。

虽然 Serializable 隔离级别下的事务具有最高的安全性,但是,由于事务是串行执行,所以效率会大大下降,应用程序的性能会急剧降低。如果没有特别重要的情景,一般都不会使用 Serializable 隔离级别。