对于两个并发执行的事务,如果涉及到操作同一条记录的时候,可能会发生问题。因为并发操作会带来数据的不一致性,包括脏读、不可重复读、幻读等。数据库系统提供了隔离级别来让我们有针对性地选择事务的隔离级别,避免数据不一致性的问题。
SQL标准定义了4种隔离级别,分别对应可能出现的数据不一致情况:
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
read uncommitted(读未提交) | yes | yes | yes |
read committed(读提交) | - | yes | yes |
repeatable read(可重复读) | - | - | yes |
serializable(串行化) | - | - | - |
关于脏读,不可重复读,幻读的详细解释,我们放在下面篇幅中通过实际例子来说明。
read uncommitted
read uncommitted(读未提交)是隔离级别最低的一种事务级别。在这种隔离级别下,一个事务会读到另一个事务更新后但未提交(commit)的数据,如果另一个事务回滚,那么当前事务读到的数据就是脏数据,这就是脏读
(Dirty Read)。
我们先准备好一个account
表,该表仅一行记录:
mysql> select * from account;
+----+-------+---------+
| id | name | balance |
+----+-------+---------+
| 1 | lilei | 600 |
+----+-------+---------+
然后开启两个MySql客户端会话,按顺序依次执行事务A和事务B
时刻 | 事务A | 事务B |
---|---|---|
1 | set session transaction isolation level read uncommitted; | set session transaction isolation level read uncommitted; |
2 | begin; | begin; |
3 | update account set balance = 100 where id = 1; | |
4 | select * from account where id = 1; | |
5 | rollback; | |
6 | select * from account where id = 1; | |
7 | commit; |
在事务A中修改表的数据,修改之后,在事务B中查询表数据。
通过上面我们就可以发现,事务A还未提交,但是事务B已经可以查询到更新的数据了。
接下来如果事务A因为某些原因回滚了,那么事务A中原先查询到的数据都会是脏数据。
你可能会说,那我事务B在事务A回滚之后再次查询,得到的数据不就是真实数据了吗。但是实际开发中,你怎么知道另外的事务有没有回滚呢?你无法知道其他事务有没有回滚,那么也就无法确定是否应该再次查询。
read committed
在read committed(读已提交)隔离级别下,一个事务只能读到其他事务已经提交的数据。这个隔离级别下,事务可能会遇到不可重复读
(Non Repeatable Read)的问题。
不可重复读是指,在一个事务内,不同时刻读同一批数据可能是不一样的。在一个事务还没有结束时,如果另一个事务恰好修改了这个数据并提交了,那么在第一个事务中,两次读取的数据就可能不一致。
我们仍然先准备好account
表的数据
mysql> select * from account;
+----+-------+---------+
| id | name | balance |
+----+-------+---------+
| 1 | lilei | 600 |
+----+-------+---------+
然后分别开启两个MySQL连接会话,按顺序依次执行事务A和事务B:
时刻 | 事务A | 事务B |
---|---|---|
1 | set session transaction isolation level read uncommitted; | set session transaction isolation level read uncommitted; |
2 | begin; | begin; |
3 | update account set balance = 100 where id = 1; | |
4 | select * from account where id = 1; | |
5 | commit; | |
6 | select * from account where id = 1; | |
7 | commit; |
开启事务之后,在事务A中修改表数据,修改之后,在事务B中查询表数据。
通过上面我们测试,我们发现,事务A未提交,事务B不能查询到事务A中更新的数据,解决了脏读问题。
但是我们前面也说过,读已提交隔离级别下,会有不可重复读的问题。
下面我们来验证下
repeatable read
repeatable read(可重复读)是对比不可重复读而言的。上面说不可重复读是指同一事务不同时刻读到的数据值可能不一样。而可重复读是指,事务不会读到其他事务对已有数据的修改,即使其它事务已经提交。也就是说,事务开始读到的已有数据是什么,在事务提交前的任意时刻,这些数据值都是一样的,但是,在修改数据的时候却可以读到那些其他事务提交的数据,这也就引发了幻读
(Phantom Read)的问题。
幻读是指,在一个事务中,查询的结果,与增删改的结果有所冲突。这么解释似乎很难理解,没关系,我们通过下面的实际操作来说明。
我们仍然先准备好account
表数据
mysql> select * from account;
+----+-------+---------+
| id | name | balance |
+----+-------+---------+
| 1 | lilei | 100 |
+----+-------+---------+
1 row in set (0.00 sec)
然后分别开启两个MySQL连接会话,按顺序依次执行事务A和事务B:
时刻 | 事务A | 事务B |
---|---|---|
1 | set session transaction isolation level read uncommitted; | set session transaction isolation level read uncommitted; |
2 | begin; | begin; |
3 | update account set balance = 500 where id = 1; | |
4 | select * from account where id = 1; | |
5 | commit; | |
6 | select * from account where id = 1; | |
7 | update account set balance = balance - 100 where id = 1; | |
8 | select * from account where id = 1; | |
9 | commit; |
开启事务后,在事务A中修改表数据,修改了之后,在事务B中查询表数据。
事务A未提交,事务B中查询不到修改结果,脏读问题解决了。接下来我们看看将事务A提交之后,事务B的查询结果会如何。
通过上面测试,我们看到,即使事务A已经提交,事务B的查询结果还是和原来一样,不可重复读的问题也就解决了。
测试还得继续,我们接下来在事务B中对一条记录做修改。
神奇的事情就发生了,对于事务B来说,自己明明查到lilei的balance是100,怎么减去100后,balance反而变成了400了,这就好像出现了幻觉一样,所以叫幻读。(尽管我们站在上帝视角是知道事务A修改了数据,但是事务B并不知道事务A的存在)
在可重复读的隔离级别下,数据的最终一致性是没有被破坏的,它使用了MVCC的机制,select操作不会更新版本号,是快照读,insert、update、delete会更新版本号,是当前读。
接下来我们再来看一个幻读的案例
依旧是使用account
表
mysql> select * from account;
+----+-------+---------+
| id | name | balance |
+----+-------+---------+
| 1 | lilei | 400 |
+----+-------+---------+
1 row in set (0.00 sec)
然后分别开启两个MySQL连接会话,按顺序依次执行事务A和事务B:
时刻 | 事务A | 事务B |
---|---|---|
1 | set session transaction isolation level read uncommitted; | set session transaction isolation level read uncommitted; |
2 | begin; | begin; |
3 | select * from account where id = 99; | |
4 | insert into account (id, name, balance) values (99, ‘hanmeimei’, 1000); | |
5 | commit; | |
6 | select * from account where id = 99; | |
7 | update account set balance = 900 where id = 99; | |
8 | select * from account where id = 99; | |
9 | commit; |
将两个会话都开启事务,然后执行下面的步骤
事务A插入一条记录并且提交,此时事务B中查询不到这条新增的记录。
接下来我们在事务B中更新id=99的记录
Serializable
Serializable(串行化)是最严格的隔离级别。在Serializable隔离级别下,所有事务按照次序一次执行,因此,脏读、不可重复读、幻读都不会出现。
虽然Serializable隔离级别下的事务具有最高的安全性,但是,由于事务是串行执行,所以并发性能极低,。我们在开发中一般都不会使用Serializable隔离级别。