> For the complete documentation index, see [llms.txt](https://mit-public-courses-cn-translatio.gitbook.io/mit6-824/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://mit-public-courses-cn-translatio.gitbook.io/mit6-824/lecture-08-zookeeper/8.1.md).

# 8.1 线性一致（Linearizability）（1）

上一节课，我对线性一致这个概念开了个头，这一次我们来讲完它。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MC79IMfHW3WeZnur0-M%2F-MC7AL1UM_neqb8G3FQ7%2Fimage.png?alt=media\&token=59b9e546-926f-47f3-8305-19210119fe99)

之所以我们要再进一步介绍这个概念，是因为这是我们对于存储系统中强一致的一种标准定义。例如，你们在Lab3中实现的系统必须是线性一致的。有时，当我们在讨论一个强一致的系统时，我们会想知道一个特定的行为是否是可接受的。其他时候，例如，当我们在讨论一个非线性一致的系统时，我们可能会想知道系统会以什么方式偏离线性一致。所以，首先，你需要能够查看某个系统的执行历史记录，并且回答这个问题：刚刚查看的操作的序列是否是线性一致的？接下来我会继续分析，并构建几个有趣的例子来帮助我们理解线性一致系统的响应。

线性一致是特定的操作历史记录的特性。所以，我们总是会提到，我们观察到了一系列不同时间的客户端请求和这些请求的响应，它们请求不同的数据，并且得到了各种各样的回复，我们需要回答，这样的一个历史记录是不是线性的？

下面会介绍一个历史记录的例子，它或许是线性的，或许不是。我们用一个图表示这个例子，在图里面，越靠右，时间越靠后。同时我们有一些客户端。这里的竖线表示客户端发送了一个请求，并且这是个写请求，它将key为X的数据的值写成0。所以这里有一个key，一个value，并且请求对应于将key为X的数据设置成0的PUT操作。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MC79IMfHW3WeZnur0-M%2F-MC7CGq0NyXOEYafbu4T%2Fimage.png?alt=media\&token=5b929d9a-4a49-4f32-b9cb-26ac1f544c31)

这是我们观察到的结果，客户端发出请求到我们的服务，某个时间点，服务响应了并说，好的，你的写操作完成了。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MC79IMfHW3WeZnur0-M%2F-MC7CsE_UMxLMb2Q3pEH%2Fimage.png?alt=media\&token=d5448253-ffd0-4581-b9cd-38977b49730d)

所以我们假设这里的服务具备通知请求完成的能力，否则我们很难判断线性一致。所以，我们有了某人发出的这个写请求。在这个例子中，我假设还有另一个请求。这根竖线意味着第二个请求在第一个请求结束之后开始。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MC79IMfHW3WeZnur0-M%2F-MC7DQ34nC1ABCQzxh5k%2Fimage.png?alt=media\&token=85babb74-0952-48dc-94f3-cbe766853e25)

这一点之所以重要的原因是，线性一致的历史记录必须与请求的实际时间匹配。这里的真实意思是在实际时间中，某个请求如果在另一个请求结束之后才开始，那么在我们构建用于证明线性一致的序列中，后来的请求都必须在先来的请求之后。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MC79IMfHW3WeZnur0-M%2F-MC7Ds-07PC7rOwvHEYW%2Fimage.png?alt=media\&token=5bf93c18-68f6-4437-af46-7a4f7b7ccba5)

所以，在这里例子中，我假设有另一个写X的请求，将X写成1。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MC79IMfHW3WeZnur0-M%2F-MC7E1Xkmg2DmNHzkj0v%2Fimage.png?alt=media\&token=ab4387e0-a4a9-45ba-be5a-c41258a63684)

之后有个并发的写请求，或许比前一个请求开始的稍晚一点，将X写成2。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MC79IMfHW3WeZnur0-M%2F-MC7EgCydrrNI1TJc6AI%2Fimage.png?alt=media\&token=de3555e8-29d1-4c0f-b116-8511576ce1ca)

这里我们有两个客户端，在差不多的时间发送了两个不同的请求，想将X设置成两个不同的值。所以，当然，我们想知道最后X会是哪个值？之后，我们还有一些读操作。当你只有一些写操作时，很难判断线性一致，因为你没有任何证据证明系统实际做了哪些操作，或者存储了什么数据，所以（在判断线性一致时）我们必须要有一些读操作。

我们假设有一些读操作，其中一个读操作，在第一条竖线发起，在第二条竖线得到回复。这个操作读的是key X，得到的是2。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MC79IMfHW3WeZnur0-M%2F-MC7GvHPRbmdCEmD-Rlo%2Fimage.png?alt=media\&token=decfd790-8990-4e3c-a684-b3bc455aa5d2)

