分布式系统设计领域有很多让人困惑的名词。很早以前我一直搞不清 ACID 中的 C 和 CAP 中的 C 到底什么关系。结果发现,没什么关系,只是一个美好的意外。今天,我们来在复习和巩固一下各种一致性级别的定义,以后要是不记得了,可以在回来看看。

什么是一致性级别

一致性级别(Consistency Level),主要是指在分布式环境中,各个节点(或者线程)对于数据的看法到底有多不一致。比如一个系统有 3 个节点 A,B,C。客户端 a 发送了一个请求将 X 设置为 1,被 A 收到了。客户端 b 发送了一个请求将 X 设置为 2,被 B 收到了。那么当一个客户端想要读取 X 的时候,不同的一致性级别中,客户端读到的值可能会不同。

常见的一致性级别

有很多不同种类的一致性级别。在我们常用的系统中,主要会见到以下几种。

  1. 最终一致性(Eventual Consistency)
  2. 因果一致性(Causal Consistency)
  3. 线性一致性(Lineariazable Consistency),也被称为强一致。
  4. 顺序一致性(Sequencial Consistency)

下面会分别介绍每一种的定义。

Eventual Consistency

最终一致性应该是大家经常遇到的一致性级别,也是最容易理解的一种。它的定义

If no new updates are made to a given data item, eventually all accesses to that item will return the last updated value

大部分系统都能满足这个级别,只要系统中各个节点会以某种方式正确的同步数据。平时我们常用的 Elastic Search 在同一个 index 的不同 replica 之前,用保证了最终一致性。它的优点是延迟低,因为客户端的写操作不需要其他节点感知就可完成。当然付出的代价也很明显,客户端没法知道一个数据是否是最新版本。同时如果有多个写操作操作同一个数据,但是分别被不同的节点处理的话,到底哪个写操作最终胜出是一个和实际应用相关的问题。如果你不觉得这是一个问题的话,建议你去读一读这一篇

Causal Consistency

那么为了解决上面这个问题,又提出了 Causal Consistency。它的定义

… that captures the causal relationships between operations in the system and guarantees that each process can observe those causally related operations in common causal order.

看定义有点绕。通俗一点解释就是说,假设进程 a 将 X 设置为 1。另一个进程 b 看到了 X=1 之后将 Y 设置为 2,那么这两个操作就构成了因果关系。任意进程如果看到 Y = 2 这个操作,那么它一定也能看到 X = 1 这个操作以及发生了。这个定义最开始应该是用于并发编程中,保证 “happens before” 这个语义,后来也被推广到了分布式系统中。

它在最终一致性的基础上又提供了更多的保证,避免出现一些很奇怪的情况。为了保证这个级别,有很多算法,比如 vector clock,CRDT 等等。但是对于一个写操作,这种级别还是难以保证各个客户端看到一致的结果。

Linearizable Consistency

Linearizable Consistency 是一种很强的一致性级别。它的定义在这里(很长,不想看的话直接往下看)。它保证了对于一个数据,所有的读写操作都在一个瞬间原子的完成。每一个读操作都能读到它完成的瞬间之前的所有写操作的效果。有了这种保证,我们就会发现,所有的 observer (发起读写操作的人)可以对这个数据的所有操作的全序关系达成共识。这句话有点绕,你可以多品一品。

上面特意强调了一个数据。这个数据的粒度在不同的系统中不同。粒度越大(比如整个数据库),保证越强,但是实现往往会影响并行度。粒度越小(比如一行),保证相对越弱,使用起来就越麻烦。

看完上面的定义,我相信你明白很强是什么意思了。HBase 中的对于一个 Row 的读写就是这种级别。它基本上可以理解成并发编程中原子的读写。

Sequencial Consistency

Sequencial Consistency 也是从并发编程中继承过来的概念。它的定义

property that requires … the result of any execution is the same as if the operations of all the processors were executed in some sequential order, and the operations of each individual processor appear in this sequence in the order specified by its program

在这种一致性级别下,对于数据的操作在观察者看来是有序的,但是不保证这个操作什么时候被某个观察者看到。比如假设一个进程 a 将 X 设为 1 将 Y 设为 2。另一个进程 b 之后将 X 设置为 3,Y 设置为 4。对于进程 c 来说,它可能在 b 修改完 X 和 Y 之后,还是看到 X = 1,Y = 2,再过了很久才看到 b 对两个值的更新。与此同时,可能存在进程 d 已经早早的读到了 b 的更新。但是不会有进程先看到 b 的更新之后,又看到 a 的更新。

Consensus 和上面的关系是什么

Consensus (共识)是为了保证多个节点对于状态达成一个共识。它往往被用于实现 Linearizable Consistency。常见的共识算法有 Raft, Paxos 等等。Consensus 往往是通过 Quorum 来实现的。我们用 Raft 来举个例子。因为 Raft 保证了一个 event 的有序序列,那么每当一个操作被 commit 了,Raft 集群中所有的节点被 query 是都会返回被 commit 之后的状态,很显然它可以保证 Linearizable Consistency,当然这个前提是这个操作要 block 直到被 commit。因此有一些系统只用 Raft 做 replication,但是写操作不会 block 以提高性能。

Isolation 又是什么呢

Isolation 是一个 RDMS 的概念,表示的是各个 transaction 之前的隔离程度。RDMS 中,如果没有合适的隔离性保障,会出现以下几种问题。

  1. 脏写(dirty write):事务回滚了但是写操作还是成功了。这个情况基本不能忍,所以所有的隔离性级别都不允许这种情况。
  2. 脏读(dirty read):事务执行过程中读到了没有 commit 的写。
  3. 不可重复读(unrepeatable read):事务执行过程中多次读取一条记录, 发现该记录中某些列值被修改过。
  4. 幻读(phantom read):事务执行过程中多次读取一个范围内的记录发现结果不一致。

以下介绍各种隔离等级以及它们解决的了什么问题

Read Committed Isolation

这个隔离级别保证只会读到 commit 的事务的写操作。这个可以通过将 commit log 在实际 commit 之后再 apply 实现。

Repeatable Read Isolation

这个可以通过行锁实现,一个 transaction 如果读了某一行,会给这一行上一个读锁。任意其他的 transaction 如果要写这一行,会被阻塞。

Seriazable Isolation

这种隔离级别让所有的 transcation 等效为顺序执行,往往通过给 key range 上锁实现。它可以避免 phantom read 的情况。

Snapshot Isolation

这种隔离等级又叫做 MVCC,它通过维护一个一直的 snapshot 实现,比如在 MySQL 中,一个记录会维护一个 version 的列表。在一个 transaction 中,所有 version 小于等于这个 transaction 的 version 并且 commit 了的修改才能被读取到。它看上去和 Seriazable Isolation 很像,但是有一点区别。考虑下面这个例子。

假设一张表里有 100 行记录,其中 50 行记录的性别 column 是男,另外 50 行记录的性别是女。现在有两个事务,一个将所有男的换成女的,另一个将所有女的换成男的。

上面的需求虽然很奇怪,但是说的是这个意思。

在 Seriazable Isolation 的情况下,最后 100 行记录要么全是男的,要么全是女的。

但是在 Snapshot Isolation 的情况下,最后 100 行记录还是 55 开。

总结

如果你看到这里,我相信你应该还是晕晕的。没关系,这很正常。

再多看几次,你就会习惯了。