This commit is contained in:
Gang Yin 2022-04-23 21:02:42 +08:00
commit ccfcc3d500

10
ch5.md
View File

@ -510,7 +510,7 @@
[^vii]: 有时候这种法定人数被称为严格的法定人数,相对 “宽松的法定人数” 而言(见 “[宽松的法定人数与提示移交](#宽松的法定人数与提示移交)”)
在 Dynamo 风格的数据库中,参数 nw 和 r 通常是可配置的。一个常见的选择是使 n 为奇数(通常为 3 或 5并设置 $w = r =n + 1/ 2$(向上取整)。但是可以根据需要更改数字。例如,设置 $w = n$ 和 $r = 1$ 的写入很少且读取次数较多的工作负载可能会受益。这使得读取速度更快,但具有只有一个失败节点导致所有数据库写入失败的缺点。
在 Dynamo 风格的数据库中,参数 nw 和 r 通常是可配置的。一个常见的选择是使 n 为奇数(通常为 3 或 5并设置 $w = r =n + 1/ 2$(向上取整)。但是可以根据需要更改数字。例如,写入次数较少且读取次数较多的工作负载可以从设置 $w = n$ 和 $r = 1$中受益。这使得读取速度更快,但具有只有一个失败节点导致所有数据库写入失败的缺点。
> 集群中可能有多于 n 的节点。(集群的机器数可能多于副本数目),但是任何给定的值只能存储在 n 个节点上。这允许对数据集进行分区,从而可以支持比单个节点的存储能力更大的数据集。我们将在 [第六章](ch6.md) 继续讨论分区。
>
@ -561,7 +561,7 @@
然而,在无领导者复制的系统中,没有固定的写入顺序,这使得监控变得更加困难。而且,如果数据库只使用读修复(没有反熵过程),那么对于一个值可能会有多大的限制是没有限制的 - 如果一个值很少被读取,那么由一个陈旧副本返回的值可能是古老的。
已经有一些关于衡量无主复制数据库中的复制陈旧度的研究,并根据参数 nw 和 r 来预测陈旧读取的预期百分比【48】。不幸的是这还不是很常见的做法但是将陈旧测量值包含在数据库的度量标准集中是一件好事。虽然最终一致性是一种有意模糊的保证,但是从可操作性角度来说,能够量化 “最终” 也是很重要的。
已经有一些关于衡量无主复制数据库中的复制陈旧度的研究,并根据参数 nw 和 r 来预测陈旧读取的预期百分比【48】。不幸的是这还不是很常见的做法但是将陈旧测量值包含在数据库的标准度量集中是一件好事。虽然最终一致性是一种有意模糊的保证,但是从可操作性角度来说,能够量化 “最终” 也是很重要的。
### 宽松的法定人数与提示移交
@ -614,7 +614,7 @@ Dynamo 风格的数据库允许多个客户端同时写入相同的 Key这意
#### 最后写入胜利(丢弃并发写入)
实现最终融合的一种方法是声明每个副本只需要存储 **“最近”** 的值,并允许 **“更旧”** 的值被覆盖和抛弃。然后,只要我们有一种明确的方式来确定哪个写是 “最近的”,并且每个写入最终都被复制到每个副本,那么复制最终会收敛到相同的值。
实现最终融合的一种方法是声明每个副本只需要存储 **“最近”** 的值,并允许 **“更旧”** 的值被覆盖和抛弃。然后,只要我们有一种明确的方式来确定哪个写是 “最近的”,并且每个写入最终都被复制到每个副本,那么复制最终会收敛到相同的值。
正如 **“最近”** 的引号所表明的,这个想法其实颇具误导性。在 [图 5-12](img/fig5-12.png) 的例子中,当客户端向数据库节点发送写入请求时,客户端都不知道另一个客户端,因此不清楚哪一个先发生了。事实上,说 “发生” 是没有意义的:我们说写入是 **并发concurrent** 的,所以它们的顺序是不确定的。
@ -633,7 +633,7 @@ LWW 实现了最终收敛的目标,但以 **持久性** 为代价:如果同
* 在 [图 5-9](fig5-9.png) 中两个写入不是并发的A 的插入发生在 B 的递增之前,因为 B 递增的值是 A 插入的值。换句话说B 的操作建立在 A 的操作上,所以 B 的操作必须有后来发生。我们也可以说 B 是 **因果依赖causally dependent** 于 A。
* 另一方面,[图 5-12](fig5-12.png) 中的两个写入是并发的:当每个客户端启动操作时,它不知道另一个客户端也正在执行操作同样的键。因此,操作之间不存在因果关系。
如果操作 B 了解操作 A或者依赖于 A或者以某种方式构建于操作 A 之上,则操作 A 在另一个操作 B 之前发生。在另一个操作之前是否发生一个操作是定义什么并发的关键。事实上,我们可以简单地说,如果两个操作都不在另一个之前发生,那么两个操作是并发的(即,两个操作都不知道另一个【54】。
如果操作 B 了解操作 A或者依赖于 A或者以某种方式构建于操作 A 之上,则操作 A 在另一个操作 B 之前发生。一个操作是否在另一个操作之前发生是定义并发含义的关键。事实上,我们可以简单地说,如果两个操作都不在另一个之前发生(即,两个操作都不知道对方),那么两个操作是并发的【54】。
因此,只要有两个操作 A 和 B就有三种可能性A 在 B 之前发生,或者 B 在 A 之前发生,或者 A 和 B 并发。我们需要的是一个算法来告诉我们两个操作是否是并发的。如果一个操作发生在另一个操作之前,则后面的操作应该覆盖较早的操作,但是如果这些操作是并发的,则存在需要解决的冲突。
@ -656,7 +656,7 @@ LWW 实现了最终收敛的目标,但以 **持久性** 为代价:如果同
1. 客户端 1 将牛奶加入购物车。这是该键的第一次写入,服务器成功存储了它并为其分配版本号 1最后将值与版本号一起回送给客户端。
2. 客户端 2 将鸡蛋加入购物车,不知道客户端 1 同时添加了牛奶(客户端 2 认为它的鸡蛋是购物车中的唯一物品)。服务器为此写入分配版本号 2并将鸡蛋和牛奶存储为两个单独的值。然后它将这两个值 **都** 返回给客户端 2 ,并附上版本号 2 。
3. 客户端 1 不知道客户端 2 的写入,想要将面粉加入购物车,因此认为当前的购物车内容应该是 [牛奶,面粉]。它将此值与服务器先前向客户端 1 提供的版本号 1 一起发送到服务器。服务器可以从版本号中知道 [牛奶,面粉] 的写入取代了 [牛奶] 的先前值,但与 [鸡蛋] 的值是 **并发** 的。因此,服务器将版本 3 分配给 [牛奶,面粉],覆盖版本 1 值 [牛奶],但保留版本 2 的值 [蛋],并将所有的值返回给客户端 1 。
4. 同时,客户端 2 想要加入火腿,不知道客户 1 刚刚加了面粉。客户端 2 在最后一个响应中从服务器收到了两个值 [牛奶] 和 [蛋],所以客户端 2 现在合并这些值,并添加火腿形成一个新的值,[鸡蛋,牛奶,火腿]。它将这个值发送到服务器,带着之前的版本号 2 。服务器检测到新值会覆盖版本 2 [鸡蛋],但新值也会与版本 3 [牛奶,面粉] **并发**,所以剩下的两个是 v3 [牛奶,面粉],和 v4[鸡蛋,牛奶,火腿]
4. 同时,客户端 2 想要加入火腿,不知道客户 1 刚刚加了面粉。客户端 2 在最后一个响应中从服务器收到了两个值 [牛奶] 和 [蛋],所以客户端 2 现在合并这些值,并添加火腿形成一个新的值,[鸡蛋,牛奶,火腿]。它将这个值发送到服务器,带着之前的版本号 2 。服务器检测到新值会覆盖版本 2 [鸡蛋],但新值也会与版本 3 [牛奶,面粉] **并发**,所以剩下的两个是 v3 [牛奶,面粉],和 v4[鸡蛋,牛奶,火腿]
5. 最后,客户端 1 想要加培根。它以前在 v3 中从服务器接收 [牛奶,面粉] 和 [鸡蛋],所以它合并这些,添加培根,并将最终值 [牛奶,面粉,鸡蛋,培根] 连同版本号 v3 发往服务器。这会覆盖 v3 [牛奶,面粉](请注意 [鸡蛋] 已经在最后一步被覆盖),但与 v4 [鸡蛋,牛奶,火腿] 并发,所以服务器保留这两个并发值。
![](img/fig5-13.png)