2. 数据库系统揭秘 Part-2:可串行化隔离级别的细分以及可能遇到的并发异常

本文探讨了在分布式数据库中,尤其是在保证可串行化隔离级别时,可能出现的并发正确性异常,如不朽写、陈旧读和因果倒置。文章介绍了这些异常的原理,以及如何通过严格的序列化保证来避免这些错误。
摘要由CSDN通过智能技术生成
  • 原文链接:https://fauna.com/blog/demystifying-database-systems-correctness-anomalies-under-serializable-isolation

  • 一些术语的翻译:

    • Serializability/Serializable:可串行性、可序列化
    • Anomaly:异常
  • Titile:可串行化隔离级别下可能遇到的并发正确性异常

许多数据库系统支持多种隔离级别,使得用户可以在暴露于各种类型的应用异常和错误与(可能很小的)潜在事务并发性增加之间进行权衡。几十年来,商业数据库系统提供的最高级别的 "bug-free correctness "是 "SERIALIZABLE "隔离,即数据库系统并行运行事务,但其方式等同于事务一个接一个地运行。这种隔离级别被认为是 “完美的”,因为它使在数据库系统之上编写代码的用户不必考虑并发可能导致的错误。只要给定的事务代码是正确的,即:如果没有其他事务同时运行,事务会将当前数据库状态从一个正确的状态带到另一个正确的状态(这里的 "正确 "是指不违反应用程序的任何语义),那么可串行化隔离就能保证并发运行的事务不会导致任何类型的竞态条件发生(从而使数据库进入不正确的状态)。
In other words, serializable isolation generally allows an application developer to avoid having to reason about concurrency, and only focus on making single-threaded code correct.

在 "数据库服务器 "运行于单台物理机的美好时代,可串行化的隔离确实足够了,数据库供应商也从未试图出售比 SERIALIZABLE 更强正确性保证的数据库软件。然而,随着分布式和复制式数据库系统在过去几十年中开始大量出现,即使在保证可串行化隔离的数据库系统上运行,应用程序中也开始出现异常和错误。因此,数据库系统供应商开始发布比可串行化隔离具有更强正确性保证的系统,并承诺不会受到这些新异常的影响。在本文章中,我们将讨论可串行化分布式数据库系统中几个众所周知的错误和异常,以及确保避免这些异常的现代正确性保证。

在分布式/复制系统下中,可串行化(可序列化)意味着什么?

我们在上文将 "可串行化隔离 "定义为一种保证,即使数据库系统允许并行运行事务,最终结果也等同于事务一个接一个地运行。在复制系统中,这种保证必须得到加强,以避免出现非复制系统中只有在隔离度较低时才会出现的异常情况。

  • 举例:现在假设 Alice 的账户中余额为$50,并且该值通过Replicate复制的方式分别存储在了欧洲和美国的两个数据中心中。许多系统不会在如此长的距离上(快速)同步复制数据。相反,事务会先在一个区域完成,然后再将其更新复制到数据库系统。
    • 如果此时同时在两地进行取钱操作:withdraw $20,两地都读取到旧值50,然后分别更新新值为30,然后更新写回到数据库系统中进行同步。
    • 这个最终余额显然是不正确的,它应该是 10 美元而非30,这是由并发执行的事务造成的。但事实上,即使事务是串行的(一个接一个),只要复制不作为事务的一部分(而是发生在事务之后),也会出现同样的结果。
  • Therefore, a concurrency bug results despite equivalence to a serial order.

1984 年,Rony Attar、Phil Bernstein 和 Nathan Goodman 等人扩展了可串行化的概念,以定义复制系统中的正确性。其基本思想是,一个数据项的所有副本都像一个逻辑数据项。当我们说事务的并发执行 "等同于以特定的序列顺序处理它们 "时,这意味着每当读取一个数据项时,返回的值将是前一个事务按照(等同的)序列顺序对该数据项写入的最新值——无论写入的是哪个副本。在这种情况下,"最近的写入 "指的是序列顺序中最靠近的(前一个)事务的写入。

  • 在我们上面的例子中,无论是在欧洲的取款还是在美国的取款都会在等效的串行顺序中被排在第一个。无论哪个交易是第二个,当它读取余额时,它必须读取第一个交易写入的值。
  • Attar 等人将这种保证命名为 "单副本One-Copy可串行化 "或 “1SR”,因为隔离保证等同于在每个数据项都有 "一份 "的无复制系统中的可串行化。
