上一篇介绍了 单机数据库 的 ACID 事务。下面将进入真正的难点:
“分布式环境”
分布式与单机最大的区别在于:单机是一个整体。组成这台机器的零件要么(看上去)都在正常状态,要么都不在状态,很少有例外。
在单机环境,底层系统(操作系统,HAL,BIOS 以及固件)既不会向软件报告有一个 CPU 或内存被拔出插槽,也不会报告某条系统总线面临堵塞。软件也不需要设计成在这种情况下继续工作。
即使遇到硬件故障,底层系统也会尽力处理,向软件维持一个机器“整体可用”的表象。如果这个故障无法处理,最终系统会向软件发送一个信号,例如著名的 Segmentation fault ——这多半会 kill 当前运行的程序。
而 分布式环境 天生就需要处理部分失效。原因很简单:分布式 = 多个节点。概率论告诉我们:系统的节点越多,单个节点发生意外故障的几率就越大。而且,这里没有 分布式操作系统 能帮忙处理这种意外。
因此,在分布式环境,软件要负责解决故障带来的部分失效。早在 2000 年,Brewer 教授就提出,部分失效遵循一个定理:
CAP 定理
缩写 CAP 代表 Consistency(一致性), Availability(可用性)和 Partition tolerance(分区容忍性),注意:这里的 Consistency 与 ACID 里的数据一致性(C)不一样。这里指数据在分布式节点的一致性。
CAP 定理断言,任何一个分布式系统都不可能同时满足 C-A-P 三个特性:
C -- 从每个节点读到相同的数据。
A -- 从任何位置发起的读写请求都能够成功返回。
P -- 即使出现分区(部分隔离),也能响应请求。
这是可以证明的。设想一下,当一个分布式系统发生了部分隔离:
节点被分隔到了两个区域:写入区域(1)的数据无法传递到区域(2),而访问区域(2)的请求也不能转发到区域(1)。
这时候,如果让左边的写入成功——优先保证可用性,则访问区域(2)的请求读不到最新一致的数据,违反了一致性。
反之,如果让写入失败(阻塞),或者彻底阻止外部请求访问区域(2)——则保证了数据一致性,但是损失了可用性。
因此,要同时保证一致性和可用性,区域(1)和区域(2)必须能够互相通讯。
这里产生了一个 困境:首先,分布式系统的软件必须在 C-A-P 三个特性之间做出选择。其次,一旦意外故障导致分布式系统部分隔离,软件又必须在可用性和一致性之间再做一次选择。
部分隔离是常见的。前面提到,意外故障会带来分布式系统的部分失效。不管这种失效是发生在一个机房还是一个节点,或者仅仅是一次网络超时 ——它都会导致机房与机房之间、故障节点和正常节点之间、超时节点和写入节点之间,出现时间或长或短的部分隔离。
因此,分布式系统必定经常要做这类选择。
回到我们关心的分布式事务。
事务 ACID 在分布式环境下也会有相同的问题。如果出现部分隔离,一个需要访问区域另一侧的 ACID 事务,要么选择回退,要么选择等待。回退会让事务直接失败。而等待会让事务无法结束,使得隔离性规定的那些需要事务结束才能对外可见的数据无法变成可见(例如:没有释放锁)。总之,两个选择都会带来可用性问题。
问题的症结就像前一篇文章说的那样:ACID 的目标是保证数据的一致性。这样,在分布式环境中,因为 CAP 定理的限制,满足 ACID 的事务系统只能选择 C-A 或者 C-P。这样,要么系统完全无法容忍分布式环境的部分失效,要么在发生部分失效时必须放弃可用性。
这是一个 分布式困境。
为了走出这个分布式困境,我们需要接触更多的理论。
敬请期待。