> 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.3-xian-xing-yi-zhi-linearizability3.md).

# 8.3 线性一致（Linearizability）（3）

这里还有另一个简单的请求历史记录的例子。假设我们先写X为1，在这个请求完成之后，有另一个客户端发送了写X为2的请求，并收到了响应说，写入完成。之后，有第三个客户端，发送了一个读X的请求，得到了1。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MCZOMvzhNQ9svfDymIJ%2F-MCZP4g_hm7B-1u8TAGk%2Fimage.png?alt=media\&token=5143e77b-fde2-4615-9fe7-58e382d7304a)

这是一个很简单的例子，它明显不是线性一致的，因为线性一致要求生成的序列与实际时间匹配，这意味着，唯一可能的序列就是写X为1，之后写X为2，之后读X得到1。但是这个顺序明显违反了线性一致的第二个限制，因为读X得到1的前一个写请求是写X为2，这里读X应该返回2，所以这里明显不是线性一致的。

我提出这个例子的原因是，这是线性一致系统，或者强一致系统不可能提供旧的数据的证据。为什么一个系统有可能会提供旧的数据呢？或许你有大量的副本，每一个副本或许没有看到所有的写请求，或者所有的commit了的写请求。所以，或许所有的副本看到第一个写请求，也就是写X为1的请求，但是只有部分副本看到了第二个写请求，也就是写X为2的请求。所以，当你向一个已经“拖后腿”的副本请求数据时，它仍然只有X的值为1。然而客户端永远也不能在一个线性一致的系统中看到旧的数据（也就是X=1），因为一个线性一致的系统不允许读出旧的数据。

所以这里不是线性一致的，这里的教训是：对于读请求不允许返回旧的数据，只能返回最新的数据。或者说，对于读请求，线性一致系统只能返回最近一次完成的写请求写入的值。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MCZOMvzhNQ9svfDymIJ%2F-MCZWpw7if6OfdG6IRk3%2Fimage.png?alt=media\&token=61238a4c-69c5-4992-b7db-039aa451a26e)

好的，我最后还有一个小的例子。现在我们有两个客户端，其中一个提交了一个写X为3的请求，之后是一个写X为4的请求。同时，我们还有另一个客户端，在这个时间点，客户端发出了一个读X的请求，但是客户端没有收到回复。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MCZOMvzhNQ9svfDymIJ%2F-MCZXOjdCGd3bze4jf6F%2Fimage.png?alt=media\&token=86848567-8022-4e74-82e2-3f95494e5afe)

在一个实际的系统实现中，可能有任何原因导致这个结果，例如：

* Leader在某个时间故障了
* 这个客户端发送了一个读请求，但是这个请求丢包了因此Leader没有收到这个请求
* Leader收到了这个读请求并且执行了它，但是回复的报文被网络丢包了
* Leader收到了请求并开始执行，在完成执行之前故障了
* Leader执行了这个请求，但是在返回响应的时候故障了

不管是哪种原因，从客户端的角度来看，就是发送了一个请求，然后就没有回复了。在大多数系统的客户端内部实现机制中，客户端将会重发请求，或许发给一个不同的Leader，或许发送给同一个Leader。所以，客户端发送了第一个请求，之后没有收到回复并且超时之后，或许在这里发送了第二个请求。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MCZOMvzhNQ9svfDymIJ%2F-MCZmo2V5LvSCf2uDpZ7%2Fimage.png?alt=media\&token=0ed7e43c-191e-495b-84aa-64feaabb303b)

之后，终于收到了一个回复。这将是Lab3的一个场景。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MCZOMvzhNQ9svfDymIJ%2F-MCZnR4O4w_F7Ad3rMwQ%2Fimage.png?alt=media\&token=3802c53f-5091-43d3-8a97-b2eb8d962345)

