青菜年糕汤

一周一篇,一期一会。以文会友,以友辅仁。

2020年8月23日

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

作者:青菜年糕汤

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

这个系列文章的目标受众是:本来就了解数据库系统中的事务是什么意思,也大致听说过(但未必分得清)五花八门的事务隔离级别。

这个系列我们将讨论两种事务隔离级别之间的区别:快照隔离(snapshot isolation)和可序列化(serializable)。

在数据系统领域,有不少挺让人费解的概念。

即使作为数据库系统的开发者 ,也不会每天都碰到这些概念;当偶尔突然碰到了,也被这些问题迷糊一下。更不要提更广大的数据库使用者、后端开发者了。

这个混乱很大程度上源自于不同系统在实现时的自由度。出于不同现实需求,不同的系统有不同的侧重点,就会有不同的实现方式,最后所谓的标准就与五花八门的事实标准不一致了。

比如可重复读(repeatable read)这个概念就是一例。它与快照隔离相近又不完全一样,来龙去脉可以单独写篇文章讨论,但在这里我们为简便起见,只采用快照隔离这一说法。

说回到数据库的事务隔离级别。关于这个话题,有很多详略各异的综述。

比如维基百科的“事务隔离”词条可以用于初次了解,或是在忘了之后激活记忆。

再比如我近来很推荐的书《数据密集型应用系统设计》(Designing Data-Intensive Applications)的第七章“事务”,既能理解理论定义,也可以了解几个常用的数据库系统的现实情况。

珠玉在前,我的文章无意于完整地构建整个体系,只是想从快照隔离与可序列化的差别这一角度入手,略窥一斑。

如果要用一句话解释这两个隔离级别提供的保证,我会这么说:

  • 快照隔离的一个事务读到的数据都来自于数据库某同一个时刻的状态(“快照”得名于此),然后所有写都发生在之后的某同一个时刻。

  • 可序列化的每个事务都是完全独立的,一个事务完成后才会做下一个。

注意这里说的是满足该隔离性后事务运行的效果,是帮助数据库使用者想清楚概念的思维模型。

为了保证优秀的并发度和速度,事实上实现它们要比这里说的复杂。但只要实现得正确,它们的运行结果一定是在这个简化模型下可能发生的。因此使用者不妨以为事实就跟简化模型一样。

比较两种隔离级别,可以看到可序列化的隔离型更强,用户理解起来、用起来也更简单。

快照隔离(以及其它更弱的隔离级别)之所以存在,之所以还在折磨着数据库开发者和使用者,是因为它们在性能上的优势。

我们在这里完全只比较两者概念模型的区别,尽量不涉及具体实现和性能。

那么快照隔离究竟比可序列化弱在哪里呢?

假设你在数据库里有个值X开始是0。现在事务甲给X加10,事务乙要给X加20。

在可序列化隔离性下,不管甲乙哪个先发生、哪个后发生,最后X都会是30。(比如甲读到X是0,把它改成10写入,然后乙读到X是10,把它改成30写入。)

而在快照隔离下,有可能两个事务读X时都读到的是它开始时的值0,然后甲试图把它改成10写入,乙试图把它改成20写入。不管最后是谁覆盖了谁,结果都不是正确的30。

这种现象就被称为“更新丢失”(update lost)。

快照隔离已经是很高的隔离性了,但按照上面的定义,它依旧会产生更新丢失这样显而易见的问题。

事实上,这个问题不需要太大的代价就能解决。所以很多快照隔离的数据库事实上都会检测、避免更新丢失的情况。以至于一般当人们讲“快照隔离”时都是默认避免了更新丢失的情况的。

那除此之外,快照隔离与可序列化还有差距吗?当然,那就是写偏(write skew)和幻读(phantom)。

为了讲清楚,我们不妨从一系列的例子入手。请移步第二篇