可串行化隔离下面临的异常问题;单拷贝可串行化下面临的异常问题

我们刚才说过,复制系统中的单拷贝可串行化性 应该与非复制系统中的可串行化性具有相同的隔离保证。但实际情况下,提供 "可串行化 "隔离级别的数据库系统有很多很多,但提供 "单拷贝可串行化 "隔离级别的复制数据库系统却很少(如果有的话)。要理解为什么会出现这种情况,我们需要解释在 "仅 "保证可串行化的系统之上编写无错误程序所面临的一些挑战。

可串行化系统只保证事务将以某种序列顺序的等效方式处理。事实上,可串行化保证本身并不对事务序列顺序施加任何限制。理论上,一个事务可以运行并提交。另一个事务可以在第一个事务提交后很长一段时间才出现——并且以这样一种方式被处理,使得最终等效的串行顺序将后来的事务排在了先前的事务之前。从某种意义上说,后来的事务“时间旅行time travel”回到了过去,并且被处理,使得数据库的最终状态等同于该事务在开始之前已完成的事务之前运行的状态。可串行化系统并不能避免这种情况。单拷贝可串行化系统也不能防止这种情况。不过,在单服务器系统中,防止时间旅行既简单又方便。因此,绝大多数保证 "可串行化 "的单服务器系统也能防止时间旅行。事实上,防止时间旅行是一件微不足道的小事,以至于大多数商业可串行化系统都不认为这有什么值得注意的,以至于没有记录它们防止这种行为的情况。
相比之下,在分布式和复制系统中,要保证没有时间旅行就不那么容易了,许多系统在其事务处理行为中允许某种形式的时间旅行。
In contrast, in distributed and replicated systems, it is far less trivial to guarantee a lack of time travel, and many systems allow some forms of time travel in their transaction processing behavior.

接下来的几节将介绍分布式系统/复制系统中可能出现的一些时间旅行导致的并发异常,以及它们可能导致的应用程序错误类型。在只保证单拷贝可串行化的系统中,所有这些异常情况都有可能发生。因此,供应商通常会记录他们允许和不允许的异常情况,从而有可能保证高于单拷贝串行化的正确性级别。在本文章的最后,我们将对不同的正确性保证及其允许的时间旅行异常情况进行分类。

The Immortal Write,永生/不朽 写异常
  • 例子1:设计概念:Time Travel、Blind Writing
    • 假设一个应用程序的用户目前的显示名称是 “Daniel”,但他决定将其更改为 “Danny”。他进入应用程序界面,相应地更改了自己的显示名称。然后,他阅读了自己的个人资料以确保更改生效,并确认已生效。两周后,他又改变了主意,决定将显示名称改为 “Danger”。他进入界面,相应地更改了自己的显示名称,并被告知更改成功。但当他对自己的个人资料进行读取时,显示的名字仍然是 “Danny”。他可以返回并更改姓名无数次。每一次,他都会被告知更改成功,但他在系统中的显示名称值仍然是 “Danny”。
      [图片]
    • 发生了什么?所有未来写入他名字的事务都回到了序列顺序中的某一点,即直接在将他的名字改为 "Danny "的事务之前。因此,"Danny "事务覆盖了所有其他事务写入的值,尽管它在实时时间上比其他事务早得多。系统认为,它所保证的序列顺序是 "Danny "事务排在所有其他改名事务之后–它完全有权力在不违反其可串行化保证的情况下做出这样的决定。
      • 题外话:当 "Danny "事务和/或其他更名事务在写入名称的同一事务中也对数据库进行读取时,在不违反可序列性的情况下进行时间旅行就变得更加困难了。但对于这些示例中的 "盲写 "事务,时间旅行很容易实现。
    • 在多主节点的异步复制数据库系统中,允许在任一副本中进行写入操作,因此有可能出现跨副本的冲突写入。在这种情况下,利用时间旅行来创建不朽的盲写很有吸引力,它可以在不违反可串行化保证的情况下直接解决冲突。
