事务隔离级别

本文简单介绍一下数据库的事务隔离级别,所用数据库采用 MySQLMySQL 提供了两种事务性的存储引擎 InnoDBNDB Cluster,本文示例采用 InnoDB 存储引擎。

查看当前数据的事务隔离级别

1
2
3
4
5
6
7
8
-- 查看当前数据库事务隔离级别
mysql> SHOW VARIABLES LIKE '%transaction_isolation%';
-- 或者
mysql> SELECT @@transaction_isolation;
-- 低版本的 MySQL 查看事务隔离级别的属性是不一样的
mysql> SHOW VARIABLES LIKE '%tx_isolation%';
-- 或者
mysql> SELECT @@tx_isolation;

不同的事务隔离级别所面对的问题

事务隔离级别 脏读 不可重复读 幻读
读未提交(read-uncommitted)
读已提交(read-committed)
可重复读(repeatable-read)
串行化(serializable)
  • 脏读:事务 A 读取了事务 B 更新的数据,然后 B 回滚操作,那么 A 读取到的数据是脏数据。

  • 不可重复读:事务 A 多次读取同一数据,事务 B 在事务 A 多次读取的过程中,对数据作了更新并提交,导致事务 A 多次读取同一数据时,结果不一致。

  • 幻读:系统管理员 A 将数据库中所有学生的成绩从具体分数改为 ABCDE 等级,但是系统管理员 B 就在这个时候插入了一条具体分数的记录,当系统管理员 A 改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。

小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表。

脏读的场景

客户端 A 先开启事务,查询用户余额

1
2
3
4
5
6
7
8
9
10
11
12
-- 客户端A设置当前会话级别为`read-uncommitted`
mysq> SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
-- 客户端A开启事务
mysq> START TRANSACTION;
-- 客户端A查询用户余额
mysq> SELECT * FROM wallet WHERE user_id = 1;
-- 结果如下:
# +----+---------+-----------+
# | id | user_id | balance |
# +----+---------+-----------+
# | 1 | 1 | 100000000 |
# +----+---------+-----------+

客户端 B 开启事务,并修改用户余额

1
2
3
4
5
6
-- 客户端B设置当前会话级别为`read-uncommitted`
mysq> SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
-- 客户端B开启事务
mysq> START TRANSACTION;
-- 客户端B修改用户余额,
mysql> UPDATE wallet SET balance = balance + 100 WHERE user_id = 1;

客户端 A 查询用户余额

1
2
3
4
5
6
7
8
-- 客户端A获取用户余额(此时由于B的事务并未提交,故A事务读到了B事务已经更新后的数据,此时发生脏读)
mysq> SELECT * FROM wallet WHERE user_id = 1;
-- 结果如下:
# +----+---------+-----------+
# | id | user_id | balance |
# +----+---------+-----------+
# | 1 | 1 | 100000100 |
# +----+---------+-----------+

客户端 B 回滚事务

1
2
-- 客户端B回滚事务
mysql> ROLLBACK;

客户端 A 修改用户余额

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 客户端A修改用户余额
mysq> UPDATE wallet SET balance = balance - 500 WHERE user_id = 1;

-- 客户端A查询用户余额
mysq> SELECT * FROM wallet WHERE user_id = 1;
-- 结果如下:
# +----+---------+----------+
# | id | user_id | balance |
# +----+---------+----------+
# | 1 | 1 | 99999500 |
# +----+---------+----------+
-- 对于A客户端来讲,此时的结果是很奇怪的。
-- 客户端A提交事务
mysql> COMMIT;

不可重复读的场景

客户端 A 开启事务,查询用户余额

1
2
3
4
5
6
7
8
9
10
11
12
-- 客户端A设置当前会话级别为`read-committed`
mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 客户端A开启事务
mysql> BEGIN;
-- 客户端A查询用户余额
SELECT * FROM wallet WHERE user_id = 1;
-- 结果如下:
# +----+---------+---------+
# | id | user_id | balance |
# +----+---------+---------+
# | 1 | 1 | 200 |
# +----+---------+---------+

