2021-06-01

六、一致性模型

 

1、概念

 

在“分布式系统”和“数据库”这两个学科中,一致性(Consistency)都是重要概念,但它表达的内容却并不相同。

 

  • 对于分布式系统而言,一致性是在探讨当系统内的一份逻辑数据存在多个物理的数据副本时,对其执行读写操作会产生什么样的结果,这也符合 CAP 理论对一致性的表述。
  • 而在数据库领域,“一致性”与事务密切相关,又进一步细化到 ACID 四个方面。其中,I 所代表的隔离性(Isolation),是“一致性”的核心内容,研究的就是如何协调事务之间的冲突。

 

当我们谈论分布式数据库的一致性时,实质上是在谈论数据一致性事务一致性两个方面。

 

2、数据一致性

 

数据一致性强调的是在分布式节点中,各个分片的数据是一致的或者看起来是一致的。

 

两个视角:

 

  • 状态一致性是指,数据所处的客观、实际状态所体现的一致性;
  • 操作一致性是指,外部用户通过协议约定的操作,能够读取到的数据一致性。

 

2.1、状态视角

 

从状态的视角来看,任何变更操作后,数据只有两种状态,所有副本一致或者不一致。在某些条件下,不一致的状态是暂时,还会转换到一致的状态,习惯上大家会把这种不一致称为“弱一致”。相对的,一致就叫做“强一致”了。

 

NoSQL 产品是应用弱一致性的典型代表,但对弱一致性的接受仍然是有限度的,这就是 BASE 理论中的 E 所代表的最终一致性(Eventually Consistency)。

 

2.2、操作视角

 

最终一致性,在语义上包含了很大的不确定性,所以很多时候并不是直接使用,而是加入一些限定条件,也就衍生出了若干种一致性模型。因为它们是在副本不一致的情况下,进行操作层面的封装来对外表现数据的状态,所以都可以纳入操作视角。

 

补充自《分布式金融架构课》

 

下面的前4个一致性都和会话(Session)有关。会话是个使用者的概念,而不是服务器端的概念。会话是用户的唯一标识符,通过会话可以判断是不是同一个用户。

 

在单机或者没有容灾的情况下,能不能判断出是同一个用户的作用不大。但是在有容灾的情况下,多台功能一样的机器会作为彼此的备份节点。这时候同一个用户的不同请求可能会被发送到不同的机器上处理。虽然这时候是多台机器在处理你的请求,但是从用户的角度来看,你需要保证最后的处理结果,和在一台机器上处理的结果是一样的

 

用户的请求可以分为读请求和写请求。

 

在简化版的容灾模型里,用户会往集群的主节点写入数据。主节点负责将数据复制到备份节点。在这里对于复制的同步和异步没有任何要求,对于复制节点的个数也没有要求,只要多于一个备份节点就行。

 

用户的读取请求比较复杂。用户既可以从主节点上读取数据,也可以选择从备份节点读取数据,也可以有时候从主节点读,有时候从备份节点读。读取哪个节点取决于用户和服务器之间的协议,也可能有一定的偶然因素。

 

2.2.1、写后读一致性

 

“写后读一致性”(Read after Write Consistency),它也称为“读写一致性”,或“读自己写一致性”(Read My Writes Consistency)。还可以称为“自读自写一致性”。

 

自己写入成功的任何数据,下一刻一定能读取到,其内容保证与自己最后一次写入完全一致,这就是“读自己写一致性”名字的由来。当然,从旁观者角度看,可以称为“读你写一致性”(Read Your Writes Consistency)。

 

 

2.2.2、单调读一致性

 

 

在小明发布照片后的瞬间,小红也刷新了朋友圈,此时读取到副本 R1,所以小红看到了照片;片刻之后,小红再次刷新,此时读取到的副本是 R2,于是照片消失了。小红以为小明删除了照片,但实际上这完全是程序错误造成的,数据向后回滚,出现了“时光倒流”。

 

