事务隔离级别

​ 本文来分析数据库领域中的事务的各个隔离级别

前言

​ 事务是什么?对于我们来说,我们不应该将其视作为一个黑盒。在以前还没有深入了解数据库的时候,我总是将事务作为我实现数据处理可靠性的一种保证,觉得其的可靠性是显而易见的。但是在我经历过一段时间的学习之后,我明白不应该将其提供的可靠性视为理所当然。事务不是天然存在的。其是为了来简化应用编程模型而创建的。但是并不是所有的场景都需要事务,有时候弱化事务保证,乃至于放弃事务也是有好处的,而且有时候事务提供的安全保障也可在没有事务的场景下实现。

​ 但是我们怎么知道我们是否应该使用事务?为了回答这个问题,我们需要确切理解事物所能够提供的安全保障,以及它们的代价。也就是将一个先前的黑盒使用转变为白盒权衡。

部分文本参考自DDIA

隔离级别

​ 对于我个人而言,隔离级别的含义是指对于一个正在运行的事务来说,其所能够看到的数据库版本数据约定。运行与不同的隔离级别下的事务会看到处于不同数据约定下的数据库版本。在最理想的场景中,我们期望的是每个事务都位于可串行化这个隔离级别之下。但是正如你所知道的,这种隔离级别过于严格,导致性能会有点过度消耗,而且主要还有一点,绝大多数场景中,我们实际上并不需要这么严格的隔离级别所提供的数据保证,所以此时我们所做的工作实际上是对于业务冗余的。而这,也是另外一个我们为什么要来了解各个隔离级别的原因。我们需要了解各个隔离级别,明确其所提供的保证,了解其所存在的问题,并在实际的业务场景中来选择合适的隔离级别,保证数据以及业务的正确性。

​ 下图为一张对于现有的常见隔离级别的一些数据保证(截取自15445 2024Fall Lecture18)。

隔离级别矩阵

读已提交

​ Read Commited是实际上最常用的隔离级别。所谓的Read UnCommited,如果以简单的语句描述,那么就是在数据库系统中裸奔,其可能看到任何状态下的数据库,这实际上是没有应用场景所能够承受的。而读已提交,其提供了基础的保证,而且在大多数的应用场景中都表现的不错,我们先以这个隔离级别为切入。

​ 如图中所示,在读已提交的隔离级别下,数据的脏写和脏读都是不被允许的。

脏写

​ 脏写可能会发生在数据库内部出现多个并发事务同时对于同一个数据进行修改时,这种场景其实就是很常见的并发写场景。这种并发场景在数据库的场景中体现的更为明显,对于常见的应用场景,有些时候我们只需要保证俩个写操作的可串行调度即可,但是在更多的业务场景中,仅仅保证俩个写操作的串行化是不够的。如果我们不对于写操作进行约束,我们将不知道写入顺序会是怎么样的,由此,我们将无法明确数据库在并发事务中的状态。我们必须对于写操作本身进行一些严格的约束,才能够方便后续的分析。

​ 而在数据库中,其最基本的写操作要求就是不能够覆盖其他未提交事务的写入。

通过防止脏写,我们避免了一些并发问题:

  • 如果事务更新多个对象,脏写会导致不好的结果。例如,考虑 图7-5,以一个二手车销售网站为例,Alice和Bob两个人同时试图购买同一辆车。购买汽车需要两次数据库写入:网站上的商品列表需要更新,以反映买家的购买,销售发票需要发送给买家。在图7-5的情况下,销售是属于Bob的(因为他成功更新了商品列表),但发票却寄送给了爱丽丝(因为她成功更新了发票表)。读已提交会阻止这样的事故。
  • 但是,读已提交并不能防止图7-1中两个计数器增量之间的竞争状态。在这种情况下,第二次写入发生在第一个事务提交后,所以它不是一个脏写。这仍然是不正确的,但是出于不同的原因,在“防止更新丢失”中将讨论如何使这种计数器增量安全。

fig7-5

脏读

​ 脏读的概念应该是针对于事务来说,对于一个正在运行中的事务,其将一些需要修改的内容写入到了数据库中,但是当前事务还未结束,或者说还未提交。那么此时另外一个并发的事务能够看到这个还未提交的事务所应用在数据库中的修改吗?如果可以,那么就视为脏读。

​ 对于运行在读已提交隔离级别的事务,我们约定其在运行过程中不会出现脏读,通过避免脏读,我们避免了一些并发问题,考虑如下的场景。若存在一方事务其想要修改一个数据x,x的初始值为1,在一个运行过程中其会先set x=2,然后在一系列过程中最后再set x=3。那么此时如果存在另外一个观察者事务,该观察者事务会不断的观察对应的变量x的值,在没有避免脏读的场景下其会发生什么呢?

​ 如果没有没有脏读隔离,那么对于这个观察者事务(这里先假设为y),该事务可能在任意时刻发出观察,那么由于没有对于脏读的隔离,就可能出现其读取的值为x=2/x=3的情况,这是很奇怪的。或者说,这是一种不确定的行为,对于应用侧来说,其需要处理这种可能不一致的场景。如果说,这种不一致场景存在什么一致性的保证还好,但是对于脏读来说,这种不一致是因为你读取的是另外一个事务的中间状态,你需要为这种中间状态的读取和使用进行补偿。而这种补偿的实现往往都很复杂,而且往往都是可避免的。试想一下,如果你一个银行的统计分析事务,该事务运行时看到了一个转账事务的中间状态,最后一统计下来,发现总金额与目标金额不匹配,可能更大又或更小,这种不一致的状态是我们很难以接受的。

​ 而如果一个事务正确的解决屏蔽了脏读,那么其的行为应该如下的时序图所示,即使其与另外一个正在写的事务所并发,其看到的也总归得是一个统一的版本快照。

fig7-4

​ 到此,我们明确了所谓的脏读和脏写,其实归根到底,这俩种场景都是为了来描述一件事:All or None。一个操作,其应该看到一个完整的数据库状态快照下的内容。但是注意,读已提交这个事务级别所提供的对于状态一致的这个保证还很薄弱,其还可能存在一些问题,就比如上文表格中所提到的不可重复读

-------------本文结束 感谢阅读-------------