揭露数据库隔离级别的肮脏秘密:可串行性与“严格”可串行化区别! - Matt Freels


多年来,“可串行化/序列化”(serializability)被称为数据库隔离级别的“ 黄金标准 ”。它是绝大多数商业数据库系统中提供的最高隔离级别,一些高度广泛部署的系统甚至无法提供隔离级别与可序列化一样高。
在这篇文章中,我们展示了可序列化的可行性,并且说明它从未成为数据库系统的“黄金标准”。事实上,严格的可串行化才一直是黄金标准。正如我们将看到的,因为在遗留数据库系统中实现可串行化提供了关键的严格保证,所以“可串行化”和“严格可串行化”之间的区别大多被忽略了。然而,在以云为中心的分布式系统的现代世界中,这种差异是显著的。
这篇文章首先解释了数据库系统中隔离的概念,然后说明了这两个隔离级别之间的重要差异,以及应用程序员需要注意的“仅仅”引入可串行化系统导致的正确性bug。

背景:什么是“隔离”
假设我们有一个售票应用程序,允许人们购买限量供应活动的门票。应用程序将以下形式的事务发送到数据存储(当客户尝试买票时):

如果库存> 0那么
     库存=库存-1; 
     过程(顺序)
     返回成功
否则 
     返回失败FAIL

如果数据存储支持ACID事务,则将以原子方式处理上述代码: 将处理整个订单并减少库存,或者订单与库存两者都不处理。要么全部处理要么全部失败,除此以外其他任何事情都不可能发生。
如果系统中只运行一个事务,则显然运行上述代码不会出现任何问题。如果库存足够,则会处理订单。否则,它不会。
问题通常仅在并发下发生:多个客户尝试同时购买一张票。如果有更多客户试图购买门票而不是库存,理想的最终结果是所有库存都将被出售,而不是更多。

如果允许上述代码在没有系统级保证的情况下运行,则代码中存在明显的“竞争条件”。两个并行线程可以同时运行第一行,并且都看到库存为“1”。因此,两个线程都通过IF条件并处理订单,这导致库存超卖的负面结果。

系统级保证在面对并发请求时防止这些负面结果属于ACID的I - “隔离”。

隔离的黄金标准是“可串行化serializability”。保证可串行化的系统能够同时处理事务,但保证最终结果等同于单独处理每个事务时发生的事情,一个接一个地处理(串行就好像没有并发)。这是一个非常强大的保证,它能够经受住时间的考验(50年),从而能够在其上构建健壮且无错误的应用程序。

可串行化隔离如此强大的原因是应用程序开发人员根本不需要推理并发性。开发人员只需单独地关注单个事务的逻辑正确性即可。只要每个单独的事务不能违反应用程序的语义,开发人员就可以确保同时运行其中许多事务也不会违反应用程序的语义。在我们的示例中,我们只需要确保上面显示的6行代码的正确性。只要在数据库的任何启动状态下处理它们时它们都是正确的,那么应用程序在并发时将保持正确。

可串行化的脏数据秘密:它看起来并不像“安全”
尽管具有令人难以置信的可串行化能力,但确实存在更强的保证,并且保证“仅仅”依靠可串行化的数据库实际上容易出现与并发性相关的其他类型的错误。

可串行化的第一个限制是:它不限制如何选择等效的事务序列顺序。指定一组并发执行的事务,系统保证它们将被等效地处理为串行顺序,但它不保证任何特定的串行顺序。
结果,指定一组相同的并发事务处理的两个副本可能最终处于非常不同的最终状态,因为他们选择以不同的等效序列顺序处理事务。因此,“仅仅”依靠可序列化的数据库的复制不能只复制输入并让每个复制事务进程同时处理输入就可以了,这不能保证复制。

相反,一个复制的进程必须首先处理工作负载,然后处理来自原初始进程发生的一系列状态改变的复制,这个状态改变的复制是原初始进程通过网络发布的,因此,数据经过网络延迟到达时间和原始进程中原始数据发生的时间有滞后性。

可串行化的第二个限制是:可序列化系统选择的等同序列顺序不必与提交给系统的事务的顺序相关,。在“事务X之后提交的事务Y“与“Y在在X之前处理”是符合等效序列顺序的。

即使在X完成后的几周内提交了Y,这也是正确的 , 理论上可以为可串行化系统带来Y及时返回,并在X存在之前处理系统状态(只要没有提交的事务读取Y写入的相同数据),从技术上讲每个只读事务都可以返回空集(即数据库的初始状态),并且不会违反串行化保证,这是合法的,因为系统可以使只读事务“及时”返回,并在写入数据B的任何事务之前以串行顺序获得一个卡位。

任何用户都不太可能使用始终为每个只读查询返回空集的数据库。那么可串行化如何成为“黄金标准”?

在过去,数据库只能在一台机器上运行。因此,数据库系统保证隔离级别保证更强一致性要超过可串行化serializability。事实上,以至于多年来没有人为这个强大的隔离保证保证(即使几乎每个数据库系统都保证它)。隔离级别最终被Herlihy和Wing命名为: 严格可串行化。

严格的可串行性更安全
严格的可串行化在普通可串行化之上添加了一个简单的额外约束。如果事务Y在事务X完成后启动(请注意,这意味着X和Y根据定义不是并发的),那么保证严格可串行化的系统保证:
(1)最终状态等同于按顺序处理事务和
(2)X必须在该序列顺序中的Y之前。
因此,如果我要向保证严格可串行化的系统提交事务,我知道该事务的任何读取都将反映由于已提交的事务(至少)到达我的事务处理时的数据库的状态。提交。事务可能看不到同时提交的事务的写入,但至少它会看到在它开始之前完成的所有写入。