想要排除这种异常,系统必须实现单调读一致性(Monotonic Read Consistency)。关于单调读一致性的定义,常见的解释是这样的:一个用户一旦读到某个值,不会读到比这个值更旧的值

 

实现单调读一致性的方式,可以是将用户与副本建立固定的映射关系,比如使用哈希算法将用户 ID 映射到固定副本上,这样避免了在多个副本中切换,也就不会出现上面的异常了。

 

2.2.3、单调写一致性(补充自《分布式金融架构课》)

 

单调写一致的英文名是 Monotonic Write。如果你往有容灾的集群里写了多次数据,单调写一致要求所有的节点的写入顺序和你的写入顺序完全一致。这样我们就能保证对于任何一个节点,它看到的别人的写操作和自己的写操作是完全一致的。

 

一个反例:

 

 

2.2.4、先读后写一致性(补充自《分布式金融架构课》)

 

先读后写的英文名是 Write follow Reads。前面三个一致性规定了一个会话的行为应该是怎样的。先读后写不同,它规定了多个会话之间互动应该满足怎样的一致性要求。

 

参见3.2.1节,单机隔离级别处理读写冲突时,都是存在多个会话的。但与这里的先读后写一致性不同,那里的两个会话都是各发生一个读或者写的操作;而先读后写一致性的两个会话,比如下图,用户2的会话包含了2次操作。

 

先读后写要求比较严格。假如你曾经读到了另一个人写入的结果,那么你想再写数据的话,你的写入一定要在另一个人的写入之后发生。也就是说,你们俩之间的写入有个先后顺序

 

你如果看到了另一个人的结果,就表示另一个人的写入是过去发生的事情,这时候如果你想再写点新东西进去,那么整个集群需要保证你们俩写入的先后顺序

 

一个反例

 

 

 

2.2.5、前缀一致性

 

 

小明和小红的评论分别写入了节点 N1 和 N2,但是它们与 N3 同步数据时,由于网络传输的问题,N3 节点接收数据的顺序与数据写入的顺序并不一致,所以小刚是先看到答案后看到问题。

 

显然,问题与答案之间是有因果关系的,但这种关系在复制的过程中被忽略了,于是出现了异常。

 

保持这种因果关系的一致性,被称为前缀读或前缀一致性(Consistent Prefix)。要实现这种一致性,可以考虑在原有的评论数据上增加一种显式的因果关系,这样系统可以据此控制在其他进程的读取顺序。

 

2.2.6、线性一致性

 

在显式声明无法奏效的情况下,如何寻找因果关系呢?更可靠的方式是将自然语意的因果关系转变为事件发生的先后顺序。

 

线性一致性(Linearizability)就是建立在事件的先后顺序之上的。在线性一致性下,整个系统表现得好像只有一个副本,所有操作被记录在一条时间线上,并且被原子化,这样任意两个事件都可以比较先后顺序。

 

这些事件一起构成的集合,在数学上称为具有“全序关系”的集合,而“全序”也称为“线性序”。

 

但是,集群中的各个节点不能做到真正的时钟同步,这样节点有各自的时间线。那么,如何将操作记录在一条时间线上呢?这就需要一个绝对时间,也就是全局时钟

 

补充自《分布式金融架构课》

 

线性一致性是分布式系统里最重要的一致性。你可以理解为线性一致性是分布式环境下的可串行化(Serializability)。

 

线性一致性所定义的环境里有一些程序,这些程序会执行一系列的操作,每个操作都有开始和结束的时间。

 

对于单个程序来说,它所有的操作之间没有时间上的重叠,也就是说属于同一个程序的两个操作不会并发执行。但是属于不同程序的操作可以在执行时间上有所重叠,比如说下面这幅图展示了 3 个程序一共 6 个操作的时序图:

 

 

线性一致性要求我们可以调整这些程序的操作开始和结束时间,调整的结果是所有程序的所有操作之间没有任何时间上的重叠(相当于串行化了)

 

