事务的四种隔离级别与脏读、幻读、不可重复读

一、事务隔离级别

数据库事务( Transaction)是访问并可能操作各种数据项的一个数据库操作序列。事务必须满足ACID原则。其中隔离性(Isolation)指数据库并发情况下,并发的事务直接是隔离的,一个事务的执行不能被其他事务影响。对此不太清楚的同学请出门左转: 事务ACID原则
事务从低到高共有四个隔离级别:读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)。

二、并发事务异常

在详细介绍事务隔离级别之前我们先看一下并发事务可能存在的脏读、幻读和不可重复读等异常。

为了便于理解我们还是以经典的银行转账情境为例,假设A和B两个账户分别拥有100元,此时A向B转账100元。综述事务A包含如下操作1和操作2:

操作1:A账户余额减少100元,账户余额为0元;
操作2:B账户余额增加100元,账户余额为200元

(一)脏读

脏读指并发环境下一个事务读取到了其他事务尚未提交的数据。假设事务A开启事务 -> 执行操作1和操作2。此时另一个事务B也开启事务 -> 读取A账户余额 -> 关闭事务。伴随着事务A的回滚,事务B读取到的0元就是脏数据,这就是脏读。

事务A 事务B
start
write(A)=0
write(B)=200 start
  read(A)=0
rollback commit

(二)不可重复读

不可重复读指并发环境下一个事务中使用相同条件的两次读取得到的数据不一致。假设事务B开始事务 -> 第一次查询A账户余额。此时事务A开始事务 -> 执行操作1和操作2 -> 提交事务。伴随着事务A的提交,事务B第二次查询A账户余额。显然,事务B中先后两次查询A账户余额的结果不一致,即不可重复读。

事务A 事务B
  start
start read(A)=100
write(A)=0
write(B)=200
commit
  read(A)=0

不可重复读与脏读的区别是脏读读取到了其他事务未提交的数据,而不可重复读则是读取到了其他事务已经提交的数据。

(三)幻读

幻读指并发环境下一个事务中使用相同条件的两次读取得到的数据量不一致。假设事务B开启事务 -> 第一次读取账户数量。此时另一个事务C开启事务 -> 插入一个新的账户C -> 提交事务。伴随着事务C的提交,事务B第二次读取账户数量。这时,事务B中先后两次读取所有账户存款总额的结果不一致,就像产生了幻觉,故称为幻读。

事务B 事务C
start
count()=2
  start
  insert(C)
  commit
count()=3

幻读和不可重复读都是读取到了其他事务提交的数据,但它们的区别是不可重复读强调的是数据的内容不一致,而幻读则侧重于数量量(数据条数)不一致。

(四)丢失更新

丢失更新指并发环境下一个事务的提交覆盖了其他事务提交的修改。假设事务A与事务B同时开启事务,事务A执行操作1和操作2 -> 提交事务。事务B修改A账户余额为200元并在事务A提交后提交,此时A账户余额被事务B提交为200元覆盖了事务A提交的修改,这就是丢失更新。

事务A 事务B
start start
write(A)=0 write(A)=200
write(B)=200
commit
  commit

(五)脏写

脏写指并发环境下一个事务的回滚覆盖了其他事务提交的修改。假设事务B开启事务 -> 查询A账户余额。此时事务A开始事务 -> 执行操作1和操作2 -> 提交事务。在事务A的提交之后事务B修改A账户余额为200元并回滚,此时A账户余额被事务B回滚为100元覆盖了事务A提交的修改,这就是脏写。

事务A 事务B
  start
  read(A)=100
start
write(A)=0
write(B)=200
commit
  write(A)=200
  rollback

脏写和丢失更新都是一个事务的操作覆盖了其他事务的操作。它们唯一的区别是丢失更新是由于提交导致的,而脏写是由于回滚导致的。

三、事务隔离级别

为了解决上述异常,事务隔离应运而生。在并发环境下事务的隔离级别越高,可能产生的异常自然越少,但同时付出的性能损失也就越大。所以我们需要根据实际应用情况在隔离级别和性能之间取得一个平衡。而最终选定的事务隔离级别可能产生的异常只能通过代码代偿了。下面我们从低到高介绍四个隔离级别:
事务的四种隔离级别与脏读、幻读、不可重复读
值得注意的是,上表事务隔离级别与并发异常对应关系只是理论上的,不同的数据库可能不同。比如MySQL的Innodb存储引擎通过Next-Key Locking技术可在Repeatable Read级别消除幻读的可能。

(一)读未提交(Read Uncommitted)

所有事务都可以读取到其他事务尚未提交的数据。该级别很少用于实际应用,它不仅可能产生各种异常且效率并不高。

(二)读已提交(Read Committed)

这是大部分数据库默认采取的隔离级别(但不包括MySQL)。它满足了隔离的简单定义:一个事务只能看见已经提交的事务所做的改变。

(三)可重复读(Repeatable Read)

这是MySQL默认的事务隔离级别,它确保同一事务中使用相同条件的两次读取得到的数据一致。该级别仍可能产生幻读,不过例如Innodb等很多存储引擎都可以通过其他的并发控制机制避免幻读。

(四)串行化(Serializable)

这是最高的隔离级别,它通过强制事务排序依次串行执行事务,从而解决一切可能产生的异常。简言之,它是在每个读的数据行上加上共享锁,可能导致大量的超时现象和锁竞争。

四、Spring事务隔离级别

Spring除提供给用户以上四种隔离级别外,还提供一种事务隔离级别:DEFAULT。由于不同数据库采用的默认事务隔离级别不同,故可通过将Spring事务配置中的isolation设为DEFAULT来使用底层数据库的默认事务隔离级别。

顺便说一句,如果使用的MySQL,可以使用"select @@tx_isolation"来查看默认的事务隔离级别。