The Stale Read,陈旧读异常
  • 例子2:
    • 最常见的异常类型是 “陈旧读异常”,它出现在复制系统中,但不出现在可串行化的单服务器系统中。例如,查理有一个银行账户,账户中还有 50 美元。他去自动取款机取了 50 美元。之后,他要了一张写有当前银行余额的收据。收据上(错误地)写着他的账户里还有 50 美元(实际上,他现在已经没有钱了)。结果,查理对自己有多少钱产生了错误的印象,并可能在现实生活中犯下行为错误(例如,花钱购买新的小玩意儿),而如果他对自己的账户余额有正确的印象,他是不会这样做的。
    • 这个异常现象的发生是由于一次陈旧的读取:他的账户里以前肯定有 50 美元。但是,当自动取款机向银行数据库发出读取请求以获得他当前的余额时,这个读取请求并没有反映几秒钟前他从账户中取款时对余额的写入。
      [图片]
    • 在异步复制的数据库系统(如 MySQL 或 Amazon Aurora 中的读复制)中,陈旧读异常极为常见。写入(对查理余额的更新)被定向到一个副本,而该副本不会立即复制到另一个副本。如果在新的写入复制到另一个副本之前,读取就被定向到另一个副本,那么它就会看到一个陈旧的值。
    • 陈旧读取并不违反可串行化原则。系统只是将读取事务的时间移动到该数据项发生新写入之前的事务等效串行顺序中的一个时间点。因此,异步复制数据库系统可以在不放弃可串行化(甚至是单副本可串行化)保证的情况下允许陈旧读取。
  • 在单服务器系统中,除了读取数据项的最新值外,几乎没有读取其他内容(旧值)的动机。相反,在复制系统中,同步复制带来的网络延迟既耗时又昂贵。因此,异步复制很有吸引力,因为从异步只读副本中读取数据不会违反可串行化原则(只要复制数据的可见顺序与原始数据相同)。
The Causal Reverse,因果倒置异常
  • 例子3:
    • 与 "陈旧读取异常 "不同,"因果反向异常 "可能发生在任何分布式数据库系统中,并且与复制方式(同步或异步)无关。在因果反向异常中,由先前写入引起的后一次写入可能会在时间上旅行到先前写入之前的序列顺序中的某一点。一般来说,这两次写入的数据项可能完全不同。在这两次写入之间的串行顺序中发生的读取可能只看到 "果 "而看不到 “因”,从而导致应用程序错误。
    • 例如,大多数银行不会在一个数据库事务中进行账户间的资金交换。而是在一个事务中将钱从一个账户转入银行自有账户。第二个事务再将钱从银行自有账户转到作为这次转账目的地的账户。第二个事务是由第一个事务引起的。如果第一个事务因任何原因没有成功,第二个就永远不会发生。
      • 假设有 100 万美元从账户 A(目前有 100 万美元,转账后将剩下 0 美元)转入账户 B(目前有 0 美元,转账后将有 100 万美元)。假设账户 A 和账户 B 为同一实体所有,该实体希望获得一笔大额贷款,首付需要 200 万美元。为了确定该客户是否符合贷款条件,贷款方会发出一个读取交易,读取账户 A 和账户 B 的值,并取这两个账户的余额之和。如果该读取交易发生在 A 账户向 B 账户转账 100 万美元之前,则将观察到各账户的总额为 100 万美元。如果该读取交易发生在将 100 万美元从 A 转入 B 之后,则各账户中仍会出现 100 万美元的总额。如果该读取交易发生在上述从 A 向 B 转账 100 万美元的两笔交易之间,则在各账户中观察到的总金额为 0 美元。在所有这三种可能的情况下,该实体都会因缺乏首付所需的资金而(正确地)被拒绝贷款。
      • 然而,如果参与转账的第二个事务(向账户B添加100万美元的那个)在导致其存在的事务(从账户A扣除100万美元的那个)之前进行了时间旅行,那么在这两次写操作之间发生的读取事务可能会(错误地)观察到账户间的余额为200万美元,从而允许该实体获得贷款。由于转账是在两个单独的事务中执行的,这个例子并没有违反可串行化。等效的串行顺序是:(1)执行转账第二部分的事务(2)读取事务和(3)执行转账第一部分的事务。然而,这个例子展示了如果允许引起事务的事务时间旅行到其原因之前的时间点,应用程序代码中可能出现严重的错误。
    • 这种因果反转异常通常出现在分区数据库系统中,这些系统中在特定分区上运行的事务根据该分区的本地时间接收一个时间戳。如果这些系统在提交事务之前不等待最大时钟偏差界限(系统中不同分区的本地时间可能的最大差异)的话,就有可能出现一个事务提交后,另一个事务随后出现,而这个后来的事务是由先前的事务引起的(它在先前的事务完成后才开始),并且仍然获得比先前事务更早的时间戳。这使得读操作有可能看到后来事务的写操作,但看不到先前的写操作。
      • 换句话说,如果我们讨论的银行示例是在一个允许因果反向的系统上实现的,那么希望获得贷款的实体就可以简单地重复提出贷款请求,然后在账户 A 和账户 B 之间转账,直到因果反向异常出现,贷款获得批准。显然,一个精心编写的应用程序应该能够检测到重复的贷款请求,并防止这种黑客行为的发生。但一般来说,很难预测所有可能的黑客攻击,也很难编写防御性应用程序代码来防止这些攻击。此外,银行通常无法招聘到优秀的应用程序程序员,这就导致现实世界的应用程序中存在一些令人匪夷所思的漏洞。