线性一致性对时间的调整也有一个要求,那就是如果两个操作之间没有时间上的重叠,那么这两个操作之间的时间先后顺序不能发生改变

 

比如对于上图来说,一共有 3 个地方有时间重叠,因此这些彼此重叠的操作可以随意调整先后顺序。例子里还有两个地方有操作的先后关系,因此在调整顺序的时候,我们不能把这几个有先后关系的操作顺序搞反。

 

 

那调整之后就是线性一致性了吗?其实还不是。你还需要对调整之后的结果进行正确性验证。这里的正确性指的是业务逻辑的正确性。

 

当你把所有操作按照线性一致性的要求进行调整之后,所有操作可以看作是先后进行的,没有任何并发。所以,你可以按照业务逻辑来分析所有程序的所有操作是否合理,比如说加减钱是否正确,或者消息入栈出栈的顺序。

 

如果你发现逻辑不正确,就需要尝试另一种线性一致性调整的顺序。要是你尝试了所有调整的排列组合后,还是找不到一个正确的结果,那么整个过程就不是线性一致性了。即只要存在一个正确的调整后的结果就是线性一致性了

 

补充自《分布式金融架构课》。

 

于严格可串行化

 

单机情况下最强的一致性是可串行化。分布式情况下最重要的一致性是可线性化。那么把这两者结合起来,就得到了分布式情况下最强的一致性,叫作严格可串行化(Strict Serializability)。

 

我们再来重温一下可串行化的定义。可串行化表示两个事务里所有操作的执行结果等价于这两个事务的某一个顺序执行结果。这里对“某一个”并没有做任何限定。

 

而严格可串行化则对这个“某一个”做出了规定,它要求两个事务的运行结果等价于唯一一个顺序执行结果。在这个结果里,原来谁的事务先结束,那么在顺序执行的情况下谁的所有操作先结束。严格可串行化虽然有着极强的正确性保障,但是它的运行效率特别低,所以一般很少用到。

 

2.2.7、因果一致性

 

有没有不依赖绝对时间的方法呢?当然是有的,这就是因果一致性(Causal Consistency)。

 

因果一致性的基础是偏序关系,也就是说,部分事件顺序是可以比较的。至少一个节点内部的事件是可以排序的,依靠节点的本地时钟就行了;节点间如果发生通讯,则参与通讯的两个事件也是可以排序的,接收方的事件一定晚于调用方的事件。

 

多数观点认为,因果一致性弱于线性一致性,但在并发性能上具有优势,也足以处理多数的异常现象,所以因果一致性也在工业界得到了应用。

 

今天介绍的几种一致性模型,用一致性强度来衡量的话:线性一致性强于因果一致性;而写后读一致性、单调读一致性、前缀一致性弱于前两者,但这三者之间无法比较强弱。还有一种常被提及的顺序一致性(Sequentially Consistent),其强度介于线性一致性与因果一致性之间,由于较少在分布式数据库中使用,所以并没有介绍。

 

综上所述,我们提到的一致性模型强度排序如下:

 

线性一致性 > 顺序一致性 > 因果一致性 > { 写后读一致性/自读自写一致性,单调读/写一致性,前缀一致性,先读后写一致性 }

 

还有一些常见的弱一致性模型,包括有限旧一致性(Bounded Staleness)、会话一致性(Session Consistency)、单调写一致性(Monotonic Write Consistency)和读后写一致性(Write Follows Read Consistency)等。

 

 

2.2.8、一致性的选择——补充自《分布式金融架构课》

 

处理一致性,要考虑三种情况:单机、多机无备份和多机有备份。如何选择在叙述单机事务之后,参见4.1节。

 

3、事物一致性

 

3.1、 BASE

 

