前言
本文翻译自网站:blog.thislongrun.com的一篇博客《The confusing CAP and ACID wording》,之所以会接触到该文章是因为它出现在CAP定理的Wiki百科中的【Refrence参见】。
正文
CAP和ACID都使用相同的词汇:原子性、一致性,等等。但是有个问题:这些词汇是一样的,但是他们的含义确是完全不同的。
CAP来自分布式系统理论,而ACID属于数据库的理论。分布式数据库同时使用CAP和ACID词汇,因此这显然会造成很多困惑。当有人说:“系统不应放弃一致性”,那么到底他说的一致性是什么?我们先来看看原子性-一致性-隔离性-持久性和一致性-可用性-分区容错性的定义。
ACID和CAP - 一个提醒
ACID性质是在70年代被确定的,于1983年术语ACID被创造。
- A for Atomicity
- C for Consistency
- I for for Isolation
- D for Durability
CAP定理在2000年由Eric Brewer提出,于2002年被Seth Gilbert和Nancy Lynch证明。
- C for Consistency
- A for Availability
- P for Partition-tolerance.
CAP和ACID措辞 - 混乱概述
首先,来看看全景图
词汇 | 数据库 | CAP | 困惑 |
---|---|---|---|
事务Transaction | 一组操作 | 没有使用该词汇或概念 | 无 |
持久性Durable | “一旦一个事务成功完成,它对状态的改变在故障后依然存在。” | 没有使用该词汇或概念 | 无 |
一致性Consistent | 数据上的完整性约束(数据类型、表…) | 对于CAP,一致性是“原子一致性”的简称。原子一致性是一种一致性模型,是不同的概念 | 同一个词,但不同概念 |
隔离性Isolation | “即使事务可能并发执行,但对每个事务而言,其他事务在事务T之前或之后执行” | 在CAP,没有使用。但是ACID中定义的单词“隔离”在CAP词汇表中是一个一致性模型。 | 不同词汇,相同概念 |
原子性Atomic | 所有的改变要么全部发生,要么一个都不发生 | 对于CAP,Atomic是一种一致性模型,CAP证明中使用的那个 | 同一个词,但不同概念 |
可用性Available | 不经常使用。使用时,定义于CAP的定义可能不一样。例如,可用性可能不需要所有没发生故障的节点响应。 | “系统中非故障节点接收到的每个请求都必须得到响应” | 相同词汇,相同概念,不同定义 |
分区Partition | 不经常使用。使用时,定义与CAP一样 | 两组节点之间所有消息丢失时,则两组节点被分区 | 无 |
现在,继续挖掘更多细节:我们将搞清楚对于分布式数据库额外的困惑来源。
事务(只存在于ACID)
一个事务时一组操作。任何一个操作都可用读写多个数据。如果ACID将赋予这组操作相同属性,使其看起来像一个唯一的操作一样。这不是CAP的目标。CAP是关于多个使用相同数据的操作,可能是复制,可能存在的属性。
持久性(只存在于ACID)
“一旦一个事务成功完成,它对状态的改变在故障后依然存在。“十分清晰,但是把故障描述留给了物理部署。这主要依赖于冗余:单一节点的多个磁盘and/or多个节点and/or多个站点。”Survive“并不意味着任何可用性概念。它的意思是,以后至少有可能恢复数据。
CAP自身并没有提及持久性。CAP中的持久性是隐含的”:CAP是关于分区的,而不是节点故障的。
CAP中的可用性
在CAP中,达到可用性意味着所有未故障的节点在系统发生分区时依然继续服务请求。许多分布式系统自认为是可用的:如果存在一个分区有一些未故障的节点继续服务请求。但这些系统并不是CAP意义上的可用性。
CAP的一致性和CAP的原子性
CAP中的一致性是原子一致性的简称。原子一致性是一种一致性模型。一个一致性模型描述了如何对系统的操作进行排序。操作列表取决于系统。举个例子,一个事务系统的一致性模型是有可能被定义出来的,因此说“提交”是其中一个操作。CAP定理的证明正是由Lynch在[V6]中定义的分布式共享内存模型上完成的,并使用读/写/ack。
一致性模型的选择绝非易事。之所以存在许多一致性模型是因为由许多权衡的可能性:
- 使用一致性模型的有多容易。而这又取决于应用本身:对于某些应用,某些一致性模型可能比其他应用更容易使用。
- 内存模型的实现效率如何。而这通常又取决于硬件和物理部署。
在ACID和CAP中使用的一致性模型实际上很简单: - 顺序一致性,由Lamport V9所定义:
程序的行为就像所有进行的内存访问一样是交错的,然后依次执行。 - 原子一致性(也叫,线性一致性linearizability)是顺序加上实时限制:“与顺序一致性不同,线性一致性隐含假设跨进程的可观测的全局时间的概念。操作由一个间隔来建模,该间隔由操作的调用和响应之间的一段时间组成,并假设每个操作在该间隔内的某个时间点立刻生效。
CAP说”一致性Consistent"对于原子一致性:就是个简称。CAP中的原子性(操作的顺序)和ACID中的原子性(要么全部、要么都不)完全不是一回事。
【个人理解,ACID中的原子性是对一组操作的打包,要么都成功,要么都不成功。而CAP的原子性,是指操作的执行顺序,进一步说,是对于整个分布式系统在可观测的全局时间下,如何对系统中的操作执行顺序的定义。参考上面对原子一致性的介绍。】
ACID中的一致性
ACID中的一致性涉及数据完整性。举个例子,通常可以在SQL数据库中实现以下规则:
- 该字段不为空
- 该字段是一个数值类型
- 该字段是对另一个表中的另一个字段的现有引用
数据库将不允许你提交破坏约束的事务。这就是ACID中的一致性契约。这个一致性的定义在CAP中没有等价的定义。
重要的是要记住,数据库(无论是否使用SQL)不会实现所有的一致性约束。引用Gray和Reuter[D2]的话:“底层系统无法检查存在的所有一致性约束。他们中大多数一开始就没有正式化”。
关于ACID的一致性的总结,再引用Gray和Reuter[D2]:“牢记这一点很重要:随着事务范式而来的一致性定义很大程度上是一个语法定义的。
ACID中的原子性
“要么全部要么都不”的行为非常直观:例如,以下模拟两个账号之间转账的事务
begin
val1 = read(account1)
val2 = read(account2)
newVal1 = val1 - 100
newVal2 = val2 + 100
write(account1, newVal1)
write(account2, newVal2)
commit
原子性意味着要么两个账号都被更新,要么都不更新。如果写入account1成功,而写入account2失败,则两个写入都会被回滚。
但是,具备ACID中的原子性,并不意味着该事务与其他事务是隔离的。换而言之,你可以声明说具备ACID的原子性,即使:
- 在事务提交之前,写入的值对于其他(事务)可见。
- 在事务期间,读取的值可以被其他(事务)修改。如果你多次读取同一个值时,可能拿到的是不同的结果。
举个例子,还是上面的事务,在很多SQL数据库中你可以预期的行为:
- 开始前:account1有1000,account是0
- 并行运行两个转账(用上面的代码)
- 你的预期是:account1有800,account2有200,然而你发现account1有900.
这是因为ACID的原子性不同于隔离性:原子性/要么全部要么都不并不意味着隔离。
隔离性
Gray和Reuter在[D2]中给出了隔离性的规范:“尽管事务并发执行,对于每个事务T来说,其他事务在T之前或者之后执行。”这定义了一个一致性模型,就像CAP中的一致性一样。格局这个定义,任何事务都是完全隔离的。这也容易理解和使用。【眼尖的同学可能发现了,这意味着并行的事务被串行提交,存在先后关系】
然而,这很难有效地实现,数据库必须放松约束。因此,一个ACID数据库中隔离性有好几个程度,即”可序列化“、“可重复读”、”读已提交“,”读未提交“。
最常用的默认的是”读已提交“:事务只能看到被提交的数据。
虽然看起来简单,但有几个问题:
- 这些定义是在考虑实现的情况下制定的。正如Hellerstein, Stonebraker和Hamilton所说[D1]:两者都以微妙的方式依赖于一个假设:使用锁定scheme的方案进行并发控制,而不是使用雷管或多版本并发方案。这意味着所提一的语义定义不清。
- 对于给定一个隔离级别,所有的数据库并不具有相同的行为。这是Martin Kleppmann在[D4]的解释。
- 隔离性的影响并不只是功能正确性,还有技术正确性:大多数数据库在后台使用锁。病房执行可能会产生死锁:独立事务之间的意外或未受管理的依赖会造成所有事务都需要另一个事务来释放它们自己的资源的情况。在这种情况下,其中一个事务将会被数据库引擎停止并失败,尽管这个事务在功能和语法上都是正确的。
最终数据库用户需要了解的远不止数据库一致性模型:他们必须明白他们使用的数据库引擎是实际上是如何实现的。
再回过头看看我们上面的例子。如果数据库使用”读已提交“(常用默认级别),我们可能得到不正确的记过。我们可能凭空生成的钱。而这可以通过显式地锁定该值来修复。数据库引擎对于一个”读已提交“的事务使用锁的伪代码可能是:
begin
val1 = readAndLock(account1)
val2 = readAndLock(account2)
newVal1 = val1 - 100
newVal2 = val2 + 100
write(account1, newVal1)
write(account2, newVal2)
commit // release all locks
随之而来的是Java等并发编程语言中已经遇到的许多复杂性。数据库实际上增加了额外的复杂度性,因为锁范围可以变得更宽泛(数据页、表…)并且可被数据库引擎动态修改(升级)。此外,死锁或性能要求可能导致那些应该只读取不可变数据的事务使用”读未提交“。这可能导致一个严重的问题:如果有人在某个时候(比方说,客户网站上的专业服务专家)在系统运行时修改了这些理论上不可修改的数据。
我们发现CAP中的”C“和ACID中的”I”很相似。但是CAP作为一个定理可以保留在模型中,而性能约束则迫使数据库添加多个级别的参数化,并迫使数据库用户理解隔离是如何实际实现的。
结论
在ACID首字母缩略词中的4个字母,3个字母的含义跟CAP中对应的字母含义不同。难怪容易困惑。并且,混淆不仅来资源措辞上的重叠,还来自于必须深入实现细节以理解实际并发应用程序中的差异。在实践中,这是导致许多人(包括我)看待“NoSQL”世界的困难之一。
#后记
有一些收获,但感觉还需要多看几遍。这些容易混淆的点,除了字母/单词相同,所描述的概念是不同层面的。