事务的ACID和四个隔离级别

2,571 阅读11分钟

在实际的业务场景中,并发读写引出了事务控制的需求。主要关注事务的ACID和隔离性的4个级别。

ACID

事务指"一个被视为单一的工作单元的操作序列"。一个良好的事务处理系统,必须具备四个标准特性,即**ACID**:

  1. 原子性(Atomicity):一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中的一部分操作。
  2. 一致性(Consistency):数据库总是从一个一致性的状态转换到另一个一致性的状态。
  3. 隔离性(Isolation):通常来说,一个事务所做的修改在最终提交以前,对其他事务是不可见的。针对不同的业务需求,隔离性分为4个级别:读未提交、读已提交、可重复读、串行化。见下
  4. 持久性(Durability):通常来说,一旦事务提交,则其所做的修改会永久保存到数据库(即使系统崩溃,修改的数据也不会丢失)。针对不同的业务需求,持久性也分为多个级别,此处略。

隔离性的4个级别

在理解隔离性级别时,很容易混淆“幻读”与“不可重复读”的问题。这里先对4个隔离性级别给出概览;然后分析原理,从实现角度理解各种问题;最后作出总结。

概览

关注隔离性的4个级别,包括读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)、可序列化(Serializable);及其对应的问题,包括脏读(Dirty Read)、不可重复读(Nonrepeatble Read)、幻读(Phantom Read)。

读未提交

读未提交是数据库应保证的最低的隔离性级别:事务中的修改,即使没有提交,对其他事务也都是可见的

读未提交面临脏读的问题:事务可以读取未提交的数据,而该数据可能在未来因回滚而消失。从性能上来说,读未提交不会比其他的级别好太多,但却缺乏其他级别的很多好处。除非真的有非常必要的理由,在实际应用中很少使用。

读已提交

读已提交满足前面提到的隔离性的简单定义:一个事务所做的修改在最终提交以前,对其他事务是不可见的。换句话说,一旦提交,该事务所作的修改对其他正在进行中的事务就是可见的

狭义上,读已提交解决了脏读的问题。这个级别有时候叫做不可重复读,面临不可重复读的问题:两次执行同样的查询,如果第二次读到了其他事务提交的结果,则会得到不一样的结果

大多数数据库的默认隔离级别都是Read Committed,但MySQL不是。

可重复读

在读已提交的基础上,可重复读解决了部分不可重复的问题:同一个事务中多次读取同样记录结果是一致的记录指具体的数据行。

未能解决的那部分称为幻读当某个事务在读取目标范围内的记录时,另一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生第一次读取范围时不存在的**幻行**(Phantom Row)。需要注意的是,只有插入会产生幻行

MySQL的默认隔离级别是可重复读,有幻读问题。

可序列化

可序列化是最高的隔离级别:强制事务序列化执行

可序列化解决了幻读问题。简单来说,可序列化会在目标范围加独占锁,将并发读写相同范围数据的请求序列化。可序列化会导致大量的超时和锁争用问题,因此,实际应用中很少用到这个隔离级别,只有在非常需要确保数据的一致性而且可以接受没有并发的情况下,才考虑采用该级别。

原理

讨论基于锁的并发控制中,4种隔离性级别的实现原理。重点关注读锁(read lock)、写锁(write lock,或称排它锁)、范围锁(range lock)、锁的持有时间等概念。

读未提交

读未提交的实现:读锁、写锁都在一个原子操作(如select、insert等)完成后立即释放。换句话说,事务作出更新后,不管是否提交,由于已经释放了目标记录的写锁,更新对其他事务就是可见的。

读未提交存在脏读问题,假设操作序列:

  1. 事务1开始
  2. 事务1读取目标记录
  3. 事务2开始
  4. 事务2修改目标记录
  5. 事务1读取目标记录
  6. 事务2回滚
  7. 事务1提交

操作5中,事务1读到了事务2修改但未提交的记录,然后事务2回滚导致修改丢失,也就称事务1读到了“脏数据”,即脏读。

区分目标记录与目标范围(见后文可重复读的实现原理):

  • 目标记录指一个具体的数据行;读锁、写锁只针对目标记录。
  • 目标范围指一个where语句描述的范围;范围锁针对目标范围,见后。

读已提交

出现脏读的原因是写锁的持有时间过短。读已提交针对这一问题作出了优化:读锁仍然在一个原子操作完成后立即释放;写锁从写操作开始持有,事务提交后释放。事务作出更新前,会先申请目标记录的写锁,并持续持有至事务提交后,释放锁后,更新对其他事务才是可见的。

对于读未提交中的操作序列,操作5发生时,由于事务2持有目标记录的写锁,事务1会阻塞,直到事务2提交释放该写锁,解决了脏读问题。

读已提交还存在不可重复读问题。假设操作序列:

  1. 事务1开始
  2. 事务1读取目标记录
  3. 事务2开始
  4. 事务2修改目标记录
  5. 事务2提交
  6. 事务1修改目标记录
  7. 事务1提交