之后，有来自于同一个客户端的另一个读请求，但是这个请求在前一个读请求结束之后才开始，第二个读X的请求得到1。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MC79IMfHW3WeZnur0-M%2F-MC7H_g7sc5-sow9L85Z%2Fimage.png?alt=media\&token=701ee4b8-ea40-4a68-8dc8-45d818c3bb60)

所以，我们面前现在有个问题，这个历史记录是不是线性一致的？这里有两种可能。

要么我们能构建一个序列，同时满足

1. 序列中的请求的顺序与实际时间匹配
2. 每个读请求看到的都是序列中前一个写请求写入的值

如果我们能构造这么一个序列，那么可以证明，这里的请求历史记录是线性的。

另一种可能是，如果将上面的规则应用之后生成了一个带环的图，那么证明请求历史记录不是线性一致的。对于小规模的历史记录，我们可以遍历每个请求来做判断。

那么这里的请求历史记录是线性一致的吗？

> 学生回答1（Robert教授复述）：这里的回答是，这里有点麻烦。我们看到读X得到2，之后读X得到1，或许这里会自相矛盾。
>
> 因为这里有两个写请求，一个写入1，另一个写入2。如果我们读X得到了3，那明显是个很糟糕的错误。但是现在有写X为1和2的请求，并且我们读到的X也是1和2。所以，这里的问题是，这里的读请求的顺序是否会与请求历史记录中的两个写请求矛盾？

> 学生回答2（Robert教授复述答案）：在这里，我们或许有2个或者3个客户端，它们与某个服务交互，或许是个Raft服务。我们能看到的只有请求和响应。这里的意思是，我们看到了一个客户端请求写X为1。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MC79IMfHW3WeZnur0-M%2F-MC7KtXfX2cT6DD6-46s%2Fimage.png?alt=media\&token=107c6196-b647-4292-bcab-d2ed990eaf01)

> 我们在这里看到了响应。所以我们知道，在这个区间里的某处，服务实际上在内部将X的值改为1。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MC79IMfHW3WeZnur0-M%2F-MC7L6XW0nfwPWJdk9V_%2Fimage.png?alt=media\&token=226ee697-83d5-44ed-8b90-596d3b818cdb)

> 这里的意思是，在这个区间中的某处，服务在内部将X的值改为2。这里可能是区间里的任意一个时间点。这回答了你的问题吗？

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MC79IMfHW3WeZnur0-M%2F-MC7MIKOy4e5M79Hig4b%2Fimage.png?alt=media\&token=99d497dc-a5a3-43d8-ae12-238db8da0f24)

> 学生回答3（Robert教授复述答案）：所以这里的回答是，这里有实际的证据证明是线性一致的，也就是说有一个序列表明它是线性一致的。

所以是的，这里的请求历史记录是线性一致的。这里的序列是，首先是将X写0的请求，之后服务器收到了两个差不多时间的写操作，服务自己要为这两个写操作挑一个顺序。所以，我们可以假设，服务器先执行了将X写2的请求，之后执行读X返回2的请求，也就是第一个读X的请求。下一个请求是将X写1的请求，最后一个请求是读X返回1。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MC79IMfHW3WeZnur0-M%2F-MC7QtikOj54NjtKS4Kl%2Fimage.png?alt=media\&token=12ab8e00-f729-4e10-9dc9-bcf8fe851df6)

所以，这就是证明这里的请求历史是线性一致的证据，因为这个序列有所有请求，并且这个序列匹配请求的实际时间。

我们来再过一遍所有的请求。将X写0的请求在最开始，因为它在所有其他操作开始之前就结束了。我们将X写2的请求排第二。这里我会标记请求在实际时间中生效的位置，我用一个大X来标记这个请求实际发生的时间。所以，第二个请求的实际生效时间在这里。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MC79IMfHW3WeZnur0-M%2F-MC7QhzizClx1zL4nH3P%2Fimage.png?alt=media\&token=dae0cb6b-8ad7-48fa-a69e-22515c08e0db)

下一个请求是读X得到2。这里并没有时间上的问题，因为读X得到2实际上与写X为2，这两个请求是并发的。这里并不是读X得到2结束之后，写X为2的请求才开始，这里它们是并发的。我们假设读X得到2的请求实际发生在这里。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MC79IMfHW3WeZnur0-M%2F-MC7Q3k0JD_IKMt_D2v7%2Fimage.png?alt=media\&token=43731000-c0a9-467b-8b5e-598d9133af42)