当数据库位于一台机器上时,保证严格的可串行化通常不会增加相对于普通可串行化的工作量。在进行交易之前,每个现有系统(一个例外是Daniel Abadi博士的2014年“懒惰交易”论文)必须执行该交易事务中涉及的写入。对于稍后出现的事务,忽略这些写入(而不是看到它们)通常比看到它们更多的工作。因此,几乎每个在一台机器上运行的可序列化系统也保证了严格的可串行化。这导致了行业现状,数据库系统的文档表明它们保证了“可串行性”,但实际上,它们保证了“严格的可串行性”。

“严格的可串行化”保证消除了系统返回我们在上一节中讨论过的陈旧/空数据的可能性。[但是,它没有消除我们在该部分讨论过的副本分歧问题。我们将在以后的帖子中回到这个问题。]

总结到目前为止:实际上,“严格可串行化”是数据库系统中的黄金标准隔离级别。但是,只要大多数数据库在一台机器上运行,在保证“可串行化”的系统和保证“严格可串行化”的系统中没有可察觉的差异。因此,通俗地说,“可串行化”被称为黄金标准,这种口语化的语义不准确性从未如此重要。

分布式系统使可串行性变得危险
在分布式系统中,从可串行化到严格可串行化的跳跃不再是微不足道的。
以我们的售票应用程序的简单示例为例:
假设票证库存在两台机器上复制 --A和B ----两者都允许处理交易。客户向机器A发出减少库存的事务。此事务完成后,将向计算机B发出另一个事务,以读取当前库存。在一台机器上,第二个事务肯定会读取第一个事务的写入。但是现在他们在不同的机器上运行,如果系统不能保证一致的读取跨机器,第二个事务很可能会返回一个陈旧的脏值(例如,如果复制是异步的,并且第一个事务的写入尚未从A复制到B,则可能会发生这种情况)。这种陈旧的读取不会违反可串行化。它相当于第一个事务之前的第二个事务的序列顺序。但是,由于第二次交易事务是在第一次交易完成后提交的,因此肯定违反了严格的可串行性。

因此,当跨机器复制数据时,可串行化和严格的可串行化之间存在巨大而实际的差异。在选择要使用的数据库系统之前,最终用户必须意识到这种差异。无法保证严格可序列化的分布式系统容易出现许多不同类型的错误。​​​​​​​

可序列化和严格可序列化之间的区别远远超过我之前示例中的“陈旧读取”错误。让我们看看可能出现的另一个错误,称为“因果反转”。

如果您存入的金额(在您的所有账户中)超过5000美元,银行会“free checking免费检查”。爱丽丝正好5000美元,需要给孩子的保姆签发支票,所以她将一些额外的钱转入她的储蓄账户。在确认转移成功并且钱存入她的储蓄账户后,她从她的支票账户中提取支票。当她看大结果时,她发现因为没有在5000美元以上的账户中保持平衡而受到的处罚。

所以发生了什么事?违反严格的可串行性。两个交易---她的储蓄账户的增加和她的支票账户的扣除被重新排序,以便在储蓄账户添加之前发生支票账户扣除,即在加法完成之后提交减法。在仅保证普通可串行化的系统中,这种重新排序是完全可能的,但在保证严格可串行化的系统中是不可能的。在这种类型的示例中,这种事务重新排序是非常常见的,其中如果不相交的数据位于分布式系统中的不同机器上,则事务访问不相交的数据(检查余额与储蓄余额)。交易事务重新排序后,Alice在两个账户的余额暂时低于5000美元,这导致了处罚。如果银行在一个保证严格可串行化的系统之上构建他们的应用程序,他们将不必处理来自Alice的愤怒投诉电话。

FaunaDB:所有交易的严格可串行化
很少有分布式数据库系统声称可以保证严格的可串行化的隔离级别。实际上,只需更少的产品即可实现这一保证。FaunaDB就是其中之一。
FaunaDB现在支持配置,使所有事务 - 包括读写和只读事务 - 以严格可序列化的隔离运行。
FaunaDB对严格可串行化的保证也是市场上最强大的。由于严格可串行化的定义基于实时(如上所述:在实时完成其他事务后提交的事务必须遵守其各自的顺序),其他供应商在分布式系统中的不同机器上使用本地时钟为了执行保证。遗憾的是,基于此方法的方法仅与跨机器的时间同步协议一样好,并且当同步算法暂时超过对最大时钟偏差的假设时,容易发生拐角情况违规。FaunaDB实现严格可串行化的方法也适用于多区域环境,并且类似于通过其可扩展的分布式统一日志实现一致性的方式。我们将在以后的文章中深入探讨技术细节。

​​​​​​​最重要的是,如果将FaunaDB配置为对所有事务严格可序列化,那么过时的读取将永远不会发生。应用程序发出的每个读取都保证反映在发出读取之前已完成的事务的写入。无论FaunaDB部署中机器的本地时钟存在多少偏差,都是如此。即使一台机器认为当年是1492年,该机器提供的任何读取都将反映所有已提交的写入。此外,FaunaDB中的事务永远不会在完成的事务之前重新排序 - 即使它们触及位于不同机器上的完全不相交的数据集。因此,我们之前看到的银行交易重新排序问题保证永远不会出现在FaunaDB中。
因此,FaunaDB对严格可串行化的支持使其成为该市场上最安全的分布式(可扩展)数据库系统。