服务器处理重复请求的合理方式是，服务器会根据请求的唯一号或者其他的客户端信息来保存一个表。这样服务器可以记住，哦，我之前看过这个请求，并且执行过它，我会发送一个相同的回复给它，因为我不想执行相同的请求两次。例如，假设这是一个写请求，你不会想要执行这个请求两次。所以，服务器必须要有能力能够过滤出重复的请求。第一个请求的回复可能已经被网络丢包了。所以，服务器也必须要有能力能够将之前发给第一个请求的回复，再次发给第二个重复的请求。所以，服务器记住了最初的回复，并且在客户端重发请求的时候将这个回复返回给客户端。如果服务器这么做了，那么因为服务器或者Leader之前执行第一个读请求的时候，可能看到的是X=3，那么它对于重传的请求，可能还是会返回X=3。所以，我们必须要决定，这是否是一个合法的行为。

你可能会说，客户端在这里发送的（重传）请求，这在写X为4的请求之后，所以你这里应该返回4，而不是3。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MCZOMvzhNQ9svfDymIJ%2F-MCZpCydiVoitwHiuEJG%2Fimage.png?alt=media\&token=e7bf7b1c-05b0-446a-a858-ef0db9855602)

这里取决于设计者，但是重传本身是一个底层的行为，或许在RPC的实现里面，或许在一些库里面实现。但是从客户端程序的角度来说，它只知道从第一条竖线的位置发送了一个请求，

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MCZOMvzhNQ9svfDymIJ%2F-MCZqL7sbqxpoFywtdCJ%2Fimage.png?alt=media\&token=c0556d09-e8aa-4360-89dc-79b73b1faeec)

并在第二条竖线的位置收到了一个回复，

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MCZOMvzhNQ9svfDymIJ%2F-MCZqVGJLCHxG5e1Px-E%2Fimage.png?alt=media\&token=89b78eb5-32fb-436e-abdc-35a83043ea42)

这是从客户端角度看到的所有事情。所以，返回X为3是完全合法的，因为这个读请求花费了一个很长的时间，它与写X为4的请求是完全并发的，而不是串行的。

因此，对于这个读请求，返回3或者4都是合法的。取决于这个读请求实际上是在这里执行，

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MCZOMvzhNQ9svfDymIJ%2F-MCZsGH-wzmV4PlgYr2C%2Fimage.png?alt=media\&token=733cb92d-112b-470b-adcf-fcbb60608e84)

还是在这里执行。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MCZOMvzhNQ9svfDymIJ%2F-MCZsMndPPUTTyLQzPwW%2Fimage.png?alt=media\&token=70051c74-b388-434d-adfa-c2ca4e06fc65)

所以，如果你的客户端有重传，并且你要从客户端的角度来定义线性一致，那么一个请求的区间从第一次传输开始，到最后应用程序实际收到响应为止，期间可能发生了很多次重传。

> 学生提问：如果客户端想要看到的是最新的数据而不是旧数据呢？
>
> Robert教授：你在这里宁愿得到最新的数据而不是老旧的数据。假设这里的请求是查询当前时间，我向服务器发送个请求说，现在是几点，服务器返回给我一个响应。现在如果我发送了一个请求，2分钟过去了因为网络问题，我还没收到任何回复。或许应用程序更喜欢看到的回复是更近的时间，而不是很久之前开始发送请求的时间。现在，事实是，如果你使用一个线性一致的系统，你必须要实现能够容纳线性一致规则的程序。你必须写出正确的应用程序来容忍这样一个场景：应用程序发出了一个请求，过了一会才收到回复，比如在这里，如果我得到了一个值是3的回复，这对于应用程序来说可能不能接受这个值。因为这意味着，我在收到响应的时候，系统中X存储的值是3，这与事实不符（实际上X=4）。所以这里最终取决于应用程序本身。

你们在实验中会完成这样的机制，服务器发现了重复的请求，并将之前的回复重新发给客户端。这里的问题是，服务器最初在这里看到了请求，最后回复的数据是本应在之前一个时间点回复的数据，这样是否合理？我们使用线性一致的定义的一个原因是，它可以用来解释问题。例如，在这个场景里面，我们可以说，这样的行为符合线性一致的原则。

好的，这就是所有我想介绍的有关线性一致的东西。在期中测试我必然会问一个线性一致的问题。