客户端 B 开启事务,并修改用户余额

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 客户端B设置当前会话级别为`read-committed`
mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 客户端B开启事务
mysql> BEGIN;
-- 客户端B修改用户余额
mysql> UPDATE wallet SET balance = balance + 100 WHERE user_id = 1;
-- 客户端B查询用户余额
mysql> SELECT * FROM wallet WHERE user_id = 1;
-- 结果如下:
# +----+---------+---------+
# | id | user_id | balance |
# +----+---------+---------+
# | 1 | 1 | 300 |
# +----+---------+---------+

客户端 A 查询用户余额

1
2
3
4
5
6
7
8
9
-- 客户端A查询用户余额
mysql> SELECT * FROM wallet WHERE user_id = 1;
-- 结果如下:
# +----+---------+---------+
# | id | user_id | balance |
# +----+---------+---------+
# | 1 | 1 | 200 |
# +----+---------+---------+
-- 此时客户端B的事务还没有提交,故客户端A不能查询到B事务已经更新的数据,解决了脏读的问题

客户端 B 提交事务

1
2
-- 客户端B提交事务
mysql> COMMIT;

客户端 A 查询用户余额

1
2
3
4
5
6
7
8
9
10
11
-- 客户端A查询用户余额
mysql> SELECT * FROM wallet WHERE user_id = 1;
-- 结果如下:
# +----+---------+---------+
# | id | user_id | balance |
# +----+---------+---------+
# | 1 | 1 | 300 |
# +----+---------+---------+
-- 此时客户端B的事务还已经提交,结果与上一步不一致,即产生了不可重复读的问题。
-- 客户端A提交事务
mysql> COMMIT;

幻读的场景

客户端 A 开启事务,查询所有用户

1
2
3
4
5
6
7
8
9
10
11
12
-- 客户端A设置当前会话级别为`repeatable-read`
mysql> SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 客户端A开启事务
mysql> BEGIN;
-- 客户端A查询所有用户余额
SELECT * FROM wallet;
-- 结果如下:
# +----+---------+---------+
# | id | user_id | balance |
# +----+---------+---------+
# | 1 | 1 | 100 |
# +----+---------+---------+

客户端 B 开启事务,并插入一条记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
-- 客户端B设置当前会话级别为`repeatable-read`
mysql> SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 客户端B开启事务
mysql> BEGIN;
-- 客户端B查询所有用户余额
SELECT * FROM wallet;
-- 结果如下:
# +----+---------+---------+
# | id | user_id | balance |
# +----+---------+---------+
# | 1 | 1 | 100 |
# +----+---------+---------+
-- 客户端B插入一条记录
mysq> INSERT INTO wallet (`id`, `user_id`, `balance`) VALUES (2, 2, 200);
-- 客户端B查询所有的用户余额
mysql> SELECT * FROM wallet;
-- 结果如下
# +----+---------+---------+
# | id | user_id | balance |
# +----+---------+---------+
# | 1 | 1 | 100 |
# | 2 | 2 | 200 |
# +----+---------+---------+

客户端 A 查询所有用户余额

1
2
3
4
5
6
7
8
-- 客户端A查询所有用户余额, 此时发生幻读。
SELECT * FROM wallet;
-- 结果如下:
# +----+---------+---------+
# | id | user_id | balance |
# +----+---------+---------+
# | 1 | 1 | 100 |
# +----+---------+---------+

客户端 B 提交事务

1
2
-- 客户端B提交事务
mysq> COMMIT;

客户端 A 插入一条记录

1
2
3
4
5
6
-- 客户A插入id为2的记录
mysql> INSERT INTO wallet (`id`, `user_id`, `balance`) VALUES (2, 2, 200);
-- 结果失败了,
ERROR 1062 (23000): Duplicate entry '2' for key 'PRIMARY'
-- 客户端A回滚
mysql> ROLLBACK;