- 原文链接:https://fauna.com/blog/demystifying-database-systems-introduction-to-consistency-levels
- 一些术语翻译:
- distributed/replicated system:分布式系统、(多副本)复制系统
- Title:对一致性等级的介绍
数据库系统通常允许用户以(损失一定的)正确性换取性能。在本系列的前两篇文章中,我们讨论了用正确性换取性能的一种特殊方法:数据库隔离级别。在分布式系统中,还有一个完全不同的类别可以用正确性来换取性能:一致性级别。越来越多的分布式数据库系统为用户提供了多种不同的一致性级别可供选择,从而允许用户指定特定应用需要系统提供哪些一致性保证。与隔离级别类似,较弱的一致性级别通常会带来更好的性能,因此也会带来与降低隔离级别相同类型的诱惑。
在这篇文章中,我们将简要介绍一致性级别——解释它们的作用和工作原理。关于一致性级别的大部分现有文献和在线讨论都是在多处理器或分布式系统的背景下进行的,这些系统每次只对单个数据项进行操作,没有 "事务 "的概念。相反,我们为如何在符合 ACID 标准的数据库系统中考虑一致性级别提供了一些指导。
什么是一致性等级?
一致性的定义从根本上说取决于上下文。一般来说,一致性指的是系统确保遵守(不出错)预定规则的能力。然而,这套规则会根据上下文发生变化。例如,ACID 的 C 和 CAP 的 C 都指一致性。然而,这两种上下文所隐含的规则却完全不同。
- 在 ACID 中,规则指的是应用程序定义的语义。一个能保证 ACID C 的系统能确保事务处理不违反参照完整性约束、外键约束和任何其他特定于应用程序的约束(如:“每个用户必须有一个名字”)。
- 相比之下,CAP 的一致性 C 指的是使并发分布式系统看起来像单线程集中式系统的相关规则。特定时间点的读取只有一种可能的结果:它们必须反映该数据项最近完成的写入(实时),无论该写入是由哪个服务器处理的。
目前我们可以消除的一个混淆点是,"一致性级别 "这一短语通常不会在 ACID 一致性的上下文中使用。这是因为 ACID 的 C 几乎完全是应用程序开发人员的责任–只有开发人员才能确保他们放在事务中的代码在隔离运行时不会违反应用程序语义。
ACID实际上是一个用词不当——真正应该是AID,因为只有这三个(原子性、隔离性和持久性)属于系统保证的范畴。[Joe Hellerstein声称他被教导说ACID中的C只是为了制造一个LSD的双关语,但他承认这可能是杜撰的。]
LSD全称是Lysergic acid diethylamide(酸二乙胺酰赖氨酸),是一种常见的致幻剂,以其强烈的心理影响而闻名。在这里提到的LSD双关语是指数据库事务的ACID原则中的Consistency(一致性)被有意地包含以形成一个词,让它听起来像LSD这种药物的缩写,这是一种幽默或讽刺的说法。
当我们谈论一致性级别时,我们实际上指的是 CAP 的 C。在这种情况下,完美的一致性(通常称为 “严格一致性”)意味着系统能确保所有读取都能反映之前的所有写入,无论这些写入是在哪里执行的。任何低于 "完美 "一致性的一致性级别都会导致读取时无法返回数据项的最新写入信息。[题外话:原始 CAP 论文中的 C of CAP 指的是 “原子一致性”,它比严格一致性稍弱,但在实际应用中仍被认为是 "完美 "的。我们稍后将讨论两者的区别。]
同样的,根据特定系统的架构,完美的一致性变得更容易或更难实现。在设计不佳的系统中,实现完美的性能和可用性代价高得令人望而却步,因此这类系统的用户不得不接受远低于完美的保证。然而,即使在设计良好的系统中,接受不完美的保证也往往会带来非同小可的性能优势。
一致性等级概览
一致性级别的概念源自共享内存多处理器系统的研究人员。早期工作的目标是推理不同的执行线程(可能同时运行并访问共享内存中的重叠数据集)应如何以及何时查看彼此执行的写操作。因此,最初的大部分工作都集中在单个数据项的读写上,而不是事务中的读写组层面。我们将使用与最初的研究和模型相同的术语开始讨论,然后再讨论如何将这些想法应用到事务中。
Sequential Consistency,顺序一致性
在顺序一致性中,所有的写入–不管是哪个线程进行的写入,也不管是哪个数据项被写入–都是全局有序的。每个执行线程都必须看到按此顺序进行的写入。
- 举例:
- 1)如果一个线程先看到数据 X 被更新为5,然后又看到数据 Y 被更新为10,那么每个线程都必须在更新 Y 之前看到 X 的更新。
- 如果有任何一个线程看到了 Y 的新值,而看到了 X 的旧值,那么它就违反了顺序一致性。如下图所示
- 上面的四个线程中,
- P1、P2线程分别独立的更新 X Y 的值,并且不相互读取。
- P3线程首先读取到了X的新值,然后又读到了Y的旧值,再然后又读到了Y的新值。
- 在顺序一致性中,读到Y的旧值只能是X的更新要早于Y的更新。相反如果在时间顺序上,Y值先被更新,在P3已经读取到X的新值的前提下,就假定了Y值的更新在时间顺序上已经“可见”了
- P4线程看到了Y、X的新值,但它并不知道哪个值先被更新的
- 上面的四个线程中,
- 如果有任何一个线程看到了 Y 的新值,而看到了 X 的旧值,那么它就违反了顺序一致性。如下图所示
- 2)与上图例一所不同的
-
图2展示了一个不同的读取顺序:它就不满足顺序一致性
- 上面四个线程中,
- P1、P2线程职责不变
- P3线程首先读取到了X的新值,然后又读到了Y的旧值
- P4线程首先读取到了Y的新值,然后又读到了Y的旧值
- 上面四个线程中,
-
- 1)如果一个线程先看到数据 X 被更新为5,然后又看到数据 Y 被更新为10,那么每个线程都必须在更新 Y 之前看到 X 的更新。
一般来说,顺序一致性不会对写入顺序提出任何要求。不过,只要每个线程都同意将写入 Y 的操作视为发生在写入 X 的操作之前,顺序一致性就允许正式历史记录与实时历史记录不同(唯一的限制是,不能对来自同一执行线程的写入和读取操作重新排序)。有关示例,请参见图 3。
Strict Consistency,严格一致性
与顺序一致性不同,严格一致性对如何安排写入顺序提出了实时(Real-time)要求。
它假定总是可以零误差地知道当前时间–即每个执行线程都对当前的精确时间达成一致。顺序中的写入顺序必须等于这些写入操作发出的实时时间。此外,每次读操作都必须实时读取最近一次写操作的值–无论该写操作是由哪个执行线程发起的。在分布式系统(甚至是多处理器单服务器系统)中,实际上不可能就精确的当前时间达成全局一致,因此严格的一致性主要是理论上的问题。
上面的图1、图2和图3都不满足严格一致性,因为它们都包含了在x或y的值被写入一个新值之后对x=0或y=0的读取。然而,下面的图4满足严格一致性,因为所有读取都反映了实时的最新写入:
Linearizability,可线性化、线性一致性
在分布式/复制系统中,写入和读取可以来自任何地方,因此在实践中获得的最高级别的一致性是线性化(也称为 “原子一致性”,CAP 定理中就是这么称呼的)。线性化与严格一致性非常相似:两者都是顺序一致性的扩展,都对写入施加了实时限制。不同之处在于,线性化模型承认,从向系统提交操作到系统回复确认操作已完成之间存在一段时间上的开销。
在分布式系统中,将写请求发送到正确的位置(可能包括复制)可以在这段时间内发生。线性一致性保证不对开始和结束时间重叠的操作施加任何排序约束。唯一的排序约束是对于时间上不重叠的操作——只有在这些情况下,较早的写操作必须在较晚的写操作之前被看到。
-
以一个详细的例子来说明:
- 上面的图5展示了一个线性一致但不严格一致的调度示例。它不是严格一致的,因为进程P3对X的读取是在进程P1对X的写入之后立即启动(并返回)的,但仍然看到了旧值。然而,它是线性一致的,因为进程P3对X的读取和进程P1对X的写入在时间上是重叠的,因此线性一致性不要求进程P3的读取必须“立即”看到进程P1写入的结果。
虽然可线性化和严格一致性比顺序一致性更强,但顺序一致性本身就是一种很高的一致性水平,还有许多低于它的一致性水平。
Causal Consistency,因果一致性
因果一致性是一种常用且有用的一致性级别,略低于顺序一致性。在顺序一致性中,所有写入都必须全局排序–即使它们之间完全不相关。因果一致性不会强制对不相关的写入进行排序。但是,如果执行线程读取了某个数据项(称作 X),然后写入该数据项或另一个数据项(称作 Y),它就会假定随后的写入可能是由读取引起的。因此,它会强制执行 X 和 Y 的顺序–具体来说,所有执行线程都必须观察到 Y 的写入顺序在 X的写入之后。
-
以一个例子说明:
- 比较图 6 和图 2。在图 2 中,P3 线程看到写入 X 的操作发生在写入 Y 的操作之前,但 P4 线程看到写入 Y 的操作发生在写入 X 的操作之前。这违反了顺序一致性,但不违反因果一致性。
- 然而在图 6 中,P2 线程在执行对 Y 的写入之前读取了对 X 的写入。这就在对 X 和 Y 的写入之间设置了因果限制:Y 的写入必须发生在 X 之后。因此,当 P4 看到对 Y 的写入而没有看到对 X 的写入时,就违反了因果一致性。
Eventually Consistency,最终一致性
最终一致性甚至更弱:即使是因果关系相关的写入也可能不按顺序显示。例如,尽管图 6 违反了我们迄今为止讨论过的所有其他一致性保证,但并不一定违反了最终一致性。最终一致性的唯一保证是,如果在 "较长 "时间内没有写入("较长 "的定义取决于系统),每个执行线程都会同意最后一次写入的值。因此,只要 P4 最终能在稍后的某个时间点(图 6 中未显示)看到 X (5) 的新值,那么最终一致性就能保持。
Strong Consistency vs. Weak Consistency
严格一致性和线性一致性/原子一致性通常被认为是“强”一致性级别。在许多情况下,顺序一致性也被认为是强一致性级别。这些一致性级别的共同关键特征是数据库状态经历了一个普遍认同的状态变化序列。这允许复制的现实对最终用户是隐藏的。
本质上,用户对数据库的看法是只有一个数据库副本,它不断地向前进行状态转换。
而相比之下,较弱的一致性级别(如因果一致性和最终一致性)允许不同的数据库状态视图看到数据库状态中不同的步骤进展——除非数据库有多个副本,否则这显然是不可能的。
事务与一致性等级:Transactions and consistency levels
如上所述,一致性级别历来是根据数据项的单个读取或写入来定义的。这使得一致性级别的讨论很难应用到数据库系统中,因为在数据库系统中,读写操作组是在原子事务中一起发生的。事实上,在数据库系统中应用一致性级别时,许多研究文献和供应商文档(针对那些提供多种一致性级别的供应商)都非常混乱,没有采取统一的方法。
在存在数据库事务的情况下,推理一致性级别的最简单方法是对我们之前讨论的一致性模型进行一些小的调整。我们仍然将一致性视为执行线程向共享数据存储发送读写请求。唯一的区别是我们用发起每个请求的事务的事务标识符来标注每个读写请求。如果每个执行线程一次只能处理一个事务,并且事务不能被多个执行线程处理,那么传统的时间线一致性图表只需要补充表示每个事务在执行线程中的开始和结束点的矩形边界,如下图所示。
在一致性视图中,事务的存在增加了与 ACID 的 AID 相对应的额外约束:事务中的所有读写都同时成功或失败(原子性),它们与其他并发运行的事务隔离(隔离程度取决于隔离级别),已提交事务的写入将在各种系统故障后仍然有效(持久性)。
事务的原子性和持久性保证在认知上很容易与一致性保证区分开来,因为它们处理的是根本不同的概念。问题在于隔离性:
- 一致性保证规定了读取数据库状态的执行线程如何以及何时可以看到写入的内容。
- 隔离保证也对写入何时可见做出了限制。
- 隔离和一致性保证在概念上的重叠是造成许多混淆的根源。
- 在本系列的下一篇文章中,我将介绍如何理解隔离级别和一致性级别之间的区别。