BA 表示基本可用性(Basically Available),S 表示软状态(Soft State),E 表示最终一致性(Eventual Consistency):

 

  • 基本可用性,是指某些部分出现故障,那么系统的其余部分依然可用。
  • 软状态或柔性事务,是指数据处理过程中,存在数据状态暂时不一致的情况,但最终会实现事务的一致性。
  • 最终一致性,是指单数据项的多副本,经过一段时间,最终达成一致。

 

关于最终一致性——补充自《分布式金融架构课》

 

为了说明一致性,我们要先弄明白什么叫作可见性(Visible)。假设有两台机器 A 和 B,这两台机器之间互相做备份。

 

如果你在机器 A 上对数据的修改,经过一段时间之后反映在了机器 B 上,这时候你的修改在机器 B 上就是可见的。一旦在机器 B 上是可见的之后,你就可以在机器 B 上使用在机器 A 上的修改结果。下面这幅图展示了可见性的意义

 

 

最终一致性的定义里的一致性指的是你的修改在所有机器上都是可见的。如果你的修改在一台机器上被看到了,那么这台机器就和原始的机器是一致的。

 

“最终”则定义了一致性的时间范围。它用到了数学上的极限(∞)概念。在有容灾的情况下,你对一台机器的数据修改会被慢慢复制到其他的机器。随着时间的推移,没有复制到数据的机器数目会越来越少。当这个时间是无穷大的时候,没有复制到数据的机器数目会降为零。

 

3.2、ACID

 

 

其中持久性的核心思想就是要应对系统故障。怎么理解系统故障呢?我们可以把故障分为两种。

 

  • 存储硬件无损、可恢复的故障。这种情况下,主要依托于预写日志(Write Ahead Log, WAL)保证第一时间存储数据。WAL 采用顺序写入的方式,可以保证数据库的低延时响应。WAL 是单体数据库的成熟技术,NoSQL 和分布式数据库都借鉴了过去。
  • 存储硬件损坏、不可恢复的故障。这种情况下,需要用到日志复制技术,将本地日志及时同步到其他节点。实现方式大体有三种:第一种是单体数据库自带的同步或半同步的方式,其中半同步方式具有一定的容错能力,实践中被更多采用;第二种是将日志存储到共享存储系统上,后者会通过冗余存储保证日志的安全性,亚马逊的 Aurora 采用了这种方式,也被称为 Share Storage;第三种是基于 Paxos/Raft 的共识算法同步日志数据,在分布式数据库中被广泛使用。无论采用哪种方式,目的都是保证在本地节点之外,至少有一份完整的日志可用于数据恢复。

 

3.2.1、隔离级别

 

隔离级别最终是为了解决一致性的问题,不同的隔离级别可以得到不同级别的一致性。但是隔离级别直接处理的是数据操作的冲突。

 

补充自《分布式金融架构课》。

 

在数据库理论里对不正确的分级叫作隔离级别(Isolation Level)

 

如果两个操作对应的是不同的对象,那么这两个操作不会有任何冲突。所以两个操作如果要有冲突,一定是它们操作了同一个对象。另外,如果两个操作都是读操作,也不会出现任何冲突。

 

所以我们可以这样理解冲突,两个操作如果冲突,一定有一个操作是写操作;发生冲突的时候,一定是一个操作先发生,另一个后发生(我们称后面的操作依赖于前面的操作)。排除两个操作都是读的情况,就只剩下了 3 个情况,分别是读写、写读和写写这三种。

 

 

 

 

最早、最正式的对隔离级别的定义,是 ANSI SQL-92(简称 SQL-92),它定义的隔离级别和异常现象如下所示:

 

 

 

3.2.1.1、解决写写冲突——读未提交(补充自《分布式金融架构课》)

 

最低的隔离级别 Read Uncommitted 解决了脏写(Dirty Write)的问题。脏写指的是两个事务写了同一份资源,这样后写的事务会覆盖先写的内容。如果可以读取到还没有提交的数据,在它的基础上再进行更新,这样就不会脏写,即不会覆盖别的写操作了。其实脏写就是上面提到的写写冲突,示意图如下:

 

 

