数据库隔离级别及原理解决方案

隔离性的4个级别

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

概览

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

读未提交

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

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

读已提交

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

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

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

可重复读

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

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

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

可序列化

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

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


原理

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

读锁又称为共享锁,共享锁就是多个事务共享一把锁,都能访问到数据,但都只能读不能修改。

写锁又称为排他锁,排他锁和其他事务不能共存,如果一个事务获取了一个数据行的排他锁,其他的事务不可以再获取到该数据行的任何锁,获取到排他锁的数据行可以对行进行读取和修改,没有获取到锁的数据行 不能 读取和修改。

mysql的innoDB引擎默认修改的语句都会加上排他锁,比如update insert delete

范围锁针对目标范围,一般是指sql语句中where约束的范围

读未提交

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

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

1. 事务1开始
2. 事务1读取目标记录
3. 事务2开始
4. 事务2修改目标记录
5. 事务1读取目标记录 // 读取到了操作4的修改
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. 乐观策略(乐观锁):多版本 + 冲突检测

悲观策略:串行化

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

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

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

对于读已提交中的操作序列,操作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的默认隔离级别是可重复读,解决了脏读、部分不可重复读问题,有幻读问题。

posted @ 2020-11-20 21:23  devhg  阅读(1096)  评论(0编辑  收藏  举报