避免时间旅行异常,Time-Travel Anomalies

我们迄今为止讨论过的三种异常情况–不朽写入、陈旧读取和因果反向–都是利用了可序列化保证中允许时间旅行的特性,从而在应用代码中引入了错误。为了避免这些漏洞,系统除了保证可序列化外,还需要保证不允许事务进行时间旅行。如上文所述,单服务器系统通常会在不做广告的情况下提供这种时间旅行保证,因为在单服务器上实现这种保证是微不足道的。在分布式和复制数据库系统中,在其他可序列化保证之外额外保证 "无时间旅行 "并非易事,但一些系统(如 Fauna/Calvin、FoundationDB 和 Google Spanner)已经做到了这一点。
This highest level of correctness is called strict serializability.

严格可串行性首先保证了我们上文讨论过的单拷贝可串行性。此外,它还保证:如果事务 X 在事务 Y 开始之前完成(实时),那么在系统保证等同的序列顺序中,X 将被放在 Y 之前。

可串行化系统的详细分类

保证严格序列化的系统可以消除所有类型的时间旅行异常。而在另一端,"仅 "保证单拷贝序列化的系统则容易受到我们在这篇文章中讨论过的所有异常情况的影响(尽管它们对我们在上一篇文章中讨论过的隔离异常情况具有免疫力)。还有一些系统能保证介于这两个极端之间的可序列化。

  • 1)一个例子是 "强会话可序列化 "系统,它能保证同一会话内事务的严格序列化,但除此之外只能保证单拷贝序列化。
  • 2)另一个例子是 "强写序列化 "系统,它保证所有插入或更新数据的事务都具有严格的序列化能力,但只读事务只有单拷贝序列化能力。这种隔离级别通常由只读副本系统实现,其中所有更新事务都进入主副本,由主副本以严格的可序列化方式进行处理。这些更新会按照在主副本处理的顺序异步复制到只读副本。从副本中读取的内容可能会过时,但它们仍然是可序列化的。
  • 3)第三类系统是 "强分区可序列化 "系统,它只保证每个分区的严格可序列化。数据被划分为多个互不相交的分区。在每个分区中,保证访问该分区中数据的事务是严格可序列化的。除此之外,系统只能保证单拷贝可序列化。这种隔离级别可以通过同步复制分区内的写入来实现,但要避免跨分区协调不相连的写入。

既然我们已经为这些不同程度的序列化赋予了名称,那么我们就可以用一张简单的图表来概括它们容易出现的异常情况:
可串行化隔离级别的分类

对于读过我上一篇关于隔离级别的文章的读者,我们可以将那篇文章中的隔离异常与这篇文章中的时间旅行异常结合起来,得到一个包含我们在这两篇文章中讨论过的所有异常的表格:
所有隔离级别分类及其对应的并发异常

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值