8.2 线性一致(Linearizability)(2)

这里还有一个例子,它与第一个例子前半部分是一样的。首先我们有一个写X为0的请求,之后有两个并发的写请求,还有与前一个例子相同的两个读请求。目前为止,与前一个例子都是一样的。所以,这里的请求历史记录必然是线性一致的。让我们假设,客户端C1发送了这里的两个读请求。客户端C1首先读X得到了2,然后读X得到了1。目前为止没有问题。

我们假设有另一个客户端C2(下图有误,第二个C1应为C2),读X得到了1,再次读X得到了2。

所以,这里的问题是,这个请求历史记录是线性一致的吗?我们要么需要构造一个序列(证明线性一致),要么需要构造一个带环的图(证明非线性一致)。

这里开始变得迷惑起来了。这里有两个并发写请求,在任何构造的序列中,要么一个写请求在前面,要么另一个写请求在前面。直观上来看,C1发现写X为2的请求在前面,之后才是写X为1的请求。它对应的两个读请求表明,在任何合法的序列中,写X为2的请求,必然要在写X为1的请求之前。这样我们才能看到这样的序列。

但是,C2的体验明显是相反的。C2发现,写X为1的请求在前面,之后才是写X为2的请求。

线性一致的一个条件是,对于整个请求历史记录,只存在一个序列,不允许不同的客户端看见不同的序列,或者说不允许一个存储在系统中的数据有不同的演进过程。这里只能有一个序列,所有的客户端必须感受到相同的序列。这里C1的读请求明显暗示了序列中先有写X为2,后有写X为1,所以不应该有其他的客户端能够观察到其他序列的证据。这里不应该有的证据就是C2现在观察到的读请求。这是直观上解释哪里出了问题。

顺便说一下,这里的请求历史记录可能出现的原因是,我们正在构建多副本的系统,要么是一个Raft系统,要么是带有缓存的系统,我们正在构建有多个拷贝的系统,所以或许有多个服务器都有X的拷贝,如果它们还没有获取到commit消息,多个服务器在不同的时间会有X的不同的值。某些副本可能有一种数值,其他可能有另一种数值。尽管这样,如果我们的系统是线性一致或者强一致,那么它必须表现的像只有一份数据的拷贝和一个线性的请求序列一样。这就是为什么这里是个有趣的例子,因为它可能出现在一些有问题的系统中。这个系统有两份数据的拷贝,一个拷贝以一种顺序执行这些写请求,另一个副本以另一种顺序执行这些写请求,这样我们就能看到这里的结果。所以这里不是线性一致,我们不能在一个正确的系统中看到这样的请求历史记录。

另一个证据证明这里不是线性一致的就是,可以构造一个带环的图。

写X为2的请求,必须在C1读X得到2的请求之前,所以这里有个这样的箭头。所以这个写请求必须在这个读请求之前。

C1读X得到2的请求必须在写X为1的请求之前,否则C1的第二个读请求不可能得到1。你可以假设写X为1的请求很早就发生了(在写X为2的实际执行时间就发生了),但那样的话,C1的第二个读请求不能看到1,只能看到2,因为第一个读请求看到的就是2(通俗解释就是,因为第一个读请求看到的是2,如果后面没有一个别写请求的话,那么后面的读请求应该看到相同的结果)。所以,读X得到2的请求必须在写X为1的请求之前。

写X为1的请求必须在任何读X得到1的请求之前,包括了C2读X得到1的请求。

但是,为了让C2先有读X得到1的请求,后有读X得到2的请求,C2的读X得到1的请求必须要在写X为2的请求之前(这样两次读才有可能是不同的值)。

这里就有了个环。所以不存在一个序列能满足线性一致的要求,因为我们构造了一个带环的图。

学生提问:所以说线性一致不是用来描述系统的,而是用来描述系统的请求记录的?

Robert教授:这是个好问题。线性一致的定义是有关历史记录的定义,而不是系统的定义。所以我们不能说一个系统设计是线性一致的,我们只能说请求的历史记录是线性一致的。如果我们不知道系统内部是如何运作的,我们唯一能做的就是在系统运行的时候观察它,那在观察到任何输出之前,我们并不知道系统是不是线性一致的,我们可以假设它是线性一致的。之后我们看到了越来越多的请求,我们发现,哈,这些请求都满足线性一致的要求,那么我们认为,或许这个系统是线性的。如果我们发现一个请求不满足线性一致的要求,那么这个系统就不是线性一致的。所以是的,线性一致不是有关系统设计的定义,这是有关系统行为的定义。

所以,当你在设计某个东西时,它不那么适用。在设计系统的时候,没有一个方法能将系统设计成线性一致。除非在一个非常简单的系统中,你只有一个服务器,一份数据拷贝,并且没有运行多线程,没有使用多核,在这样一个非常简单的系统中,要想违反线性一致还有点难。但是在任何分布式系统中,又是非常容易违反线性一致性。

所以这个例子的教训是,对于系统执行写请求,只能有一个顺序,所有客户端读到的数据的顺序,必须与系统执行写请求的顺序一致。

(下面的内容在视频中时间不连续,是在讲解其他例子的时候,学生对这个例子的提问,因为内容相关,就放到这里)

学生提问:可以再解释一下为什么写X为1的请求会在C1的读X得到2和读X得到1请求之间吗?

Robert教授:或许我这里偷懒了,这里实际发生的是,C1先有读X得到2,再有读X得到1。读X得到1在实际时间中的确在读X得到2之后,所以在这两个读请求中间,必然有一个写X为1的请求。在最终的序列中,在读X得到2的请求之后,在读X得到1的请求之前,必然会有一个写X为1的请求。这里只有一个写X为1的请求,如果有多个写X为1的请求,或许我们或许还能想想办法,但是这里只有一个请求,所以在最终的序列中,这个写X为1的请求必须位于这两个读请求中间。因此,我认为可以画这样一条箭头(从读X得到2到写X为1的箭头) 。这些箭头都表明了线性一致的规则。

学生提问:有没有可能有一个更简单的环?

Robert教授:可能会有一个更简单的环,这里4个请求的问题是,它们是出了问题的主要证据。这里例子值得好好思考一下,因为我我不能想到更好的解释方法。

最后更新于