操作5完成后,事务2的修改对事务1可见,从而操作6中,事务1会读到修改,与操作2的结果不同,因此修改结果无法保证(如根据操作2读取的结果做修改);但是事务1在此之前未对目标记录作出任何修改,因此事务1进行操作6时的状态理应与操作2后一致(回顾事务的一致性要求)。以上即为不可重复读。

不可重复读与脏读之间存在交叉。脏读侧重读到不应存在的数据,不可重复读强调两次相同查询的结果不一样。实际上,可以将描述放宽到“目标记录的状态不符合预期状态”,如本应该不同,却读到了相同。本质上也是由于读已提交实现原理导致的问题。

可重复读

解决不可重复读可以使用两种方法:

  1. 悲观策略:串行化
  2. 乐观策略:多版本 + 冲突检测

悲观策略:串行化

“串行化”不需要解释,放弃并发、串行执行当然不存在任何问题。

“串行化”的可重复读实现是:读锁、写锁从读、写操作开始持有,事务提交后释放。与读已提交的实现相比,可重复读延长读锁的持有时间直到事务提交后,在此期间,目标记录无法被修改。

对于读已提交中的操作序列,操作2发生时,事务1开始持有目标记录的读锁,导致事务2的操作4会陷入阻塞,直到事务1提交释放锁。

“串行化”不同于“可序列化”。为了区分,前、后文中均将隔离性级别称为“可序列化”,将此处的悲观策略称为“串行化”。

乐观策略:多版本 + 冲突检测

“多版本 + 冲突检测”是更常见的实现方案:多个事务采用多个版本,最后提交时检测是否与当前数据版本冲突,如果冲突则报错提醒,否则成功提交

“多版本 + 冲突检测”的可重复读实现是:事务开始时持有当前数据的快照,读写均不冲突,提交时检测修改的快照与当前数据是否冲突。使用乐观的冲突检测策略代替悲观的锁策略,在中低程度的并发情况下性能更好。

对于读已提交中的操作序列,事务1、2各自持有不同版本的快照,在操作4修改自己版本的目标记录后,操作5提交事务2,检测不冲突(假设没有其他事务),合并到当前数据,当前数据完成修改;然后操作6继续修改自己版本的目标记录,操作7提交事务1,发现与当前数据冲突,给出报错。

幻读问题

幻读是一种特殊的不可重复读。

为什么会出现幻读问题呢?

Java的内置锁以对象为单位,RDBMS的锁呢?前面的注释中略有介绍。为了提高并发性能,简单的以数据表、数据库为单位实现锁的性能过低;标准SQL中,读、写锁以记录(数据行)为单位,范围锁以范围(逻辑上的范围,用where描述)为单位。如果没有范围锁,那么显然读、写锁只能“锁”在已存在的记录上。假设操作序列,这次具体一些:

  1. 事务1开始
  2. 事务1统计表内数据的总行数
  3. 事务2开始
  4. 事务2插入一条新纪录
  5. 事务2提交
  6. 事务1利用“旧的总行数+新的数据表内容”计算区分度
  7. 事务1提交

该操作序列是读已提交中操作序列的一个具体实例。因此,可以解决部分不可重复读问题,不能解决的那部分就是幻读了。

以基于锁的“串行化”方案为例(“多版本+并发冲突”同理),假设不使用范围锁,则幻读表现如下:由于事务2插入的记录不获取锁,操作2获取的读锁无法发挥作用,操作5提交事务2后,新记录就对事务1可见了;操作6读取时,事务1认为一致性依然满足,便使用了旧的总行数,并重新读表计算distinct count,却读到了一条意料之外的新纪录,破坏了一致性——好像出现了幻觉一样,这条新纪录就被称为“幻行”,该现象即“幻读”。

可序列化

对于基于锁的“串行化”方案,可序列化实现:从各操作开始前持有读锁、写锁、范围锁,直到事务提交后释放。对于“多版本 + 冲突检测”方案,可序列化基于更严格的写冲突检测来实现,详见“快照隔离”技术,此处不展开。

范围锁如何解决幻读问题呢?

范围锁是一个逻辑概念上的锁,事务从读、写操作(带显式或隐式where)开始前持有范围锁,直到事务提交后释放。忽略读、写锁,对可重复读中操作序列的影响如下:操作2中事务1获取了目标范围上的范围锁,操作4发现目标范围被锁,陷入阻塞,直到操作7事务提交。

隔离性级别的总结

各隔离级别解决了不同的问题。"Y"说明存在问题,"-"说明不存在:

隔离级别/问题 脏读 不可重复读 幻读
读未提交 Y Y Y
读已提交 - Y Y
可重复读 - - Y
可序列化 - - -

在基于锁的并发控制中,依靠不同的锁持有时间实现各隔离级别。锁均从操作前开始持有,"S"表示操作结束后释放,"C"表示事务提交后释放:

隔离级别/问题 脏读 不可重复读 幻读
读未提交 S S S
读已提交 C S S
可重复读 C C S
可序列化 C C C

MySQL的默认隔离级别是可重复读,解决了脏读、部分不可重复读问题,有幻读问题。


参考:


本文链接:事务的ACID和四个隔离级别 作者:猴子007 出处:monkeysayhi.github.io 本文基于 知识共享署名-相同方式共享 4.0 国际许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名及链接。