update ch5.md for multi-leader replication

This commit is contained in:
Gang Yin 2022-04-25 19:38:39 +08:00
parent 1a4321904e
commit 18ad7510ae

85
ch5.md
View File

@ -288,29 +288,29 @@
## 多主复制
本章到目前为止,我们只考虑使用单个领导者的复制架构。 虽然这是一种常见的方法,但也有一些有趣的选择。
本章到目前为止,我们只考虑了使用单个主库的复制架构。虽然这是一种常见的方法,但还有其它一些有趣的选择。
基于领导者的复制有一个主要的缺点:只有一个主库,而所有的写入都必须通过它 [^iv]。如果出于任何原因(例如和主库之间的网络连接中断)无法连接到主库, 就无法向数据库写入。
基于领导者的复制有一个主要的缺点:只有一个主库,而所有的写入都必须通过它 [^iv]。如果出于任何原因(例如和主库之间的网络连接中断)无法连接到主库, 就无法向数据库写入。
[^iv]: 如果数据库被分区(见 [第六章](ch6.md)),每个分区都有一个领导。 不同的分区可能在不同的节点上有其领导者,但是每个分区必须有一个领导者节点
[^iv]: 如果数据库被分区(见 [第六章](ch6.md)),每个分区都有一个主库。不同的分区的主库可能在不同的节点上,但是每个分区都必须有一个主库
基于领导者的复制模型的自然延伸是允许多个节点接受写入。 复制仍然以同样的方式发生:处理写入的每个节点都必须将该数据更改转发给所有其他节点。 称之为 **多领导者配置**(也称多主、多活复制)。 在这种情况下,每个领导者同时扮演其他领导者的追随者
基于领导者的复制模型的自然延伸是允许多个节点接受写入。复制仍然以同样的方式发生:处理写入的每个节点都必须将该数据变更转发给所有其他节点。我们将其称之为 **多领导者配置**multi-leader configuration也称多主、多活复制即 master-master replication 或 active/active replication。在这种情况下每个主库同时是其他主库的从库
### 多主复制的应用场景
在单个数据中心内部使用多个主库没有太大意义,因为复杂性已经超过了能带来的好处。 但在一些情况下,多活配置是也合理的。
在单个数据中心内部使用多个主库的配置没有太大意义,因为其导致的复杂性已经超过了能带来的好处。但在一些情况下,这种配置也是合理的。
#### 运维多个数据中心
假如你有一个数据库,副本分散在好几个不同的数据中心(也许这样可以容忍单个数据中心的故障,或地理上更接近用户)。 使用常规的基于领导者的复制设置,主库必须位于其中一个数据中心,且所有写入都必须经过该数据中心。
假如你有一个数据库,副本分散在好几个不同的数据中心(可能会用来容忍单个数据中心的故障,或者为了在地理上更接近用户)。如果使用常规的基于领导者的复制设置,主库必须位于其中一个数据中心,且所有写入都必须经过该数据中心。
多领导者配置中可以在每个数据中心都有主库。 [图 5-6](img/fig5-6.png) 展示了这个架构的样子 在每个数据中心内使用常规的主从复制;在数据中心之间,每个数据中心的主库都会将其更改复制到其他数据中心的主库中。
多领导者配置中可以在每个数据中心都有主库。[图 5-6](img/fig5-6.png) 展示了这个架构。在每个数据中心内使用常规的主从复制;在数据中心之间,每个数据中心的主库都会将其更改复制到其他数据中心的主库中。
![](img/fig5-6.png)
**图 5-6 跨多个数据中心的多主复制**
我们来比较一下在运维多个数据中心时,单主和多主的适应情况
我们来比较一下在运维多个数据中心时,单主和多主的适应情况
* 性能
@ -318,17 +318,17 @@
* 容忍数据中心停机
在单主配置中,如果主库所在的数据中心发生故障,故障切换必须使另一个数据中心里的追随者成为领导者。在多主配置中,每个数据中心可以独立于其他数据中心继续运行,并且当发生故障的数据中心归队时,复制会自动赶上。
在单主配置中,如果主库所在的数据中心发生故障,故障切换必须使另一个数据中心里的从库成为主库。在多主配置中,每个数据中心可以独立于其他数据中心继续运行,并且当发生故障的数据中心归队时,复制会自动赶上。
* 容忍网络问题
数据中心之间的通信通常穿过公共互联网,这可能不如数据中心内的本地网络可靠。单主配置对数据中心间的连接问题非常敏感,因为通过这个连接进行的写操作是同步的。采用异步复制功能的多主配置通常能更好地承受网络问题:临时的网络中断并不会妨碍正在处理的写入。
数据中心之间的通信通常穿过公共互联网,这可能不如数据中心内的本地网络可靠。单主配置对数据中心间的连接问题非常敏感,因为通过这个连接进行的写操作是同步的。采用异步复制功能的多主配置通常能更好地承受网络问题:临时的网络中断并不会妨碍正在处理的写入。
有些数据库默认情况下支持多主配置,但使用外部工具实现也很常见,例如用于 MySQL 的 Tungsten Replicator 【26】用于 PostgreSQL 的 BDR【27】以及用于 Oracle 的 GoldenGate 【19】。
尽管多主复制有这些优势,但也有一个很大的缺点:两个不同的数据中心可能会同时修改相同的数据,写冲突是必须解决的(如 [图 5-6](img/fig5-6.png) 中 “冲突解决conflict resolution。本书将在 “[处理写入冲突](#处理写入冲突)” 中详细讨论这个问题。
尽管多主复制有这些优势,但也有一个很大的缺点:两个不同的数据中心可能会同时修改相同的数据,写冲突是必须解决的(如 [图 5-6](img/fig5-6.png) 中 “冲突解决conflict resolution。本书将在 “[处理写入冲突](#处理写入冲突)” 中详细讨论这个问题。
由于多主复制在许多数据库中都属于改装的功能,所以常常存在微妙的配置缺陷,且经常与其他数据库功能之间出现意外的反应。例如自增主键、触发器、完整性约束等,都可能会有麻烦。因此多主复制往往被认为是危险的领域应尽可能避免【28】。
由于多主复制在许多数据库中都属于改装的功能,所以常常存在微妙的配置缺陷,且经常与其他数据库功能之间出现意外的反应。比如自增主键、触发器、完整性约束等都可能会有麻烦。因此多主复制往往被认为是危险的领域应尽可能避免【28】。
#### 需要离线操作的客户端
@ -336,19 +336,19 @@
例如,考虑手机,笔记本电脑和其他设备上的日历应用。无论设备目前是否有互联网连接,你需要能随时查看你的会议(发出读取请求),输入新的会议(发出写入请求)。如果在离线状态下进行任何更改,则设备下次上线时,需要与服务器和其他设备同步。
在这种情况下,每个设备都有一个充当领导者的本地数据库(它接受写请求),并且在所有设备上的日历副本之间同步时,存在异步的多主复制过程。复制延迟可能是几小时甚至几天,具体取决于何时可以访问互联网。
在这种情况下,每个设备都有一个充当主库的本地数据库(它接受写请求),并且在所有设备上的日历副本之间同步时,存在异步的多主复制过程。复制延迟可能是几小时甚至几天,具体取决于何时可以访问互联网。
从架构的角度来看,这种设置实际上与数据中心之间的多领导者复制类似,每个设备都是一个 “数据中心”,而它们之间的网络连接是极度不可靠的。从历史上各类日历同步功能的破烂实现可以看出,想把多活配好是多么困难的一件事。
从架构的角度来看,这种设置实际上与数据中心之间的多领导者复制类似,每个设备都是一个 “数据中心”,而它们之间的网络连接是极度不可靠的。从历史上各类日历同步功能的破烂实现可以看出,想把多主复制用好是多么困难的一件事。
有一些工具旨在使这种多领导者配置更容易。例如CouchDB 就是为这种操作模式而设计的【29】。
#### 协同编辑
实时协作编辑应用程序允许多个人同时编辑文档。例如Etherpad 【30】和 Google Docs 【31】允许多人同时编辑文本文档或电子表格该算法在 “[自动冲突解决](#自动冲突解决)” 中简要讨论。我们通常不会将协作式编辑视为数据库复制问题但与前面提到的离线编辑用例有许多相似之处。当一个用户编辑文档时所做的更改将立即应用到其本地副本Web 浏览器或客户端应用程序中的文档状态),并异步复制到服务器和编辑同一文档的任何其他用户。
实时协作编辑应用程序允许多个人同时编辑文档。例如Etherpad 【30】和 Google Docs 【31】允许多人同时编辑文本文档或电子表格该算法在 “[自动冲突解决](#自动冲突解决)” 中简要讨论)。我们通常不会将协作式编辑视为数据库复制问题,但与前面提到的离线编辑用例有许多相似之处。当一个用户编辑文档时所做的更改将立即应用到其本地副本Web 浏览器或客户端应用程序中的文档状态),并异步复制到服务器和编辑同一文档的任何其他用户。
如果要保证不会发生编辑冲突,则应用程序必须先取得文档的锁定,然后用户才能对其进行编辑。如果另一个用户想要编辑同一个文档,他们首先必须等到第一个用户提交修改并释放锁定。这种协作模式相当于主从复制模型下在主节点上执行事务操作。
但是,为了加速协作,你可能希望将更改的单位设置得非常小(例如,一个按键),并避免锁定。这种方法允许多个用户同时进行编辑,但同时也带来了多领导者复制的所有挑战包括需要解决冲突【32】。
但是,为了加速协作,你可能希望将更改的单位设置得非常小(例如单次按键),并避免锁定。这种方法允许多个用户同时进行编辑,但同时也带来了多复制的所有挑战包括需要解决冲突【32】。
### 处理写入冲突
@ -362,17 +362,17 @@
#### 同步与异步冲突检测
在单主数据库中,第二个写入将被阻塞,并等待第一个写入完成,或中止第二个写入事务,强制用户重试。另一方面,在多主配置中,两个写入都是成功的,并且在稍后的时间点仅仅异步地检测到冲突。那时要求用户解决冲突可能为时已晚。
在单主数据库中,第二个写入将被阻塞并等待第一个写入完成,或者中止第二个写入事务并强制用户重试。另一方面,在多主配置中,两个写入都是成功的,在稍后的某个时间点才能异步地检测到冲突。那时再来要求用户解决冲突可能为时已晚。
原则上,可以使冲突检测同步 - 即等待写入被复制到所有副本,然后再告诉用户写入成功。但是,通过这样做,你将失去多主复制的主要优点:允许每个副本独立接受写入。如果你想要同步冲突检测,那么你可以使用单主程序复制。
原则上,可以使冲突检测同步 - 即等待写入被复制到所有副本,然后再告诉用户写入成功。但是,通过这样做,你将失去多主复制的主要优点:允许每个副本独立接受写入。如果你想要同步冲突检测,那么你可能不如直接使用单主复制。
#### 避免冲突
处理冲突的最简单的策略就是避免它们:如果应用程序可以确保特定记录的所有写入都通过同一个领导者,那么冲突就不会发生。由于许多的多领导者复制实现在处理冲突时处理得相当不好避免冲突是一个经常推荐的方法【34】。
处理冲突的最简单的策略就是避免它们:如果应用程序可以确保特定记录的所有写入都通过同一个主库,那么冲突就不会发生。由于许多的多主复制实现在处理冲突时处理得相当不好,避免冲突是一个经常推荐的方法【34】。
例如,在用户可以编辑自己数据的应用程序中,可以确保来自特定用户的请求始终路由到同一数据中心,并使用该数据中心的领导者进行读写。不同的用户可能有不同的 “家庭” 数据中心(可能根据用户的地理位置选择),但从任何用户的角度来看,配置基本上都是单一的领导者
例如,在一个用户可以编辑自己数据的应用程序中,可以确保来自特定用户的请求始终路由到同一数据中心,并使用该数据中心的主库进行读写。不同的用户可能有不同的 “主” 数据中心(可能根据用户的地理位置选择),但从任何一位用户的角度来看,本质上就是单主配置了
但是,有时你可能需要更改指定的记录的主库 —— 可能是因为一个数据中心出现故障,你需要将流量重新路由到另一个数据中心,或者可能是因为用户已经迁移到另一个位置,现在更接近不同的数据中心。在这种情况下,冲突避免会中断,你必须处理不同主库同时写入的可能性。
但是,有时你可能需要更改被指定的主库 —— 可能是因为某个数据中心出现故障,你需要将流量重新路由到另一个数据中心,或者可能是因为用户已经迁移到另一个位置,现在更接近其它的数据中心。在这种情况下,冲突避免将失效,你必须处理不同主库同时写入的可能性。
#### 收敛至一致的状态
@ -380,11 +380,11 @@
在多主配置中,没有明确的写入顺序,所以最终值应该是什么并不清楚。在 [图 5-7](img/fig5-7.png) 中,在主库 1 中标题首先更新为 B 而后更新为 C在主库 2 中,首先更新为 C然后更新为 B。两种顺序都不比另一种“更正确”。
如果每个副本只是按照它看到写入的顺序写入,那么数据库最终将处于不一致的状态:最终值将是在主库 1 的 C 和主库 2 的 B。这是不可接受的每个复制方案都必须确保数据在所有副本中最终都是相同的。因此,数据库必须以一种 **收敛convergent** 的方式解决冲突,这意味着所有副本必须在所有变更复制完成时收敛至一个相同的最终值。
如果每个副本只是按照它看到写入的顺序写入,那么数据库最终将处于不一致的状态:最终值将是在主库 1 的 C 和主库 2 的 B。这是不可接受的每个复制方案都必须确保数据最终在所有副本中都是相同的。因此,数据库必须以一种 **收敛convergent** 的方式解决冲突,这意味着所有副本必须在所有变更复制完成时收敛至一个相同的最终值。
实现冲突合并解决有多种途径:
* 给每个写入一个唯一的 ID例如,一个时间戳,一个长的随机数,一个 UUID 或者一个键和值的哈希),挑选最高 ID 的写入作为胜利者,并丢弃其他写入。如果使用时间戳,这种技术被称为 **最后写入胜利LWW, last write wins**。虽然这种方法很流行但是很容易造成数据丢失【35】。我们将在本章末尾的 [检测并发写入](#检测并发写入) 更详细地讨论 LWW。
* 给每个写入一个唯一的 ID例如时间戳、长随机数、UUID 或者键和值的哈希),挑选最高 ID 的写入作为胜利者,并丢弃其他写入。如果使用时间戳,这种技术被称为 **最后写入胜利LWW, last write wins**。虽然这种方法很流行但是很容易造成数据丢失【35】。我们将在本章末尾的 [检测并发写入](#检测并发写入) 一节更详细地讨论 LWW。
* 为每个副本分配一个唯一的 IDID 编号更高的写入具有更高的优先级。这种方法也意味着数据丢失。
* 以某种方式将这些值合并在一起 - 例如,按字母顺序排序,然后连接它们(在 [图 5-7](img/fig5-7.png) 中,合并的标题可能类似于 “B/C”
* 用一种可保留所有信息的显式数据结构来记录冲突,并编写解决冲突的应用程序代码(也许通过提示用户的方式)。
@ -392,7 +392,7 @@
#### 自定义冲突解决逻辑
作为解决冲突最合适的方法可能取决于应用程序,大多数多主复制工具允许使用应用程序代码编写冲突解决逻辑。该代码可以在写入或读取时执行:
解决冲突最合适的方法可能取决于应用程序,大多数多主复制工具允许使用应用程序代码编写冲突解决逻辑。该代码可以在写入或读取时执行:
* 写时执行
@ -400,63 +400,62 @@
* 读时执行
当检测到冲突时,所有冲突写入被存储。下一次读取数据时,会将这些多个版本的数据返回给应用程序。应用程序可能会提示用户或自动解决冲突并将结果写回数据库。例如CouchDB 以这种方式工作。
当检测到冲突时,所有冲突写入被存储。下一次读取数据时,会将这些多个版本的数据返回给应用程序。应用程序可以提示用户或自动解决冲突,并将结果写回数据库。例如 CouchDB 就以这种方式工作。
请注意,冲突解决通常适用于单行或文档层面而不是整个事务【36】。因此如果你有一个事务会原子性地进行几次不同的写入请参阅 [第七章](ch7.md),对于冲突解决而言,每个写入仍需分开单独考虑。
请注意,冲突解决通常适用于单行记录单个文档层面而不是整个事务【36】。因此如果你有一个事务会原子性地进行几次不同的写入请参阅 [第七章](ch7.md),对于冲突解决而言,每个写入仍需分开单独考虑。
> #### 自动冲突解决
>
> 冲突解决规则可能很快变得复杂,并且自定义代码可能容易出错。亚马逊是一个经常被引用的例子由于冲突解决处理程序而产生令人意外的效果一段时间以来购物车上的冲突解决逻辑将保留添加到购物车的物品但不包括从购物车中移除的物品。因此顾客有时会看到物品重新出现在他们的购物车中即使他们之前已经被移走【37】。
> 冲突解决规则可能很容易变得变得越来越复杂,自定义代码可能也很容易出错。亚马逊是一个经常被引用的例子,由于冲突解决处理程序而产生令人意外的效果一段时间以来购物车上的冲突解决逻辑将保留添加到购物车的物品但不包括从购物车中移除的物品。因此顾客有时会看到物品重新出现在他们的购物车中即使他们之前已经被移走【37】。
>
> 已经有一些有趣的研究来自动解决由于数据修改引起的冲突。有几项研究值得一提:
>
> * **无冲突复制数据类型Conflict-free replicated datatypesCRDT**【32,38】是可以由多个用户同时编辑的集合,映射,有序列表,计数器等的一系列数据结构,它们以合理的方式自动解决冲突。一些 CRDT 已经在 Riak 2.0 中实现【39,40】。
> * **无冲突复制数据类型Conflict-free replicated datatypesCRDT**【32,38】是可以由多个用户同时编辑的集合、映射、有序列表、计数器等一系列数据结构,它们以合理的方式自动解决冲突。一些 CRDT 已经在 Riak 2.0 中实现【39,40】。
> * **可合并的持久数据结构Mergeable persistent data structures**【41】显式跟踪历史记录类似于 Git 版本控制系统,并使用三向合并功能(而 CRDT 使用双向合并)。
> * **可执行的转换operational transformation**[42] 是 Etherpad 【30】和 Google Docs 【31】等合作编辑应用背后的冲突解决算法。它是专为同时编辑项目的有序列表而设计的例如构成文本文档的字符列表。
>
> 这些算法在数据库中的实现还很年轻,但很可能将来它们将被集成到更多的复制数据系统中。自动冲突解决方案可以使应用程序处理多领导者数据同步更为简单。
> * **操作转换operational transformation**[42] 是 Etherpad 【30】和 Google Docs 【31】等协同编辑应用背后的冲突解决算法。它是专为有序列表的并发编辑而设计的例如构成文本文档的字符列表。
>
> 这些算法在数据库中的实现还很年轻,但很可能将来它们会被集成到更多的复制数据系统中。自动冲突解决方案可以使应用程序处理多主数据同步更为简单。
#### 什么是冲突?
有些冲突是显而易见的。在 [图 5-7](img/fig5-7.png) 的例子中,两个写操作并发地修改了同一条记录中的同一个字段,并将其设置为两个不同的值。毫无疑问这是一个冲突。
其他类型的冲突可能更为微妙难以发现。例如,考虑一个会议室预订系统:它记录谁订了哪个时间段的哪个房间。应用需要确保每个房间只有一组人同时预定(即不得有相同房间的重叠预订)。在这种情况下,如果同时为同一个房间创建两个不同的预订,则可能会发生冲突。即使应用程序在允许用户进行预订之前检查可用性,如果两次预订是由两个不同的领导者进行的,则可能会有冲突。
其他类型的冲突可能更为微妙难以发现。例如,考虑一个会议室预订系统:它记录谁订了哪个时间段的哪个房间。应用程序需要确保每个房间在任意时刻都只能被一组人进行预定(即不得有相同房间的重叠预订)。在这种情况下,如果为同一个房间同时创建两个不同的预订,则可能会发生冲突。即使应用程序在允许用户进行预订之前检查会议室的可用性,如果两次预订是由两个不同的主库进行的,则仍然可能会有冲突。
现在还没有一个现成的答案,但在接下来的章节中,我们将更好地了解这个问题。我们将在 [第七章](ch7.md) 中看到更多的冲突示例,在 [第十二章](ch12.md) 中我们将讨论用于检测和解决复制系统中冲突的可伸缩方法。
虽然现在还没有一个现成的答案,但在接下来的章节中,我们将更好地了解这个问题。我们将在 [第七章](ch7.md) 中看到更多的冲突示例,在 [第十二章](ch12.md) 中我们将讨论用于检测和解决复制系统中冲突的可伸缩方法。
### 多主复制拓扑
**复制拓扑**replication topology描述写入从一个节点传播到另一个节点的通信路径。如果你有两个领导者,如 [图 5-7](img/fig5-7.png) 所示,只有一个合理的拓扑结构:领导者 1 必须把他所有的写到领导者 2反之亦然。当有两个以上的领导各种不同的拓扑是可能的。[图 5-8](img/fig5-8.png) 举例说明了一些例子。
**复制拓扑**replication topology用来描述写入操作从一个节点传播到另一个节点的通信路径。如果你有两个主库,如 [图 5-7](img/fig5-7.png) 所示,只有一个合理的拓扑结构:主库 1 必须把它所有的写入都发送到主库 2反之亦然。当有两个以上的主库多种不同的拓扑都是可能的。[图 5-8](img/fig5-8.png) 举例说明了一些例子。
![](img/fig5-8.png)
**图 5-8 三个可以设置多领导者复制的示例拓扑。**
**图 5-8 三种可以在多主复制中使用的拓扑示例。**
普遍的拓扑是全部到全部([图 5-8 (c)](img/fig5-8.png)其中每个领导者将其写入每个其他领导。但是也会使用更多受限制的拓扑例如默认情况下MySQL 仅支持 **环形拓扑circular topology**【34】其中每个节点接收来自一个节点的写入并将这些写入加上自己的任何写入转发给另一个节点。另一种流行的拓扑结构具有星形的形状 [^v]。一个指定的根节点将写入转发给所有其他节点。星形拓扑可以推广到树。
常见的拓扑是全部到全部all-to-all如 [图 5-8 (c)](img/fig5-8.png)),其中每个主库都将其写入发送给其他所有的主库。然而,一些更受限的拓扑也会被使用到:例如,默认情况下 MySQL 仅支持 **环形拓扑circular topology**【34】其中每个节点都从一个节点接收写入并将这些写入加上自己的写入转发给另一个节点。另一种流行的拓扑结构具有星形的形状 [^v]一个指定的根节点将写入转发给所有其他节点。星形拓扑可以推广到树。
[^v]: 不要与星型模式混淆(请参阅 “[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”),其中描述了数据模型的结构,而不是节点之间的通信拓扑。
在环形和星形拓扑中写入可能需要在到达所有副本之前通过多个节点。因此节点需要转发从其他节点收到的数据更改。为了防止无限复制循环每个节点被赋予一个唯一的标识符并且在复制日志中每次写入都会使用其经过的所有节点的标识符进行标记【43】。当一个节点收到用自己的标识符标记的数据更改时该数据更改将被忽略因为节点知道它已经被处理过。
环形和星形拓扑的问题是,如果只有一个节点发生故障,则可能会中断其他节点之间的复制消息流,导致它们无法通信,直到节点修复。拓扑结构可以重新配置为在发生故障的节点上工作,但在大多数部署中,这种重新配置必须手动完成。更密集连接的拓扑结构(例如全部到全部)的容错性更好,因为它允许消息沿着不同的路径传播,避免单点故障。
环形和星形拓扑的问题是,如果只有一个节点发生故障,则可能会中断其他节点之间的复制消息流,导致它们无法通信,除非节点被修复。拓扑结构可以重新配置为跳过发生故障的节点,但在大多数部署中,这种重新配置必须手动完成。更密集连接的拓扑结构(例如全部到全部)的容错性更好,因为它允许消息沿着不同的路径传播,可以避免单点故障。
另一方面,全部到全部的拓扑也可能有问题。特别是,一些网络链接可能比其他网络链接更快(例如由于网络拥塞),结果是一些复制消息可能 “超” 其他复制消息,如 [图 5-9](img/fig5-9.png) 所示。
另一方面,全部到全部的拓扑也可能有问题。特别是,一些网络链接可能比其他网络链接更快(例如由于网络拥塞),结果是一些复制消息可能 “超” 其他复制消息,如 [图 5-9](img/fig5-9.png) 所示。
![](img/fig5-9.png)
**图 5-9 使用多主程序复制时,可能会在某些副本中写入错误的顺序。**
**图 5-9 使用多主复制时,写入可能会以错误的顺序到达某些副本。**
在 [图 5-9](img/fig5-9.png) 中,客户端 A 向主库 1 的表中插入一行,客户端 B 在主库 3 上更新该行。然而,主库 2 可以以不同的顺序接收写入:它可以首先接收更新(从它的角度来看,是对数据库中不存在的行的更新),并且仅在稍后接收到相应的插入(其应该在更新之前)。
在 [图 5-9](img/fig5-9.png) 中,客户端 A 向主库 1 的表中插入一行,客户端 B 在主库 3 上更新该行。然而,主库 2 可以以不同的顺序接收写入:它可能先接收到更新(从它的角度来看,是对数据库中不存在的行的更新),稍后接收到相应的插入(其应该在更新之前)。
这是一个因果关系的问题,类似于我们在 “[一致前缀读](#一致前缀读)” 中看到的:更新取决于先前的插入,所以我们需要确保所有节点先处理插入,然后再处理更新。仅仅在每一次写入时添加一个时间戳是不够的,因为时钟不可能被充分地同步,以便在主库 2 处正确地排序这些事件(见 [第八章](ch8.md))。
这是一个因果关系的问题,类似于我们在 “[一致前缀读](#一致前缀读)” 中看到的:更新取决于先前的插入,所以我们需要确保所有节点先处理插入,然后再处理更新。仅仅在每一次写入时添加一个时间戳是不够的,因为时钟不可能被充分地同步,所以主库 2 就无法正确地对这些事件进行排序(见 [第八章](ch8.md))。
要正确排序这些事件,可以使用一种称为 **版本向量version vectors** 的技术,本章稍后将讨论这种技术(请参阅 “[检测并发写入](#检测并发写入)”)。然而,冲突检测技术在许多多领导者复制系统中执行得不好。例如在撰写本文时PostgreSQL BDR 不提供写入的因果排序【27】而 Tungsten Replicator for MySQL 甚至不尝试检测冲突【34】。
要正确排序这些事件,可以使用一种称为 **版本向量version vectors** 的技术,本章稍后将讨论这种技术(请参阅 “[检测并发写入](#检测并发写入)”)。然而,许多多主复制系统中的冲突检测技术实现得并不好。例如在撰写本文时PostgreSQL BDR 不提供写入的因果排序【27】而 Tungsten Replicator for MySQL 甚至都不做检测冲突【34】。
如果你正在使用具有多领导者复制功能的系统,那么应该了解这些问题,仔细阅读文档,并彻底测试你的数据库,以确保它确实提供了你认为具有的保证。
如果你正在使用基于多主复制的系统,那么你应该多了解这些问题,仔细阅读文档,并彻底测试你的数据库,以确保它确实提供了你想要的保证。
## 无主复制