Transaction弱隔离之Write Skew和Phantoms

我们在前面的《Transaction弱隔离之读提交的介绍和实现》和《Transaction弱隔离之更新的丢失》中分别介绍了脏写和更新丢失。他们都是有两个写同时发生,从而产生了冲突。那么对于这种情况,我们必然需要进行保护和处理,可以是数据库来自动处理也可以是手动的加一些保护比如锁或者原子写操作等。然而上面提到的两种写冲突就是全部了吗?现实世界显然更加残酷,本文就来具体看看别的冲突的例子。

如下图所示,假设你正在维护一个医院的值班系统,这个系统一般来说会安排几个医生一起来值班,当然它的最低要求是必须有一个医生值班。所以说,当轮到你值班的时候,假如还有别的医生在值班,你就可以从这个系统中请假。请假的操作也很简单,就是假如系统中显示有两个及以上的医生在值班,那么就允许你更新自己的状态成不值班。

现在我们假设Alice和Bob突然同时有事,一个身体不舒服,一个想去和女朋友约会,他们在下班之前的那一刻同时准备到系统中来请假,并且同时执行了请假的操作。然后就有了两个transaction同时执行,他们都是读的snapshot的内容,所以在更新的之前on call count都是大于等于2的,然后他们同时更新了他们自己的状态到请假(on_call = false)。这样一来,当天就没有医生值班了。

Write Skew的特征

我们把上面这个例子中出现的现象称之为write skew。它既不是脏写也不是更新丢失,因为他们最终更新的其实是各自的属性,并没有更新同一个结构体。这种冲突其实很难发现,因为他们看起来就像没有关系的两个独立体。只是和之前提到的脏写以及更新丢失不同,他们在更新了不同的object之后,导致的结果就是某一个查询的结果发生了变化(比如值班的人数)。那一般来说Write skew有什么特征呢?

  • 单个的object加锁没有用,因为这里引入了多个object。
  • 一些数据中的自动的更新丢失的探测也没有效果。
  • 有些数据库允许你配置一些约束条件,然后来强制数据库进行检查。但是像这个例子中,你要保证至少有一个医生值班,就需要把这个约束条件扩大到多个object。而很多数据库内置是不支持这样的约束条件的。
  • 一个比较好的方法可能就是把所涉及的行加上锁。比如说使用下面这种语句:

这里的FOR UPDATE就是告诉数据库你需要对我查询的所有行加一个锁。

常见的Write Skew的例子

开始看到Write Skew的例子的时候,你可能觉得这种情况应该比较少见,但你了解了之后就会发现,其实还是蛮常见的:

会议室预定系统

比如说你希望一个会议室同一时间不会被两个人预定。所以在预定之前,你需要先确认这个会议室在这段时间内没有被预定,然后才允许这个预定操作的发生,比如使用下面这种语句(不安全)

当使用snapshot隔离的时候,这个语句其实不能处理同时的写,所以可能需要serialization隔离。

创建一个用户名

当我们注册一个网页的时候,需要保证用户名是唯一的,假如有两个用户同时尝试创建同一个用户名该如何处理。假如你和上面例子类似使用操作说先检查有没有用户名,如果没有就创建,那么这种在snapshot隔离的场景中就会有问题。当然这个例子的解决方案比较简单,只要使用一个简单的unique限制就好了,这样第二个用户名的插入会报错返回。

消费保护

比如说你有一个账号,里面有钱和积分,当用户用这些钱或者积分购买东西的时候,一个简单的要求就是花费不能超过账户的钱或者积分的总和。假如你的实现是每一个消费的内容都插入一条数据,然后看总的消费是不是超过了余额,那么假如有两个插入同时发生的话就有可能会有问题,因为他们并不知道对方的存在。

Phantoms导致的Write Skew

我们上面提到的例子都有一个共同的pattern:

  1. 一个SELECT语句检查是否满足某一个条件。(比如至少有两个人值班,没有人已经预定了会议室等)
  2. 基于查询的结果,决定如何做。
  3. 上面的操作之后,会更新数据库中的某些数据。而这个数据的更新会导致1中的查询条件发生变化。也就是说你要是再去执行1会发现条件可能已经不满足了。

我们把这种一个transaction中的写改变了另外一个transaction的查询条件的情况称之为Phantom.

物化冲突

其实假如我们细想一下上面这种情况,问题的根源就是我们没有一个共同的object来显示这种冲突(查询的结果),假如我们假想一个共同的object,然后只要简单的加个锁就可以了。

比如我们上面的例子中提到的会议预定系统,假如我们可以创建一个表包含time slots和会议室,然后这个表中的每一行就是一个time slot和会议室的组合(比如time slot是15分钟,是不是恍然大悟为什么我们预定会议室的时间没有精确到分钟,而是整点或者15,30,45这种,或者干脆就以半小时为单位)。

这样一来预定的transaction就只要锁住对应的行就可以了(FOR UPDATE),当然这个表并不用来表示预定的情况,只是用来处理预定冲突。

我们把这种方法称之为物化冲突,但是这种方法其实有其最大的缺点就是很难去搞清楚如何进行物化冲突。所以一般来说,我们把这种方法作为最后的救命稻草,也就是没有别的方法才会选择它,那么一般我们会选择什么办法呢?那就是我们下一篇需要讲解的serializable隔离。

总结

本文介绍了Write Skew的常见例子和最无奈的解决方案,我们会在后面的文章中再具体介绍常见的解决方案:serializable隔离。

You may also like...

1 Response

  1. July 7, 2021

    […] 《Transaction弱隔离之Write Skew和Phantoms》 […]

Leave a Reply

Your email address will not be published.