# 3.2 错误的设计（Bad Design）

这里说到了一致性，我在后面的课程会对“好”的一致性做更多的介绍（lec6-7）。对于具备强一致或者好的一致性的系统，从应用程序或者客户端看起来就像是和一台服务器在通信。尽管我们会通过数百台计算机构建一个系统，但是对于一个理想的强一致模型，你看到的就像是只有一台服务器，一份数据，并且系统一次只做一件事情。这是一种直观的理解强一致的方式。你可以认为只有一台服务器，甚至这个服务器只运行单线程，它同一时间只处理来自客户端的一个请求。这很重要，因为可能会有大量的客户端并发的发送请求到服务器上。这里要求服务器从请求中挑选一个出来先执行，执行完成之后再执行下一个。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MDSxKBFbUGlxi7edbSo%2F-MDT1s1DADahedhcoKlz%2Fimage.png?alt=media\&token=d62a93d8-5ebc-4a90-865f-00d61cda7156)

对于存储服务器来说，它上面会有一块磁盘。执行一个写请求或许意味着向磁盘写入一个数据或者对数据做一次自增。如果这是一次修改操作，并且我们有一个以key-value为索引的数据表单，那么我们会修改这个表单。如果是一次读取操作，我们只需要将之前写入的数据，从表单中取出即可。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MDSxKBFbUGlxi7edbSo%2F-MDT31lRs46i3FmWZBL6%2Fimage.png?alt=media\&token=15cdb836-64b0-4881-a9b6-3af5604024fb)

为了让这里的简单服务有可预期的行为，需要定义一条规则：一个时间只执行一条请求。这样每个请求都可以看到之前所有请求按照顺序执行生成的数据。所以，如果有一些写请求，并且服务器以某种顺序一次一个的处理了它们，当你从服务器读数据时，你可以看到期望的数据。这里的解释不是很直观，你可以通过下面的例子去理解。

例如，我们有一些客户端，客户端C1发起写请求将X设置成1。在同一时刻，客户端C2发起写请求将X设置成2。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MDUnHaTtIsKXJPfS1Fx%2F-MDUxe4lkwXBT5cDmoxd%2Fimage.png?alt=media\&token=2ab9dee6-f8f6-4a2a-85ff-bdedaff9a406)

过了一会，在C1和C2的写请求都执行完毕之后，客户端C3会发送读取X的请求，并得到了一个结果。客户端C4也会发送读取X的请求，也得到了一个结果。现在的问题是，这两个客户端看到的结果是什么？

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MDUnHaTtIsKXJPfS1Fx%2F-MDUy94X2FMFFDkbenpq%2Fimage.png?alt=media\&token=832133c0-5aec-4b92-8063-efee412664e8)

> 学生提问：为什么一定要一次只处理一个请求？
>
> Robert教授：这是个好问题。在这里，我假设C1和C2在同一时间发起请求。所以，如果我们在监控网络的话，我们可以看到两个请求同时发往服务器。之后在某个时刻，服务器会响应它们。但是这里没有足够的信息来判断，服务器会以哪种顺序执行这两个写请求。如果服务器先处理了写X为1的请求，那么就意味着它接下来会处理写X为2的请求，所以接下来的读X请求可以看到2。然而，如果服务器先处理了写X为2的请求，再处理写X为1的请求，那么接下来的读X请求看到的就是1。

我这里提出这个场景的目的是为了展示，即使在一个非常简单的系统中，仍然会出现一些模糊的场景使得你不知道系统的执行过程以及输出结果。你能做的只是从产生的结果来判断系统的输出是一致性还是非一致性。

如果C3读X得到2，那么C4最好也是读X得到2，因为在我们的例子中，C3读X得到2意味着，写X为2的请求必然是第二个执行的写请求。当C4读X时，写X为2应该仍然是第二个写请求。希望这里完全直观的介绍清楚了有关强一致的一个模型。

当然，这里的问题是，因为只有单个服务器，所以容错能力很差。如果服务器故障了，磁盘坏了，系统整个就不可用了。所以，在现实世界中，我们会构建多副本的分布式系统，但这却又是所有问题的开始。

这里有一个几乎是最糟糕的多副本设计，我提出它是为了让你们知道问题所在，并且同样的问题在GFS中也存在。这个设计是这样，我们有两台服务器，每个服务器都有数据的一份完整拷贝。它们在磁盘上都存储了一个key-value表单。当然，直观上我们希望这两个表单是完全一致的，这样，一台服务器故障了，我们可以切换到另一台服务器去做读写。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MDV6sci_uAAPtKmWU-G%2F-MDWwACCq_8HNooz5yNM%2Fimage.png?alt=media\&token=c45c4cb2-3cbb-4bef-802f-5245ce78f543)

两个表单完全一致意味着，每一个写请求都必须在两台服务器上执行，而读请求只需要在一台服务器上执行，否则就没有容错性了。因为如果读请求也需要从两台服务器读数据，那么一台服务器故障我们就没法提供服务了。现在问题来了，假设客户端C1和C2都想执行写请求，其中一个要写X为1，另一个写X为2。C1会将写X为1的请求发送个两个服务器，因为我们想要更新两台服务器上的数据。C2也会将写X为2的请求发送给两个服务器。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MDV6sci_uAAPtKmWU-G%2F-MDWz0Pc8uVGTQs98wQE%2Fimage.png?alt=media\&token=e8e7efe7-615a-46d4-89b3-38f2d4f92719)

这里会出现什么错误呢？是的，我们没有做任何事情来保障两台服务器以相同的顺序处理这2个请求。这个设计真不咋样。如果服务器1（S1）先处理C1的请求，那么在它的表单里面，X先是1，之后S1看到了来自C2的请求，会将自己表单中的X覆盖成2。但是，如果S2恰好以不同的顺序收到客户端请求，那么S2会先执行C2的请求，将X设置为2，之后收到C1的请求，将X设置为1。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MDV6sci_uAAPtKmWU-G%2F-MDX-JB90ExdaZ1h51Jn%2Fimage.png?alt=media\&token=67f2bb95-ac66-4067-842b-d69111fb8bf3)

之后，如果另外一些客户端，假设C3从S1读数据，C4从S2读数据，我们就会面临一个可怕的场景：这两个客户端读取的数据不一样。但是从前一个例子中的简单模型可以看出，相连的读请求应该读出相同的数据。

这里的问题可以以另一种方式暴露出来。假设我们尝试修复上面的问题，我们让客户端在S1还在线的时候，只从S1读取数据，S1不在线了再从S2读取数据。这样最开始所有的客户端读X都能得到2。但是突然，如果S1故障了，尽管没有写请求将X改成1，客户端读X得到的数据将会从2变成1。因为S1故障之后，所有的客户端都会切换到S2去读数据。这种数据的神奇变化与任何写操作都没有关联，并且也不可能在前一个例子的简单模型中发生。

当然，这里的问题是可以修复的，修复需要服务器之间更多的通信，并且复杂度也会提升。由于获取强一致会带来不可避免的复杂性的提升，有大量的方法可以在好的一致性和一些小瑕疵行为之间追求一个平衡。