3.2.1.2、解决写读冲突——读已提交(补充自《分布式金融架构课》)

 

这个隔离级别除了解决脏写问题以外,还解决了脏读(Dirty Read)问题。

 

脏读指的是当一个未结束的事务写了一个值之后,另一个事务读取了这个值。一旦前面的事务通过回滚取消了自己的所有操作,那么后面的事务就会读取到一个不应该存在的值,也就是读了一份脏数据。

 

 

3.2.1.3、解决读写冲突——可重复读(补充自《分布式金融架构课》)

 

它相对于前一个级别也多解决了一个模糊读(Fuzzy Read)的问题,其实就是前面提到的读写冲突。

 

读写冲突和写读冲突刚好相反。读写冲突发生的时候需要负责写的事务提交,而写读冲突需要写的事务回滚。那为什么要叫这个名字呢?

 

原因是读的事务如果再读一次的话,会将另一个事务写入的值读回来,因此前后两次读到的结果会不一致。示意图如下:

 

 

 

写读冲突发生的前提是先发生了写操作,后发生了读操作,此时要处理的冲突是如果之前的写操作回滚了,现在的读操作该怎么办。即过去影响现在的问题

 

读写冲突发生的前提是先发生了读操作,后发生了写操作。此时要处理的冲突是如果后面的写操作提交了,之前发生的读操作是否可以读取到这个后写入的值。即未来影响现在的问题

 

两者的共性是由于写操作改变了数据的状态,它们要处理这个改变对过去和现在的影响

 

3.2.1.4、小结写写、写读、读写冲突

 

 

3.2.1.5、可串行化(Serializability)——补充自《分布式金融架构课》

 

写写、写读、读写冲突这些所有冲突都已经解决了,那可串行化还要解决什么问题?可串行化相当于把一个大的正确性问题,分解成了以事务为单位的小正确性问题,通过分而治之的办法来降低正确性成本。即它主要用于分析问题——在解决这些读写冲突问题的基础上。

 

可串行化规定了这些同时在运行的事务的结果,它要求这些并发执行的事务的最终结果永远等同于它们某个顺序执行的结果。

 

顺序执行

 

一个例子,下面的图上有两个事务,这两个事务交互地读写 x 和 y :

 

这时候我可以调整这两个事务的读写操作,把第一个事务里所有的操作都放到第二个事务的前面,就像下面这幅图展示的一样:

 

 

注意,我们在可串行化中还有一个关键的定语是“某个”。这意味着我们只要找到一个等价的顺序执行结果就可以,这个结果不一定唯一。这也说明可串行化也具有一定的随机性。2.2.6节说的严格可串行化(Strict Serializability)的隔离级别,它可以消除这种不确定性。

 

3.2.1.6、冲突可串行化——补充自《分布式金融架构课》

 

通常使用的关系型数据库用的是另一种叫作冲突可串行化(Conflict Serializability)的调度方案。

 

这里的“冲突”就是我们开始提到的读写、写读和写写这 3 种冲突。冲突可串行化依然要求等价于某个事务串行化的结果。但是它和可串行化不一样,可串行化只需要你找到一个等价的串行结果就行,而冲突可串行化要求你通过一系列无冲突的互换过程将原来的执行序列变为等价的串行执行。

 

如果两个操作之间没有冲突,你可以互换他们的顺序,也叫无冲突互换过程。所以一共有两种情况。一种是两个操作的对象不一样,这样不管是读写都不会有冲突,你可以随便调整。另一种情况是两个操作都是读操作。

 

一个调整的例子:

 

冲突可串行化是可串行化的充分条件:如果一个事务是冲突可串行化,那么它一定是可串行化。反过来,如果一个事务是可串行化,那它可能不是冲突可串行化。

 

所以冲突可串行化的集合是可串行化的子集,就像下面这幅图展示的一样:

 

 

3.2.1.7、Critique:更严谨的隔离级别。

 

