|
28 | 28 |
|
29 | 29 | ## 一致性保证
|
30 | 30 |
|
31 |
| -在 “[复制延迟问题](ch5.md#复制延迟问题)” 中,我们看到了数据库复制中发生的一些时序问题。如果你在同一时刻查看两个数据库节点,则可能在两个节点上看到不同的数据,因为写请求在不同的时间到达不同的节点。无论数据库使用何种复制方法(单主复制,多主复制或无主复制),都会出现这些不一致情况。 |
| 31 | +在 “[复制延迟问题](ch5.md#复制延迟问题)” 中,我们看到了数据库复制中发生的一些时序问题。如果你在同一时刻查看两个数据库节点,则可能在两个节点上看到不同的数据,因为写请求在不同的时间到达不同的节点。无论数据库使用何种复制方法(单主复制、多主复制或无主复制),都会出现这些不一致情况。 |
32 | 32 |
|
33 | 33 | 大多数复制的数据库至少提供了 **最终一致性**,这意味着如果你停止向数据库写入数据并等待一段不确定的时间,那么最终所有的读取请求都会返回相同的值【1】。换句话说,不一致性是暂时的,最终会自行解决(假设网络中的任何故障最终都会被修复)。最终一致性的一个更好的名字可能是 **收敛(convergence)**,因为我们预计所有的副本最终会收敛到相同的值【2】。
|
34 | 34 |
|
|
75 | 75 |
|
76 | 76 | **图 9-2 如果读取请求与写入请求并发,则可能会返回旧值或新值**
|
77 | 77 |
|
78 |
| -为了简单起见,[图 9-2](img/fig9-2.png) 采用了用户请求的视角,而不是数据库内部的视角。每个柱都是由客户端发出的请求,其中柱头是请求发送的时刻,柱尾是客户端收到响应的时刻。因为网络延迟变化无常,客户端不知道数据库处理其请求的精确时间 —— 只知道它发生在发送请求和接收响应之间的某个时刻。[^i] |
| 78 | +为了简单起见,[图 9-2](img/fig9-2.png) 采用了用户请求的视角,而不是数据库内部的视角。每个横柱都是由客户端发出的请求,其中柱头是请求发送的时刻,柱尾是客户端收到响应的时刻。因为网络延迟变化无常,客户端不知道数据库处理其请求的精确时间 —— 只知道它发生在发送请求和接收响应之间的某个时刻。[^i] |
79 | 79 |
|
80 |
| -[^i]: 这个图的一个微妙的细节是它假定存在一个全局时钟,由水平轴表示。即使真实的系统通常没有准确的时钟(请参阅 “[不可靠的时钟](ch8.md#不可靠的时钟)”),但这种假设是允许的:为了分析分布式算法,我们可以假设一个精确的全局时钟存在,不过算法无法访问它【47】。算法只能看到由石英振荡器和 NTP 产生的实时逼近。 |
| 80 | +[^i]: 这个图的一个微妙的细节是它假定存在一个全局时钟,由水平轴表示。虽然真实的系统通常没有准确的时钟(请参阅 “[不可靠的时钟](ch8.md#不可靠的时钟)”),但这种假设是允许的:为了分析分布式算法,我们可以假设存在一个精确的全局时钟,不过算法无法访问它【47】。算法只能看到由石英振荡器和 NTP 产生的对真实时间的逼近。 |
81 | 81 |
|
82 | 82 | 在这个例子中,寄存器有两种类型的操作:
|
83 | 83 |
|
|
90 | 90 | * 客户端 A 的最后一个读操作,开始于写操作完成之后。如果数据库是线性一致性的,它必然返回新值 `1`:因为读操作和写操作一定是在其各自的起止区间内的某个时刻被处理。如果在写入结束后开始读取,则读取处理一定发生在写入完成之后,因此它必须看到写入的新值。
|
91 | 91 | * 与写操作在时间上重叠的任何读操作,可能会返回 `0` 或 `1` ,因为我们不知道读取时,写操作是否已经生效。这些操作是 **并发(concurrent)** 的。
|
92 | 92 |
|
93 |
| -但是,这还不足以完全描述线性一致性:如果与写入同时发生的读取可以返回旧值或新值,那么读者可能会在写入期间看到数值在旧值和新值之间来回翻转。这不是我们所期望的仿真 “单一数据副本” 的系统。[^ii] |
| 93 | +但是,这还不足以完全描述线性一致性:如果与写入同时发生的读取可以返回旧值或新值,那么读者可能会在写入期间看到数值在旧值和新值之间来回翻转。这个系统对 “单一数据副本” 的模拟还不是我们所期望的。[^ii] |
94 | 94 |
|
95 | 95 | [^ii]: 如果读取(与写入同时发生时)可能返回旧值或新值,则称该寄存器为 **常规寄存器(regular register)**【7,25】
|
96 | 96 |
|
|
102 | 102 |
|
103 | 103 | 在一个线性一致的系统中,我们可以想象,在 `x` 的值从 `0` 自动翻转到 `1` 的时候(在写操作的开始和结束之间)必定有一个时间点。因此,如果一个客户端的读取返回新的值 `1`,即使写操作尚未完成,所有后续读取也必须返回新值。
|
104 | 104 |
|
105 |
| -[图 9-3](img/fig9-3.png) 中的箭头说明了这个时序依赖关系。客户端 A 是第一个读取新的值 `1` 的位置。在 A 的读取返回之后,B 开始新的读取。由于 B 的读取严格在发生于 A 的读取之后,因此即使 C 的写入仍在进行中,也必须返回 `1`(与 [图 9-1](img/fig9-1.png) 中的 Alice 和 Bob 的情况相同:在 Alice 读取新值之后,Bob 也希望读取新的值)。 |
| 105 | +[图 9-3](img/fig9-3.png) 中的箭头说明了这个时序依赖关系。客户端 A 是第一个读取新的值 `1` 的位置。在 A 的读取返回之后,B 开始新的读取。由于 B 的读取严格发生于 A 的读取之后,因此即使 C 的写入仍在进行中,也必须返回 `1`(与 [图 9-1](img/fig9-1.png) 中的 Alice 和 Bob 的情况相同:在 Alice 读取新值之后,Bob 也希望读取新的值)。 |
106 | 106 |
|
107 | 107 | 我们可以进一步细化这个时序图,展示每个操作是如何在特定时刻原子性生效的。[图 9-4](img/fig9-4.png) 显示了一个更复杂的例子【10】。
|
108 | 108 |
|
109 | 109 | 在 [图 9-4](img/fig9-4.png) 中,除了读写之外,还增加了第三种类型的操作:
|
110 | 110 |
|
111 | 111 | * $cas(x, v_{old}, v_{new})⇒r$ 表示客户端请求进行原子性的 [**比较与设置**](ch7.md#比较并设置(CAS)) 操作。如果寄存器 $x$ 的当前值等于 $v_{old}$ ,则应该原子地设置为 $v_{new}$ 。如果 $x$ 不等于 $v_{old}$ ,则操作应该保持寄存器不变并返回一个错误。$r$ 是数据库的响应(正确或错误)。
|
112 | 112 |
|
113 |
| -[图 9-4](img/fig9-4.png) 中的每个操作都在我们认为执行操作的时候用竖线标出(在每个操作的条柱之内)。这些标记按顺序连在一起,其结果必须是一个有效的寄存器读写序列(**每次读取都必须返回最近一次写入设置的值**)。 |
| 113 | +[图 9-4](img/fig9-4.png) 中的每个操作都在我们认为操作被执行的时候用竖线标出(在每个操作的横柱之内)。这些标记按顺序连在一起,其结果必须是一个有效的寄存器读写序列(**每次读取都必须返回最近一次写入设置的值**)。 |
114 | 114 |
|
115 | 115 | 线性一致性的要求是,操作标记的连线总是按时间(从左到右)向前移动,而不是向后移动。这个要求确保了我们之前讨论的新鲜度保证:一旦新的值被写入或读取,所有后续的读都会看到写入的值,直到它被再次覆盖。
|
116 | 116 |
|
117 | 117 | 
|
118 | 118 |
|
119 |
| -**图 9-4 可视化读取和写入看起来已经生效的时间点。B 的最后读取不是线性一致性的** |
| 119 | +**图 9-4 将读取和写入看起来已经生效的时间点进行可视化。客户端 B 的最后一次读取不是线性一致的** |
120 | 120 |
|
121 | 121 | [图 9-4](img/fig9-4.png) 中有一些有趣的细节需要指出:
|
122 | 122 |
|
123 |
| -* 第一个客户端 B 发送一个读取 `x` 的请求,然后客户端 D 发送一个请求将 `x` 设置为 `0`,然后客户端 A 发送请求将 `x` 设置为 `1`。尽管如此,返回到 B 的读取值为 `1`(由 A 写入的值)。这是可以的:这意味着数据库首先处理 D 的写入,然后是 A 的写入,最后是 B 的读取。虽然这不是请求发送的顺序,但这是一个可以接受的顺序,因为这三个请求是并发的。也许 B 的读请求在网络上略有延迟,所以它在两次写入之后才到达数据库。 |
| 123 | +* 第一个客户端 B 发送一个读取 `x` 的请求,然后客户端 D 发送一个请求将 `x` 设置为 `0`,然后客户端 A 发送请求将 `x` 设置为 `1`。然而,返回给 B 的读取值为 `1`(由 A 写入的值)。这是可以的:这意味着数据库首先处理 D 的写入,然后是 A 的写入,最后是 B 的读取。虽然这不是请求发送的顺序,但这是一个可以接受的顺序,因为这三个请求是并发的。也许 B 的读请求在网络上略有延迟,所以它在两次写入之后才到达数据库。 |
124 | 124 |
|
125 | 125 | * 在客户端 A 从数据库收到响应之前,客户端 B 的读取返回 `1` ,表示写入值 `1` 已成功。这也是可以的:这并不意味着在写之前读到了值,这只是意味着从数据库到客户端 A 的正确响应在网络中略有延迟。
|
126 | 126 |
|
127 |
| -* 此模型不假设有任何事务隔离:另一个客户端可能随时更改值。例如,C 首先读取 `1` ,然后读取 `2` ,因为两次读取之间的值由 B 更改。可以使用原子 **比较并设置(cas)** 操作来检查该值是否未被另一客户端同时更改:B 和 C 的 **cas** 请求成功,但是 D 的 **cas** 请求失败(在数据库处理它时,`x` 的值不再是 `0` )。 |
| 127 | +* 此模型不假设有任何事务隔离:另一个客户端可能随时更改值。例如,C 首先读取到 `1` ,然后读取到 `2` ,因为两次读取之间的值被 B 所更改。可以使用原子 **比较并设置(cas)** 操作来检查该值是否未被另一客户端同时更改:B 和 C 的 **cas** 请求成功,但是 D 的 **cas** 请求失败(在数据库处理它时,`x` 的值不再是 `0` )。 |
128 | 128 |
|
129 |
| -* 客户 B 的最后一次读取(阴影条柱中)不是线性一致性的。该操作与 C 的 **cas** 写操作并发(它将 `x` 从 `2` 更新为 `4` )。在没有其他请求的情况下,B 的读取返回 `2` 是可以的。然而,在 B 的读取开始之前,客户端 A 已经读取了新的值 `4` ,因此不允许 B 读取比 A 更旧的值。再次,与 [图 9-1](img/fig9-1.png) 中的 Alice 和 Bob 的情况相同。 |
| 129 | +* 客户 B 的最后一次读取(阴影条柱中)不是线性一致的。该操作与 C 的 **cas** 写操作并发(它将 `x` 从 `2` 更新为 `4` )。在没有其他请求的情况下,B 的读取返回 `2` 是可以的。然而,在 B 的读取开始之前,客户端 A 已经读取了新的值 `4` ,因此不允许 B 读取比 A 更旧的值。再次,与 [图 9-1](img/fig9-1.png) 中的 Alice 和 Bob 的情况相同。 |
130 | 130 |
|
131 | 131 | 这就是线性一致性背后的直觉。正式的定义【6】更准确地描述了它。通过记录所有请求和响应的时序,并检查它们是否可以排列成有效的顺序,以测试一个系统的行为是否线性一致性是可能的(尽管在计算上是昂贵的)【11】。
|
132 | 132 |
|
|
0 commit comments