我们并不关心第一个请求在什么时候发生。现在我们有了前3个请求的执行时间。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MC79IMfHW3WeZnur0-M%2F-MC7RCNRZAxMgJoAfLKu%2Fimage.png?alt=media\&token=fee1037d-a846-4c3a-89b4-3b1559230912)

之后我们有个请求将X写为1，我们假设它在实际时间中发生在这里，因为它必须在序列中的前一个请求之后发生。所以，这是第4个请求。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MC79IMfHW3WeZnur0-M%2F-MC7RnVU85qVwWBRDV43%2Fimage.png?alt=media\&token=68548261-f4d5-4395-babf-2e6e17ff9aec)

之后，我们有读X得到1的请求，它可能在任何时间发生，但是让我们假设它发生在这里。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MC79IMfHW3WeZnur0-M%2F-MC7ST2IIpgzwXV6Q-p8%2Fimage.png?alt=media\&token=8fb99135-93d1-4e1f-9a34-8395cb2d3394)

所以，这里展示了一个与实际时间匹配的序列，我们可以为每一个请求，在其开始和结束时间的区间里面挑选一个时间，来执行这个请求，挑选的时间可以匹配请求的实际时间。

所以这里的最后一个问题是，每个读操作是不是看到了前一个写请求写入的值？这里的读X得到2的请求，在写X为2的请求之后，这没问题。&#x20;

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MC79IMfHW3WeZnur0-M%2F-MC7SmzA9LBSEWu_Cy1s%2Fimage.png?alt=media\&token=da739111-c50f-44a0-ad6b-970e38a1a3ee)

读X得到1的请求，紧跟在在写X为1的请求之后。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MC79IMfHW3WeZnur0-M%2F-MC7StBlelD49xqNDcP1%2Fimage.png?alt=media\&token=ef02c8c9-2111-4086-afb7-51f7477ca9d1)

所以这里的历史记录是线性一致的。

并不是所有的请求历史记录都能直接明了的判断是否是线性一致的。当看到这个例子里的历史记录时，很容易被误导。比如，写X为1的请求（比写X为2的请求）先开始，所以我们就假设X会先被写成1，但是在实际中不一定是这样的。

大家有什么问题吗？

> 学生提问：如果将写X为2的开始时间改在读X为2的结束时间之后会怎样？
>
> Robert教授：如果写X为2的请求在读X得到2的请求结束之后才开始，那就不是线性一致了。因为在任何我们构建的序列中，都必须要遵守实际时间的顺序。同时，因为在上面的例子中，因为我们没有其他的写X为2的请求，这意味着这里的读请求只能得到0或者1，因为这里是其他两个可能在这个读请求之前的写请求。所以，修改之后，这里的例子就不再是线性一致的了。
>
> 学生提问：所以这里完全是根据客户端看到的响应来判断？
>
> Robert教授：是的，所以这（线性一致）是一个非常以客户端为中心的定义，它表明客户端应该看到怎样的请求顺序。但是这背后发生了什么，或许服务有大量的副本，或许是一个复杂的网络，谁知道呢？这些基本与我们无关。这里的定义只关心客户端看到了什么。这里有一些灰度空间，我后面会介绍。例如，我们需要考虑，客户端可能需要重传一个请求。

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

> 学生提问：也就是收，系统可以在一个请求区间的任意时间点执行请求？
>
> Robert教授：是的，如果请求的区间有重合，那么系统可以在区间的任何时间点执行请求，所以系统可能以任何的顺序执行这些请求。现在，你知道，如果不是这里的两个读请求，那么系统可以自由的以任何顺序执行这些写请求。但是因为现在我们看到了这两个读请求，我们知道了唯一的合法的顺序是先写X为2的请求，之后是写X为1的请求。所以是的，如果这里的两个读请求是重叠的，那么这两个读请求可以是任意的执行顺序。实际上，直到我们看见读请求返回了2和1，系统在commit之前可以以任意顺序返回读请求的数值。
>
> 学生提问：线性一致和强一致的区别是什么？
>
> Robert教授：我将它们（线性一致和强一致）看成同义词。对于大部分的论文，尽管最近的论文可能不太一样，线性一致有明确的定义。人们对于线性一致的定义实际上没有相差太多，但是，对于强一致的具体定义来说，我认为共识会少一些。通常来说，它的定义与线性一致的定义非常接近。例如，强一致系统表现的也与系统中只有一份数据的拷贝一样，这与线性一致的定义非常接近。所以，可以合理的认为强一致与线性一致是一样的。