幻读和写倾斜。

 

在 SQL-92 中可重复读(Repeatable Read, RR)与可串行化(Serializable)两个隔离级别的主要差别是对幻读(Phantom)的处理。这似乎是说,解决幻读问题的就是可串行化。但随着 Critique 的发表,快照隔离被明确提出,这个说法就不适用了,因为快照隔离能解决幻读的问题,但却无法处理写倾斜(Write Skew)问题,也不符合可串行化要求。

 

因此,今天,使用最广泛的隔离级别有四个,就是已提交读、可重复读、快照隔离、可串行化

 

Critique 对幻读的描述大致是这样的,事务 T1 使用特定的查询条件获得一个结果集,事务 T2 插入新的数据,并且这些数据符合 T1 刚刚执行的查询条件。T2 提交成功后,T1 再次执行同样的查询,此时得到的结果集会增大。这种异常现象就是幻读。

 

不少人会将幻读与不可重复读混淆,这是因为它们在自然语义上非常接近,都是在一个事务内用相同的条件查询两次,但两次的结果不一样。差异在于,对不可重复读来说,第二次的结果集相对第一次,有些记录被修改(Update)或删除(Delete)了;而幻读是第二次结果集里出现了第一次结果集没有的记录 (Insert)。一个更加形象的说法,幻读是在第一次结果集的记录“间隙”中增加了新的记录。所以,MySQL 将防止出现幻读的锁命名为间隙锁(Gap Lock)。

 

写倾斜

 

比如,箱子里有三个白球和三个黑球,两个事务(T1,T2)并发修改,不知道对方的存在。T1 要让 6 个球都变成白色;T2 则希望 6 个球都变成黑色。

 

 

 

最终的执行结果是,盒子里仍然有三个黑球和三个白球。作为对比,串行执行的效果图如下

 

 

可串行化的定义,“多事务并行执行所得到的结果,与串行执行(一个接一个)完全相同”。比照两张图,很容易发现事务并行执行没有达到串行的同等效果,所以这是一种异常现象。也可以说,写倾斜是一种更不易察觉的更新丢失

 

快照隔离 & MVCC

 

 SQL-92 主要考虑了基于锁(Lock-base)的并发控制,而快照隔离的实现基础则是多版本并发控制(MVCC)。后来,MVCC 成为一项非常重要的技术,与乐观并发控制和悲观并发控制并列。在现代数据库中 MVCC 已经成为一种底层技术,用于更高效地实现乐观或悲观并发控制

 

4、总结

 

数据一致性和事务一致性,它们共同构成了分布式数据库的强一致性这个概念。

 

 

分布式数据库产品的“一致性”实现情况

 

 

4.1、如何选择一致性级别(补充自《分布式金融架构课》)

 

很多情况下,我们并不是追究极端的一致性,而是根据我们的业务和经济情况来选择合适的一致性级别。

 

  • 首先我们要看是单机问题还是多机问题。如果是单机问题,那么首选快照隔离,一般不需要用到可串行化。
  • 如果是多机问题,那么先解决的是多机容灾。这时候有多台机器需要提供同一份数据,你可以根据容灾后的正确性要求具体判断。
    • 一种情况是你对容灾后的正确性要求不高,这时就要看看从客户端角度发起的会话是否需要有正确性。
      • 如果你只需要保证一个会话的正确性,那么一致性要求就是保证单调读一致、单调写一致和自读自写。
      • 如果需要保证多个会话之间的正确性,就要保证先读后写。
    • 另一种情况是对容灾之后的数据访问正确性要求高,那么就要保证线性一致性。
  • 最后,如果你要解决的是在有容灾的情况下的分库分表问题,就需要解决分布式事务。这时候,每个分完的库和它的容灾机器组成的集群需要先满足线性一致性,这样容灾集群对外才能表现得像单个节点一样。然后我们再用 TCC 或者 2PL 来实现分布式事务。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值