mirror of
https://github.com/Vonng/ddia.git
synced 2024-12-06 15:20:12 +08:00
Merge pull request #93 from kemingy/fix-ch5
ch5: fix markdown and some typos
This commit is contained in:
commit
e663d950be
44
ch5.md
44
ch5.md
@ -22,19 +22,19 @@
|
|||||||
|
|
||||||
在复制时需要进行许多权衡:例如,使用同步复制还是异步复制?如何处理失败的副本?这些通常是数据库中的配置选项,细节因数据库而异,但原理在许多不同的实现中都类似。本章会讨论这些决策的后果。
|
在复制时需要进行许多权衡:例如,使用同步复制还是异步复制?如何处理失败的副本?这些通常是数据库中的配置选项,细节因数据库而异,但原理在许多不同的实现中都类似。本章会讨论这些决策的后果。
|
||||||
|
|
||||||
数据库的复制算得上是老生常谈了 ——70年代研究得出的基本原则至今没有太大变化【1】,因为网络的基本约束仍保持不变。然而在研究之外,许多开发人员仍然假设一个数据库只有一个节点。分布式数据库变为主流只是最近发生的事。许多程序员都是这一领域的新手,因此对于诸如**最终一致性(eventual consistency)**等问题存在许多误解。在“[复制延迟问题](#复制延迟问题)”一节,我们将更加精确地了解最终的一致性,并讨论诸如**读己之写(read-your-writes)**和**单调读(monotonic read)**保证等内容。
|
数据库的复制算得上是老生常谈了 ——70年代研究得出的基本原则至今没有太大变化【1】,因为网络的基本约束仍保持不变。然而在研究之外,许多开发人员仍然假设一个数据库只有一个节点。分布式数据库变为主流只是最近发生的事。许多程序员都是这一领域的新手,因此对于诸如 **最终一致性(eventual consistency)** 等问题存在许多误解。在“[复制延迟问题](#复制延迟问题)”一节,我们将更加精确地了解最终的一致性,并讨论诸如 **读己之写(read-your-writes)** 和 **单调读(monotonic read)** 保证等内容。
|
||||||
|
|
||||||
## 领导者与追随者
|
## 领导者与追随者
|
||||||
|
|
||||||
存储数据库副本的每个节点称为 **副本(replica)** 。当存在多个副本时,会不可避免的出现一个问题:如何确保所有数据都落在了所有的副本上?
|
存储数据库副本的每个节点称为 **副本(replica)** 。当存在多个副本时,会不可避免的出现一个问题:如何确保所有数据都落在了所有的副本上?
|
||||||
|
|
||||||
每一次向数据库的写入操作都需要传播到所有副本上,否则副本就会包含不一样的数据。最常见的解决方案被称为 **基于领导者的复制(leader-based replication)** (也称**主动/被动(active/passive)** 或 **主/从(master/slave)**复制),如[图5-1](#fig5-1.png)所示。它的工作原理如下:
|
每一次向数据库的写入操作都需要传播到所有副本上,否则副本就会包含不一样的数据。最常见的解决方案被称为 **基于领导者的复制(leader-based replication)** (也称 **主动/被动(active/passive)** 或 **主/从(master/slave)** 复制),如[图5-1](#fig5-1.png)所示。它的工作原理如下:
|
||||||
|
|
||||||
1. 副本之一被指定为 **领导者(leader)**,也称为 **主库(master|primary)** 。当客户端要向数据库写入时,它必须将请求发送给**领导者**,领导者会将新数据写入其本地存储。
|
1. 副本之一被指定为 **领导者(leader)**,也称为 **主库(master|primary)** 。当客户端要向数据库写入时,它必须将请求发送给**领导者**,领导者会将新数据写入其本地存储。
|
||||||
2. 其他副本被称为**追随者(followers)**,亦称为**只读副本(read replicas)**,**从库(slaves)**,**备库( sencondaries)**,**热备(hot-standby)**[^i]。每当领导者将新数据写入本地存储时,它也会将数据变更发送给所有的追随者,称之为**复制日志(replication log)**记录或**变更流(change stream)**。每个跟随者从领导者拉取日志,并相应更新其本地数据库副本,方法是按照领导者处理的相同顺序应用所有写入。
|
2. 其他副本被称为**追随者(followers)**,亦称为**只读副本(read replicas)**,**从库(slaves)**,**备库( sencondaries)**,**热备(hot-standby)**[^i]。每当领导者将新数据写入本地存储时,它也会将数据变更发送给所有的追随者,称之为**复制日志(replication log)**记录或**变更流(change stream)**。每个跟随者从领导者拉取日志,并相应更新其本地数据库副本,方法是按照领导者处理的相同顺序应用所有写入。
|
||||||
3. 当客户想要从数据库中读取数据时,它可以向领导者或追随者查询。 但只有领导者才能接受写操作(从客户端的角度来看从库都是只读的)。
|
3. 当客户想要从数据库中读取数据时,它可以向领导者或追随者查询。 但只有领导者才能接受写操作(从客户端的角度来看从库都是只读的)。
|
||||||
|
|
||||||
[^i]: 不同的人对**热(hot)**,**温(warm)**,**冷(cold)** 备份服务器有不同的定义。 例如在PostgreSQL中,**热备(hot standby)**指的是能接受客户端读请求的副本。而**温备(warm standby)**只是追随领导者,但不处理客户端的任何查询。 就本书而言,这些差异并不重要。
|
[^i]: 不同的人对 **热(hot)**,**温(warm)**,**冷(cold)** 备份服务器有不同的定义。 例如在PostgreSQL中,**热备(hot standby)** 指的是能接受客户端读请求的副本。而 **温备(warm standby)** 只是追随领导者,但不处理客户端的任何查询。 就本书而言,这些差异并不重要。
|
||||||
|
|
||||||
![](img/fig5-1.png)
|
![](img/fig5-1.png)
|
||||||
**图5-1 基于领导者(主-从)的复制**
|
**图5-1 基于领导者(主-从)的复制**
|
||||||
@ -43,7 +43,7 @@
|
|||||||
|
|
||||||
### 同步复制与异步复制
|
### 同步复制与异步复制
|
||||||
|
|
||||||
复制系统的一个重要细节是:复制是**同步(synchronously)**发生还是**异步(asynchronously)**发生。 (在关系型数据库中这通常是一个配置项,其他系统通常硬编码为其中一个)。
|
复制系统的一个重要细节是:复制是 **同步(synchronously)** 发生还是 **异步(asynchronously)** 发生。 (在关系型数据库中这通常是一个配置项,其他系统通常硬编码为其中一个)。
|
||||||
|
|
||||||
想象[图5-1](fig5-1.png)中发生的情况,网站的用户更新他们的个人头像。在某个时间点,客户向主库发送更新请求;不久之后主库就收到了请求。在某个时刻,主库又会将数据变更转发给自己的从库。最后,主库通知客户更新成功。
|
想象[图5-1](fig5-1.png)中发生的情况,网站的用户更新他们的个人头像。在某个时间点,客户向主库发送更新请求;不久之后主库就收到了请求。在某个时刻,主库又会将数据变更转发给自己的从库。最后,主库通知客户更新成功。
|
||||||
|
|
||||||
@ -82,7 +82,7 @@
|
|||||||
1. 在某个时刻获取主库的一致性快照(如果可能),而不必锁定整个数据库。大多数数据库都具有这个功能,因为它是备份必需的。对于某些场景,可能需要第三方工具,例如MySQL的innobackupex 【12】。
|
1. 在某个时刻获取主库的一致性快照(如果可能),而不必锁定整个数据库。大多数数据库都具有这个功能,因为它是备份必需的。对于某些场景,可能需要第三方工具,例如MySQL的innobackupex 【12】。
|
||||||
2. 将快照复制到新的从库节点。
|
2. 将快照复制到新的从库节点。
|
||||||
3. 从库连接到主库,并拉取快照之后发生的所有数据变更。这要求快照与主库复制日志中的位置精确关联。该位置有不同的名称:例如,PostgreSQL将其称为 **日志序列号(log sequence number, LSN)**,MySQL将其称为 **二进制日志坐标(binlog coordinates)**。
|
3. 从库连接到主库,并拉取快照之后发生的所有数据变更。这要求快照与主库复制日志中的位置精确关联。该位置有不同的名称:例如,PostgreSQL将其称为 **日志序列号(log sequence number, LSN)**,MySQL将其称为 **二进制日志坐标(binlog coordinates)**。
|
||||||
4. 当从库处理完快照之后积压的数据变更,我们说它**赶上(caught up)**了主库。现在它可以继续处理主库产生的数据变化了。
|
4. 当从库处理完快照之后积压的数据变更,我们说它 **赶上(caught up)** 了主库。现在它可以继续处理主库产生的数据变化了。
|
||||||
|
|
||||||
建立从库的实际步骤因数据库而异。在某些系统中,这个过程是完全自动化的,而在另外一些系统中,它可能是一个需要由管理员手动执行的,有点神秘的多步骤工作流。
|
建立从库的实际步骤因数据库而异。在某些系统中,这个过程是完全自动化的,而在另外一些系统中,它可能是一个需要由管理员手动执行的,有点神秘的多步骤工作流。
|
||||||
|
|
||||||
@ -132,7 +132,7 @@
|
|||||||
|
|
||||||
虽然听上去很合理,但有很多问题会搞砸这种复制方式:
|
虽然听上去很合理,但有很多问题会搞砸这种复制方式:
|
||||||
|
|
||||||
* 任何调用**非确定性函数(nondeterministic)**的语句,可能会在每个副本上生成不同的值。例如,使用`NOW()`获取当前日期时间,或使用`RAND()`获取一个随机数。
|
* 任何调用 **非确定性函数(nondeterministic)** 的语句,可能会在每个副本上生成不同的值。例如,使用`NOW()`获取当前日期时间,或使用`RAND()`获取一个随机数。
|
||||||
* 如果语句使用了**自增列(auto increment)**,或者依赖于数据库中的现有数据(例如,`UPDATE ... WHERE <某些条件>`),则必须在每个副本上按照完全相同的顺序执行它们,否则可能会产生不同的效果。当有多个并发执行的事务时,这可能成为一个限制。
|
* 如果语句使用了**自增列(auto increment)**,或者依赖于数据库中的现有数据(例如,`UPDATE ... WHERE <某些条件>`),则必须在每个副本上按照完全相同的顺序执行它们,否则可能会产生不同的效果。当有多个并发执行的事务时,这可能成为一个限制。
|
||||||
* 有副作用的语句(例如,触发器,存储过程,用户定义的函数)可能会在每个副本上产生不同的副作用,除非副作用是绝对确定的。
|
* 有副作用的语句(例如,触发器,存储过程,用户定义的函数)可能会在每个副本上产生不同的副作用,除非副作用是绝对确定的。
|
||||||
|
|
||||||
@ -242,7 +242,7 @@
|
|||||||
|
|
||||||
**图5-4 用户首先从新副本读取,然后从旧副本读取。时光倒流。为了防止这种异常,我们需要单调的读取。**
|
**图5-4 用户首先从新副本读取,然后从旧副本读取。时光倒流。为了防止这种异常,我们需要单调的读取。**
|
||||||
|
|
||||||
**单调读(Monotonic reads)**【23】是这种异常不会发生的保证。这是一个比**强一致性(strong consistency)**更弱,但比**最终一致性(eventually consistency)**更强的保证。当读取数据时,您可能会看到一个旧值;单调读取仅意味着如果一个用户顺序地进行多次读取,则他们不会看到时间后退,即,如果先前读取到较新的数据,后续读取不会得到更旧的数据。
|
**单调读(Monotonic reads)**【23】保证这种异常不会发生。这是一个比 **强一致性(strong consistency)** 更弱,但比 **最终一致性(eventually consistency)** 更强的保证。当读取数据时,您可能会看到一个旧值;单调读取仅意味着如果一个用户顺序地进行多次读取,则他们不会看到时间后退,即,如果先前读取到较新的数据,后续读取不会得到更旧的数据。
|
||||||
|
|
||||||
实现单调读取的一种方式是确保每个用户总是从同一个副本进行读取(不同的用户可以从不同的副本读取)。例如,可以基于用户ID的散列来选择副本,而不是随机选择副本。但是,如果该副本失败,用户的查询将需要重新路由到另一个副本。
|
实现单调读取的一种方式是确保每个用户总是从同一个副本进行读取(不同的用户可以从不同的副本读取)。例如,可以基于用户ID的散列来选择副本,而不是随机选择副本。但是,如果该副本失败,用户的查询将需要重新路由到另一个副本。
|
||||||
|
|
||||||
@ -287,7 +287,7 @@
|
|||||||
|
|
||||||
如前所述,应用程序可以提供比底层数据库更强有力的保证,例如通过主库进行某种读取。但在应用程序代码中处理这些问题是复杂的,容易出错。
|
如前所述,应用程序可以提供比底层数据库更强有力的保证,例如通过主库进行某种读取。但在应用程序代码中处理这些问题是复杂的,容易出错。
|
||||||
|
|
||||||
如果应用程序开发人员不必担心微妙的复制问题,并可以信赖他们的数据库“做了正确的事情”,那该多好呀。这就是**事务(transaction)**存在的原因:**数据库通过事务提供强大的保证**,所以应用程序可以更加简单。
|
如果应用程序开发人员不必担心微妙的复制问题,并可以信赖他们的数据库“做了正确的事情”,那该多好呀。这就是 **事务(transaction)** 存在的原因:**数据库通过事务提供强大的保证**,所以应用程序可以更加简单。
|
||||||
|
|
||||||
单节点事务已经存在了很长时间。然而在走向分布式(复制和分区)数据库时,许多系统放弃了事务。声称事务在性能和可用性上的代价太高,并断言在可伸缩系统中最终一致性是不可避免的。这个叙述有一些道理,但过于简单了,本书其余部分将提出更为细致的观点。第七章和第九章将回到事务的话题,并讨论一些替代机制。
|
单节点事务已经存在了很长时间。然而在走向分布式(复制和分区)数据库时,许多系统放弃了事务。声称事务在性能和可用性上的代价太高,并断言在可伸缩系统中最终一致性是不可避免的。这个叙述有一些道理,但过于简单了,本书其余部分将提出更为细致的观点。第七章和第九章将回到事务的话题,并讨论一些替代机制。
|
||||||
|
|
||||||
@ -387,7 +387,7 @@
|
|||||||
|
|
||||||
在多主配置中,没有明确的写入顺序,所以最终值应该是什么并不清楚。在[图5-7](img/fig5-7.png)中,在主库1中标题首先更新为B而后更新为C;在主库2中,首先更新为C,然后更新为B。两个顺序都不是“更正确”的。
|
在多主配置中,没有明确的写入顺序,所以最终值应该是什么并不清楚。在[图5-7](img/fig5-7.png)中,在主库1中标题首先更新为B而后更新为C;在主库2中,首先更新为C,然后更新为B。两个顺序都不是“更正确”的。
|
||||||
|
|
||||||
如果每个副本只是按照它看到写入的顺序写入,那么数据库最终将处于不一致的状态:最终值将是在主库1的C和主库2的B。这是不可接受的,每个复制方案都必须确保数据在所有副本中最终都是相同的。因此,数据库必须以一种**收敛(convergent)**的方式解决冲突,这意味着所有副本必须在所有变更复制完成时收敛至一个相同的最终值。
|
如果每个副本只是按照它看到写入的顺序写入,那么数据库最终将处于不一致的状态:最终值将是在主库1的C和主库2的B。这是不可接受的,每个复制方案都必须确保数据在所有副本中最终都是相同的。因此,数据库必须以一种 **收敛(convergent)** 的方式解决冲突,这意味着所有副本必须在所有变更复制完成时收敛至一个相同的最终值。
|
||||||
|
|
||||||
实现冲突合并解决有多种途径:
|
实现冲突合并解决有多种途径:
|
||||||
|
|
||||||
@ -447,7 +447,7 @@
|
|||||||
|
|
||||||
**图5-8 三个可以设置多领导者复制的示例拓扑。**
|
**图5-8 三个可以设置多领导者复制的示例拓扑。**
|
||||||
|
|
||||||
最普遍的拓扑是全部到全部([图5-8 [c]]()),其中每个领导者将其写入每个其他领导。但是,也会使用更多受限制的拓扑:例如,默认情况下,MySQL仅支持**环形拓扑(circular topology)**【34】,其中每个节点接收来自一个节点的写入,并将这些写入(加上自己的任何写入)转发给另一个节点。另一种流行的拓扑结构具有星形的形状[^v]。个指定的根节点将写入转发给所有其他节点。星型拓扑可以推广到树。
|
最普遍的拓扑是全部到全部([图5-8 [c]]()),其中每个领导者将其写入每个其他领导。但是,也会使用更多受限制的拓扑:例如,默认情况下,MySQL仅支持**环形拓扑(circular topology)**【34】,其中每个节点接收来自一个节点的写入,并将这些写入(加上自己的任何写入)转发给另一个节点。另一种流行的拓扑结构具有星形的形状[^v]。一个指定的根节点将写入转发给所有其他节点。星型拓扑可以推广到树。
|
||||||
|
|
||||||
[^v]: 不要与星型模式混淆(请参阅“[分析模式:星型还是雪花](ch2.md#分析模式:星型还是雪花)”),其中描述了数据模型的结构,而不是节点之间的通信拓扑。
|
[^v]: 不要与星型模式混淆(请参阅“[分析模式:星型还是雪花](ch2.md#分析模式:星型还是雪花)”),其中描述了数据模型的结构,而不是节点之间的通信拓扑。
|
||||||
|
|
||||||
@ -465,7 +465,7 @@
|
|||||||
|
|
||||||
这是一个因果关系的问题,类似于我们在“[一致前缀读](ch8.md#一致前缀读)”中看到的:更新取决于先前的插入,所以我们需要确保所有节点先处理插入,然后再处理更新。仅仅在每一次写入时添加一个时间戳是不够的,因为时钟不可能被充分地同步,以便在主库2处正确地排序这些事件(见[第8章](ch8.md))。
|
这是一个因果关系的问题,类似于我们在“[一致前缀读](ch8.md#一致前缀读)”中看到的:更新取决于先前的插入,所以我们需要确保所有节点先处理插入,然后再处理更新。仅仅在每一次写入时添加一个时间戳是不够的,因为时钟不可能被充分地同步,以便在主库2处正确地排序这些事件(见[第8章](ch8.md))。
|
||||||
|
|
||||||
要正确排序这些事件,可以使用一种称为**版本向量(version vectors)**的技术,本章稍后将讨论这种技术(参阅“[检测并发写入](#检测并发写入)”)。然而,冲突检测技术在许多多领导者复制系统中执行得不好。例如,在撰写本文时,PostgreSQL BDR不提供写入的因果排序【27】,而Tungsten Replicator for MySQL甚至不尝试检测冲突【34】。
|
要正确排序这些事件,可以使用一种称为 **版本向量(version vectors)** 的技术,本章稍后将讨论这种技术(参阅“[检测并发写入](#检测并发写入)”)。然而,冲突检测技术在许多多领导者复制系统中执行得不好。例如,在撰写本文时,PostgreSQL BDR不提供写入的因果排序【27】,而Tungsten Replicator for MySQL甚至不尝试检测冲突【34】。
|
||||||
|
|
||||||
如果您正在使用具有多领导者复制功能的系统,那么应该了解这些问题,仔细阅读文档,并彻底测试您的数据库,以确保它确实提供了您认为具有的保证。
|
如果您正在使用具有多领导者复制功能的系统,那么应该了解这些问题,仔细阅读文档,并彻底测试您的数据库,以确保它确实提供了您认为具有的保证。
|
||||||
|
|
||||||
@ -479,7 +479,7 @@
|
|||||||
|
|
||||||
[^vi]: Dynamo不适用于Amazon以外的用户。 令人困惑的是,AWS提供了一个名为DynamoDB的托管数据库产品,它使用了完全不同的体系结构:它基于单引导程序复制。
|
[^vi]: Dynamo不适用于Amazon以外的用户。 令人困惑的是,AWS提供了一个名为DynamoDB的托管数据库产品,它使用了完全不同的体系结构:它基于单引导程序复制。
|
||||||
|
|
||||||
在一些无领导者的实现中,客户端直接将写入发送到到几个副本中,而另一些情况下,一个**协调者(coordinator)**节点代表客户端进行写入。但与主库数据库不同,协调者不执行特定的写入顺序。我们将会看到,这种设计上的差异对数据库的使用方式有着深远的影响。
|
在一些无领导者的实现中,客户端直接将写入发送到到几个副本中,而另一些情况下,一个 **协调者(coordinator)** 节点代表客户端进行写入。但与主库数据库不同,协调者不执行特定的写入顺序。我们将会看到,这种设计上的差异对数据库的使用方式有着深远的影响。
|
||||||
|
|
||||||
### 当节点故障时写入数据库
|
### 当节点故障时写入数据库
|
||||||
|
|
||||||
@ -538,7 +538,7 @@
|
|||||||
|
|
||||||
**图5-11 如果$w + r > n$,读取r个副本,至少有一个r副本必然包含了最近的成功写入**
|
**图5-11 如果$w + r > n$,读取r个副本,至少有一个r副本必然包含了最近的成功写入**
|
||||||
|
|
||||||
如果少于所需的w或r节点可用,则写入或读取将返回错误。 由于许多原因,节点可能不可用:因为由于执行操作的错误(由于磁盘已满而无法写入)导致节点关闭(崩溃,关闭电源),由于客户端和服务器之间的网络中断 节点,或任何其他原因。 我们只关心节点是否返回了成功的响应,而不需要区分不同类型的错误。
|
如果少于所需的w或r节点可用,则写入或读取将返回错误。 由于许多原因,节点可能不可用:因为执行操作的错误(由于磁盘已满而无法写入)导致节点关闭(崩溃,关闭电源),由于客户端和服务器之间的网络中断节点,或任何其他原因。 我们只关心节点是否返回了成功的响应,而不需要区分不同类型的错误。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -559,7 +559,7 @@
|
|||||||
* 如果写操作与读操作同时发生,写操作可能仅反映在某些副本上。在这种情况下,不确定读取是返回旧值还是新值。
|
* 如果写操作与读操作同时发生,写操作可能仅反映在某些副本上。在这种情况下,不确定读取是返回旧值还是新值。
|
||||||
* 如果写操作在某些副本上成功,而在其他节点上失败(例如,因为某些节点上的磁盘已满),在小于w个副本上写入成功。所以整体判定写入失败,但整体写入失败并没有在写入成功的副本上回滚。这意味着如果一个写入虽然报告失败,后续的读取仍然可能会读取这次失败写入的值【47】。
|
* 如果写操作在某些副本上成功,而在其他节点上失败(例如,因为某些节点上的磁盘已满),在小于w个副本上写入成功。所以整体判定写入失败,但整体写入失败并没有在写入成功的副本上回滚。这意味着如果一个写入虽然报告失败,后续的读取仍然可能会读取这次失败写入的值【47】。
|
||||||
* 如果携带新值的节点失败,需要读取其他带有旧值的副本。并且其数据从带有旧值的副本中恢复,则存储新值的副本数可能会低于w,从而打破法定人数条件。
|
* 如果携带新值的节点失败,需要读取其他带有旧值的副本。并且其数据从带有旧值的副本中恢复,则存储新值的副本数可能会低于w,从而打破法定人数条件。
|
||||||
* 即使一切工作正常,有时也会不幸地出现关于**时序(timing)**的边缘情况,我们将在第334页上的“[线性化和法定人数](ch9.md#线性化和法定人数)”中看到这点。
|
* 即使一切工作正常,有时也会不幸地出现关于**时序(timing)** 的边缘情况,我们将在第334页上的“[线性化和法定人数](ch9.md#线性化和法定人数)”中看到这点。
|
||||||
|
|
||||||
因此,尽管法定人数似乎保证读取返回最新的写入值,但在实践中并不那么简单。 Dynamo风格的数据库通常针对可以忍受最终一致性的用例进行优化。允许通过参数w和r来调整读取陈旧值的概率,但把它们当成绝对的保证是不明智的。
|
因此,尽管法定人数似乎保证读取返回最新的写入值,但在实践中并不那么简单。 Dynamo风格的数据库通常针对可以忍受最终一致性的用例进行优化。允许通过参数w和r来调整读取陈旧值的概率,但把它们当成绝对的保证是不明智的。
|
||||||
|
|
||||||
@ -584,7 +584,7 @@
|
|||||||
在一个大型的群集中(节点数量明显多于n个),网络中断期间客户端可能连接到某些数据库节点,而不是为了为特定值组成法定人数的节点们。在这种情况下,数据库设计人员需要权衡一下:
|
在一个大型的群集中(节点数量明显多于n个),网络中断期间客户端可能连接到某些数据库节点,而不是为了为特定值组成法定人数的节点们。在这种情况下,数据库设计人员需要权衡一下:
|
||||||
|
|
||||||
* 将错误返回给我们无法达到w或r节点的法定数量的所有请求是否更好?
|
* 将错误返回给我们无法达到w或r节点的法定数量的所有请求是否更好?
|
||||||
* 或者我们是否应该接受写入,然后将它们写入一些可达的节点,但不在n值通常存在的n个节点之间?
|
* 或者我们是否应该接受写入,然后将它们写入一些可达的节点,但不在这些值通常存在的n个节点之间?
|
||||||
|
|
||||||
后者被认为是一个**宽松的法定人数(sloppy quorum)**【37】:写和读仍然需要w和r成功的响应,但是那些可能包括不在指定的n个“主”节点中的值。比方说,如果你把自己锁在房子外面,你可能会敲开邻居的门,问你是否可以暂时停留在沙发上。
|
后者被认为是一个**宽松的法定人数(sloppy quorum)**【37】:写和读仍然需要w和r成功的响应,但是那些可能包括不在指定的n个“主”节点中的值。比方说,如果你把自己锁在房子外面,你可能会敲开邻居的门,问你是否可以暂时停留在沙发上。
|
||||||
|
|
||||||
@ -626,11 +626,11 @@
|
|||||||
|
|
||||||
#### 最后写入胜利(丢弃并发写入)
|
#### 最后写入胜利(丢弃并发写入)
|
||||||
|
|
||||||
实现最终融合的一种方法是声明每个副本只需要存储最**“最近”**的值,并允许**“更旧”**的值被覆盖和抛弃。然后,只要我们有一种明确的方式来确定哪个写是“最近的”,并且每个写入最终都被复制到每个副本,那么复制最终会收敛到相同的值。
|
实现最终融合的一种方法是声明每个副本只需要存储最 **“最近”** 的值,并允许 **“更旧”** 的值被覆盖和抛弃。然后,只要我们有一种明确的方式来确定哪个写是“最近的”,并且每个写入最终都被复制到每个副本,那么复制最终会收敛到相同的值。
|
||||||
|
|
||||||
正如**“最近”**的引号所表明的,这个想法其实颇具误导性。在[图5-12](img/fig5-12.png)的例子中,当客户端向数据库节点发送写入请求时,客户端都不知道另一个客户端,因此不清楚哪一个先发生了。事实上,说“发生”是没有意义的:我们说写入是**并发(concurrent)**的,所以它们的顺序是不确定的。
|
正如 **“最近”** 的引号所表明的,这个想法其实颇具误导性。在[图5-12](img/fig5-12.png)的例子中,当客户端向数据库节点发送写入请求时,客户端都不知道另一个客户端,因此不清楚哪一个先发生了。事实上,说“发生”是没有意义的:我们说写入是 **并发(concurrent)** 的,所以它们的顺序是不确定的。
|
||||||
|
|
||||||
即使写入没有自然的排序,我们也可以强制任意排序。例如,可以为每个写入附加一个时间戳,挑选最**“最近”**的最大时间戳,并丢弃具有较早时间戳的任何写入。这种冲突解决算法被称为**最后写入胜利(LWW, last write wins)**,是Cassandra 【53】唯一支持的冲突解决方法,也是Riak 【35】中的一个可选特征。
|
即使写入没有自然的排序,我们也可以强制任意排序。例如,可以为每个写入附加一个时间戳,挑选最 **“最近”** 的最大时间戳,并丢弃具有较早时间戳的任何写入。这种冲突解决算法被称为 **最后写入胜利(LWW, last write wins)**,是Cassandra 【53】唯一支持的冲突解决方法,也是Riak 【35】中的一个可选特征。
|
||||||
|
|
||||||
LWW实现了最终收敛的目标,但以**持久性**为代价:如果同一个Key有多个并发写入,即使它们都被报告为客户端成功(因为它们被写入 w 个副本),但只有一个写入将存活,而其他写入将被静默丢弃。此外,LWW甚至可能会删除不是并发的写入,我们将在的“[有序事件的时间戳](ch8.md#有序事件的时间戳)”中讨论。
|
LWW实现了最终收敛的目标,但以**持久性**为代价:如果同一个Key有多个并发写入,即使它们都被报告为客户端成功(因为它们被写入 w 个副本),但只有一个写入将存活,而其他写入将被静默丢弃。此外,LWW甚至可能会删除不是并发的写入,我们将在的“[有序事件的时间戳](ch8.md#有序事件的时间戳)”中讨论。
|
||||||
|
|
||||||
@ -642,7 +642,7 @@
|
|||||||
|
|
||||||
我们如何判断两个操作是否是并发的?为了建立一个直觉,让我们看看一些例子:
|
我们如何判断两个操作是否是并发的?为了建立一个直觉,让我们看看一些例子:
|
||||||
|
|
||||||
* 在[图5-9](fig5-9.png)中,两个写入不是并发的:A的插入发生在B的增量之前,因为B递增的值是A插入的值。换句话说,B的操作建立在A的操作上,所以B的操作必须有后来发生。我们也可以说B是**因果依赖(causally dependent)**于A
|
* 在[图5-9](fig5-9.png)中,两个写入不是并发的:A的插入发生在B的增量之前,因为B递增的值是A插入的值。换句话说,B的操作建立在A的操作上,所以B的操作必须有后来发生。我们也可以说B是 **因果依赖(causally dependent)** 于A
|
||||||
* 另一方面,[图5-12](fig5-12.png)中的两个写入是并发的:当每个客户端启动操作时,它不知道另一个客户端也正在执行操作同样的Key。因此,操作之间不存在因果关系。
|
* 另一方面,[图5-12](fig5-12.png)中的两个写入是并发的:当每个客户端启动操作时,它不知道另一个客户端也正在执行操作同样的Key。因此,操作之间不存在因果关系。
|
||||||
|
|
||||||
如果操作B了解操作A,或者依赖于A,或者以某种方式构建于操作A之上,则操作A在另一个操作B之前发生。在另一个操作之前是否发生一个操作是定义什么并发的关键。事实上,我们可以简单地说,如果两个操作都不在另一个之前发生,那么两个操作是并发的(即,两个操作都不知道另一个)【54】。
|
如果操作B了解操作A,或者依赖于A,或者以某种方式构建于操作A之上,则操作A在另一个操作B之前发生。在另一个操作之前是否发生一个操作是定义什么并发的关键。事实上,我们可以简单地说,如果两个操作都不在另一个之前发生,那么两个操作是并发的(即,两个操作都不知道另一个)【54】。
|
||||||
@ -653,7 +653,7 @@
|
|||||||
|
|
||||||
> #### 并发性,时间和相对性
|
> #### 并发性,时间和相对性
|
||||||
>
|
>
|
||||||
> 如果两个操作**“同时”**发生,似乎应该称为并发——但事实上,它们在字面时间上重叠与否并不重要。由于分布式系统中的时钟问题,现实中是很难判断两个事件是否**同时**发生的,这个问题我们将在[第8章](ch8.md)中详细讨论。
|
> 如果两个操作 **“同时”** 发生,似乎应该称为并发——但事实上,它们在字面时间上重叠与否并不重要。由于分布式系统中的时钟问题,现实中是很难判断两个事件是否**同时**发生的,这个问题我们将在[第8章](ch8.md)中详细讨论。
|
||||||
>
|
>
|
||||||
> 为了定义并发性,确切的时间并不重要:如果两个操作都意识不到对方的存在,就称这两个操作**并发**,而不管它们发生的物理时间。人们有时把这个原理和狭义相对论的物理学联系起来【54】,它引入了信息不能比光速更快的思想。因此,如果事件之间的时间短于光通过它们之间的距离,那么发生一定距离的两个事件不可能相互影响。
|
> 为了定义并发性,确切的时间并不重要:如果两个操作都意识不到对方的存在,就称这两个操作**并发**,而不管它们发生的物理时间。人们有时把这个原理和狭义相对论的物理学联系起来【54】,它引入了信息不能比光速更快的思想。因此,如果事件之间的时间短于光通过它们之间的距离,那么发生一定距离的两个事件不可能相互影响。
|
||||||
>
|
>
|
||||||
@ -700,7 +700,7 @@
|
|||||||
|
|
||||||
以购物车为例,一种合理的合并兄弟方法就是集合求并。在[图5-14](img/fig5-14.png)中,最后的两个兄弟是[牛奶,面粉,鸡蛋,熏肉]和[鸡蛋,牛奶,火腿]。注意牛奶和鸡蛋出现在两个,即使他们每个只写一次。合并的价值可能是像[牛奶,面粉,鸡蛋,培根,火腿],没有重复。
|
以购物车为例,一种合理的合并兄弟方法就是集合求并。在[图5-14](img/fig5-14.png)中,最后的两个兄弟是[牛奶,面粉,鸡蛋,熏肉]和[鸡蛋,牛奶,火腿]。注意牛奶和鸡蛋出现在两个,即使他们每个只写一次。合并的价值可能是像[牛奶,面粉,鸡蛋,培根,火腿],没有重复。
|
||||||
|
|
||||||
然而,如果你想让人们也可以从他们的手推车中**删除**东西,而不是仅仅添加东西,那么把兄弟求并可能不会产生正确的结果:如果你合并了两个兄弟手推车,并且只在其中一个兄弟值里删掉了它,那么被删除的项目会重新出现在兄弟的并集中【37】。为了防止这个问题,一个项目在删除时不能简单地从数据库中删除;相反,系统必须留下一个具有合适版本号的标记,以指示合并兄弟时该项目已被删除。这种删除标记被称为**墓碑(tombstone)**。 (我们之前在“[哈希索引”](ch3.md#哈希索引)中的日志压缩的上下文中看到了墓碑。)
|
然而,如果你想让人们也可以从他们的手推车中**删除**东西,而不是仅仅添加东西,那么把兄弟求并可能不会产生正确的结果:如果你合并了两个兄弟手推车,并且只在其中一个兄弟值里删掉了它,那么被删除的项目会重新出现在兄弟的并集中【37】。为了防止这个问题,一个项目在删除时不能简单地从数据库中删除;相反,系统必须留下一个具有合适版本号的标记,以指示合并兄弟时该项目已被删除。这种删除标记被称为**墓碑(tombstone)**。 (我们之前在“[哈希索引”](ch3.md#哈希索引)中的日志压缩的上下文中看到了墓碑。)
|
||||||
|
|
||||||
因为在应用程序代码中合并兄弟是复杂且容易出错的,所以有一些数据结构被设计出来用于自动执行这种合并,如“[自动冲突解决]()”中讨论的。例如,Riak的数据类型支持使用称为CRDT的数据结构家族【38,39,55】可以以合理的方式自动合并兄弟,包括保留删除。
|
因为在应用程序代码中合并兄弟是复杂且容易出错的,所以有一些数据结构被设计出来用于自动执行这种合并,如“[自动冲突解决]()”中讨论的。例如,Riak的数据类型支持使用称为CRDT的数据结构家族【38,39,55】可以以合理的方式自动合并兄弟,包括保留删除。
|
||||||
|
|
||||||
@ -712,7 +712,7 @@
|
|||||||
|
|
||||||
所有副本的版本号集合称为**版本向量(version vector)**【56】。这个想法的一些变体正在使用,但最有趣的可能是在Riak 2.0 【58,59】中使用的**分散版本矢量(dotted version vector)**【57】。我们不会深入细节,但是它的工作方式与我们在购物车示例中看到的非常相似。
|
所有副本的版本号集合称为**版本向量(version vector)**【56】。这个想法的一些变体正在使用,但最有趣的可能是在Riak 2.0 【58,59】中使用的**分散版本矢量(dotted version vector)**【57】。我们不会深入细节,但是它的工作方式与我们在购物车示例中看到的非常相似。
|
||||||
|
|
||||||
与[图5-13](img/fig5-13.png)中的版本号一样,当读取值时,版本向量会从数据库副本发送到客户端,并且随后写入值时需要将其发送回数据库。 (Riak将版本向量编码为一个字符串,它称为**因果上下文(causal context)**)。版本向量允许数据库区分覆盖写入和并发写入。
|
与[图5-13](img/fig5-13.png)中的版本号一样,当读取值时,版本向量会从数据库副本发送到客户端,并且随后写入值时需要将其发送回数据库。(Riak将版本向量编码为一个字符串,它称为**因果上下文(causal context)**)。版本向量允许数据库区分覆盖写入和并发写入。
|
||||||
|
|
||||||
另外,就像在单个副本的例子中,应用程序可能需要合并兄弟。版本向量结构确保从一个副本读取并随后写回到另一个副本是安全的。这样做可能会创建兄弟,但只要兄弟姐妹合并正确,就不会丢失数据。
|
另外,就像在单个副本的例子中,应用程序可能需要合并兄弟。版本向量结构确保从一个副本读取并随后写回到另一个副本是安全的。这样做可能会创建兄弟,但只要兄弟姐妹合并正确,就不会丢失数据。
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user