数据库事务的ACID特性中,I为Isolation的缩写,即隔离性,隔离性指在不同业务的处理过程中,事务能够对各业务正在读、写的数据相互独立做出保证,不会相互影响。
本文主要阐明数据库并发事务中常见的异常情况,如脏读、可重复读、幻读等情况,以及各常见隔离级别分别能够解决什么样的问题。
常见的事务隔离级别
如今的数据库事务常见的隔离级别有以下五类:
- 串行化(Serializability)
- 可重复读(Repeatable Read)
- 快照隔离(SnapShot Isolation) / 多版本并发控制 (MVCC)
- 读已提交(Read Committed)
- 读未提交(Read Uncommitted)
此五类隔离级别自上至下,串行化提供最高强度的隔离性,但就以串行化作为隔离级别系统的可用性最差,读未提交相反,拥有隔离级别中最高的可用性,以及最弱的事务间数据隔离保证(理论上还有一种完全不隔离的策略,不纳入讨论范围)。
各隔离级别常见实现简述
多线程开发环境下,为了保证线程安全,程序开发人员通常会使用加锁做同步处理,数据库事务也是同理,使用加锁实现数据库事务的隔离性,除了快照实现隔离性除外,锁可以大致分为以下三类:
- 读锁(共享锁):多个事务可以同时读(共享)同一个数据,对于被读锁锁定的数据的写操作应该被阻塞,等待读操作执行完毕后唤醒。
- 写锁(排他锁):只有持有写锁的事务才能够对数据执行写入操作,其他事务对被写锁锁定的读、写操作均应被阻塞。
- 范围锁:对一个范围的数据加写锁,整个选定范围的数据不应被其他事务读、写。Mysql中范围锁的体现为临键锁和排他锁,通过锁定范围防止幻读问题。
接下来说明不同隔离级别都采用了何种锁做同步处理。
- 读未提交:对事务涉及到的数据加写锁,持有到当前事务结束。
- 读已提交:对事务涉及到的数据加写锁和读锁,只有写锁会持有到当前事务结束,读锁在读操作执行完毕后立即释放。
- 可重复读:对事务涉及到的数据都加写锁和读锁,且一直持有到当前事务结束。
- 快照隔离:无锁实现,此处无锁特指无读锁,数据库内部保证新版本数据与老版本数据共存,保证任一写操作都不会覆盖之前的数据,因此读操作可以完全不加锁。Mysql中实现为多版本并发控制(MVCC),与读已提交和可重复读隔离级别共同使用。
- 串行化:对事务的所有读、写操作加写锁、读锁、范围锁,所有锁均持有到当前事务结束,保证指令串行。
常见并发事务异常
脏写
指一个事务的写操作,覆盖了另一个正在运行未提交的事务的写入值。
脏写会破坏数据的完整性约束(例如数据库要求保证x + y = 100)。
例如,有事务A和事务B,事务A包含两个写操作x=1、y=1,事务B也包含两个写操作x=2、y=2,假设事务的一致性要求保证x与y的值必须相等,那么有可能出现以下的情况:
事务A | 事务B |
---|---|
x = 1 | |
x = 2 | |
y = 2 | |
commit | |
y = 1 | |
commit |
提交后结果为x=2,y=1破坏了数据一致性的约束。
脏读
指一个事务读到了另一个事务尚未提交的写入值。
脏读可能导致事务回滚后数据库数据状态的矛盾,假如事务A读取到了事务B尚未提交的写入值,并基于写入值或依据该值作为前提,执行了其他的写入操作,若后续事务B执行失败回滚,事务A写入操作的依据就有可能是错误的。
不可重复读
指同一个事务对于同一事务的两次读操作,读到了不同的值
不可重复读可能导致数据的非法写入等问题(需要考虑具体场景),例如事务的第一次读取值作为某条件判断的依据,第二次读取的值需要执行某写入操作。
实例如下:
事务A | 事务B |
---|---|
Read(x) | |
x = 1 | |
Read(x) | |
commit | commit |
幻读
指同一个事务的两次条件查询或范围查询,得到的结果不同
示例如下:
事务A | 事务B |
---|---|
select count(*) from users | |
insert into users values (‘user’) | |
select count(*) from users | |
commit | commit |
事务B相同查询条件的两次读取得到了不同的值,视具体业务场景,可能会导致意料之外的结果。
更新丢失
指两个事务都尝试对同一个数据执行写操作,后执行写操作的事务先提交,导致该事务的更新被覆盖。
假设有一物流管理的系统,事务A需要向仓库添加5件物品,事务B需要向仓库添加10件物品,两事务同时执行的情况下可能出现更新丢失,结果可能是事务A或事务B的添加被覆盖,导致少计算的若干物品。
示例如下:
事务A | 事务B |
---|---|
x = 5 | |
x = 10 | |
commit | |
commit |
示例会导致x = 10写操作丢失。
读倾斜
指读到了数据一致性被破坏的数据,一致性特指业务逻辑上的约束,例如前文提及的x + y = 100
示例如下:
事务A | x | y | 事务B |
---|---|---|---|
50 | 50 | Read(x) | |
Write(x,30) | 30 | 50 | |
Write(y,70) | 30 | 70 | |
commit | 30 | 70 | Read(y) |
30 | 70 | commit |
在该示例中,事务A负责更新x与y的值,事务B负责读取x与y的值用作某可能的条件判断,执行结果为B读取到的x与y分别为50与70,在事务B的视角这违背了数据的约束,与数据库的事实状态违背。
写倾斜
指两个同时执行的事务都以相同的约束为条件,随后各自执行自己的写操作,某时刻破坏了这个约束条件。
假设现有事务A和事务B以及数据x = 2,数据库存在数据约束x > 0,事务A和事务B中均会做x自减操作,意味着事务A和事务B执行的先决条件均为x > 0,如果两事务并发的执行,可能会导致两事务执行前的条件检查均通过且最终执行成功,执行完毕后却破坏了数据x的约束。
总结
根据本文内容,汇总表示不同隔离级别分别能解决的并发事务异常,行表示各隔离级别,列表示各并发事务异常,内容表示在当前隔离级别下是否会出现该并发异常。
脏写 | 脏读 | 不可重复读 | 幻读 | 更新丢失 | 读倾斜 | 写倾斜 | |
---|---|---|---|---|---|---|---|
读未提交 | No | Yes | Yes | Yes | Yes | Yes | Yes |
读已提交 | No | No | Yes | Yes | Yes | Yes | Yes |
快照隔离 | No | No | No | Yes | No | No | Yes |
可重复读 | No | No | No | Yes | No | No | No |
串行化 | No | No | No | No | No | No | No |
参考书目:《深入理解分布式系统》唐伟志