7

更新丢失、写偏、幻读:数据库事务从快照隔离到可序列化(三)

 3 years ago
source link: https://qcngt.com/2020/08/23/snapshot-3.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

更新丢失、写偏、幻读:数据库事务从快照隔离到可序列化(三)

作者:青菜年糕汤

更新丢失、写偏、幻读:数据库事务从快照隔离到可序列化(,三,番外

第二篇讲了四个例子,都是可序列化(serializable)行而快照隔离(snapshot isolation)不行的情况。我们可以从中说三件事。

一、它们都不是上篇讲的“更新丢失”(lost update)。

有些例子可能很像,但都不是。

更新丢失的特点是两个事务都读并写了同一个记录。对单个事务来说,需要依次发生“读-修改-写”某个变量的操作。

例1中事务甲写了前两个棋子的记录,事务乙写了后两个棋子的记录,没有写同一个。

例2中两个事务分别写了两个医生的记录,也不是同一个。

例3、4中每个事务根本没有读到任何记录。

二、它们都发生了“写偏”(write skew)。

我们发现这四个例子的操作有个共同点:它都先对数据库做了查询,根据查询的结果决定之后怎么写。

例1中查询到了值为黑(白)的棋子,决定了需要改写哪些棋子。

例2中查询了正在值班的所有医生的人数,决定了是否改写想下班的医生的值班状态。

例3中查询了是否存在该用户名,决定了是否插入用户名。

例4中查询了是否存在相近的入学时间,决定了是否插入入学时间。

让我们重复一下快照隔离的效果是:

一个事务读到的数据都来自于数据库某同一个时刻(时刻甲)的状态,然后所有写都发生在之后的某同一个时刻(时刻乙)。

这里的矛盾的矛盾就清楚了:

时刻甲时数据有个状态,等到了时刻乙,数据的状态可能不一样了。根据时刻甲的状态作出写的决定,这个决定到时刻乙真正写时,就未必适用了。

有点刻舟求剑的味道。

(可以注意到,要形成写偏,一个事务的写操作未必需要能够改变这个事务自己前面的读的信息,而是只要其它的事务改变了这个事务读的信息就行。这道理不难想,但好像不太容易构造出一个足够漂亮的例子。)

三、例3、4中发生了“幻读”(phantom)。

对于上面提到的写偏,如果不考虑效率,只考虑正确性,可以想到一种很直观的解决办法:

记录下这个事务读过哪些数据,等提交时,检查这些数据没有在这个事务期间被别人改写过。如果有就中断,如果没有就成功。

还有一种办法更悲观一些:

锁住读过的数据,不让其它事务写这些记录。

这两种办法都能解决例1、2的情况。但例3、4却可以说明这两种方法是不足以把快照隔离变成可序列化的。

我们可以先对比一下例2和例3。

在例2中,这个事务读过哪些数据?所有医生的值班状态(在没有二级索引的情况下,为了数有多少医生正在值班,这个事务会需要遍历所有医生的状态)。只要把所有医生的数据都加入检查(或锁上),就可以阻止同时的改动。

而在例3中,这个事务读过哪些数据?没有。因而检查(或锁上)读的数据,完全无助于例3。

有人会注意到,例3中虽然没读到任何数据,但尝试查询了这个用户名的记录。可以稍微修改一下解决办法:不管有没有读到数据,只要尝试读了这个数据,就要把它加入检查(或锁上)中。

那么对于例4该怎么办呢?

例3中我们的查找是离散的,只尝试读取了一个数据点。而例4却是想查找一个范围,它需要把这个范围内的所有数据点都加入检查(或锁上)中,即使它们无穷无尽,即使它们尚未出现。

解决方法可以是把整个范围都加入到检查(或锁上)。

这在具体的实现上,未必会像听上去的那么简单。有的数据库设计得很难对任意范围上锁,就会预先划分好每块范围,对覆盖这个范围的几块范围都上锁。这么做会稍微多锁一些,但不会有正确性的问题。

这样,幻读的问题就解决了。

当解决更新丢失、写偏、幻读这三个问题,快照隔离的数据库就能被改造成一个可序列化的数据库。

这里的数据库可以是传统的关系型数据库,也可以是键值数据库、文档数据库等等。在这个问题上,它们都是一样的,本质上都是键、值的读写。事实上,一个好的键值数据库(如FoundationDB、Spanner),完全可以作为其他类型的数据库的底层,关于这个话题以后也可以讨论讨论。

当然,不是谁都每天有闲心去这么改装的。对于数据库的使用者,如果换个角度看,可能会得到另一结论:

只要能够通过其它途径解决更新丢失、写偏、幻读的问题,或者证明它们不会对业务造成影响,我们就可以用快照隔离这个更松更高效的隔离级别,来达到可序列化级别的正确性。

我之后会另发一篇文章,讲一个最近工作中遇到的问题,为上面这段话做注解。

到这里,我们这个从快照隔离到可序列化的旅程可以算告一段落了。

欢迎留言交流。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK