diff --git a/ch4.md b/ch4.md
index 1e6ce1c..b10359b 100644
--- a/ch4.md
+++ b/ch4.md
@@ -1,28 +1,28 @@
-# 第四章:编码与演化 
+# 第四章:编码与演化
 
 ![](img/ch4.png)
 
 > 唯变所适
 >
-> —— 以弗所的赫拉克利特,为柏拉图所引(公元前360年)
+> —— 以弗所的赫拉克利特,为柏拉图所引(公元前 360 年)
 >
 
 -------------------
 
 [TOC]
 
-应用程序不可避免地随时间而变化。新产品的推出,对需求的深入理解,或者商业环境的变化,总会伴随着**功能(feature)** 的增增改改。[第一章](ch1.md)介绍了**可演化性(evolvability)** 的概念:应该尽力构建能灵活适应变化的系统(请参阅“[可演化性:拥抱变化](ch1.md#可演化性:拥抱变化)”)。
+应用程序不可避免地随时间而变化。新产品的推出,对需求的深入理解,或者商业环境的变化,总会伴随着 **功能(feature)** 的增增改改。[第一章](ch1.md) 介绍了 **可演化性(evolvability)** 的概念:应该尽力构建能灵活适应变化的系统(请参阅 “[可演化性:拥抱变化](ch1.md#可演化性:拥抱变化)”)。
 
 在大多数情况下,修改应用程序的功能也意味着需要更改其存储的数据:可能需要使用新的字段或记录类型,或者以新方式展示现有数据。
 
-我们在[第二章](ch2.md)讨论的数据模型有不同的方法来应对这种变化。关系数据库通常假定数据库中的所有数据都遵循一个模式:尽管可以更改该模式(通过模式迁移,即`ALTER`语句),但是在任何时间点都有且仅有一个正确的模式。相比之下,**读时模式**(schema-on-read,或**无模式**,即schemaless)数据库不会强制一个模式,因此数据库可以包含在不同时间写入的新老数据格式的混合(请参阅 “[文档模型中的模式灵活性](ch2.md#文档模型中的模式灵活性)” )。
+我们在 [第二章](ch2.md) 讨论的数据模型有不同的方法来应对这种变化。关系数据库通常假定数据库中的所有数据都遵循一个模式:尽管可以更改该模式(通过模式迁移,即 `ALTER` 语句),但是在任何时间点都有且仅有一个正确的模式。相比之下,**读时模式**(schema-on-read,或 **无模式**,即 schemaless)数据库不会强制一个模式,因此数据库可以包含在不同时间写入的新老数据格式的混合(请参阅 “[文档模型中的模式灵活性](ch2.md#文档模型中的模式灵活性)” )。
 
-当数据**格式(format)** 或**模式(schema)** 发生变化时,通常需要对应用程序代码进行相应的更改(例如,为记录添加新字段,然后修改程序开始读写该字段)。但在大型应用程序中,代码变更通常不会立即完成:
+当数据 **格式(format)** 或 **模式(schema)** 发生变化时,通常需要对应用程序代码进行相应的更改(例如,为记录添加新字段,然后修改程序开始读写该字段)。但在大型应用程序中,代码变更通常不会立即完成:
 
 * 对于 **服务端(server-side)** 应用程序,可能需要执行 **滚动升级 (rolling upgrade)** (也称为 **阶段发布(staged rollout)** ),一次将新版本部署到少数几个节点,检查新版本是否运行正常,然后逐渐部完所有的节点。这样无需中断服务即可部署新版本,为频繁发布提供了可行性,从而带来更好的可演化性。
 * 对于 **客户端(client-side)** 应用程序,升不升级就要看用户的心情了。用户可能相当长一段时间里都不会去升级软件。
 
-这意味着,新旧版本的代码,以及新旧数据格式可能会在系统中同时共处。系统想要继续顺利运行,就需要保持**双向兼容性**:
+这意味着,新旧版本的代码,以及新旧数据格式可能会在系统中同时共处。系统想要继续顺利运行,就需要保持 **双向兼容性**:
 
 * 向后兼容 (backward compatibility)
 
@@ -36,63 +36,63 @@
 
 向前兼容性可能会更棘手,因为旧版的程序需要忽略新版数据格式中新增的部分。
 
-本章中将介绍几种编码数据的格式,包括 JSON,XML,Protocol Buffers,Thrift和Avro。尤其将关注这些格式如何应对模式变化,以及它们如何对新旧代码数据需要共存的系统提供支持。然后将讨论如何使用这些格式进行数据存储和通信:在Web服务中,**表述性状态传递(REST)** 和**远程过程调用(RPC)**,以及**消息传递系统**(如Actor和消息队列)。
+本章中将介绍几种编码数据的格式,包括 JSON、XML、Protocol Buffers、Thrift 和 Avro。尤其将关注这些格式如何应对模式变化,以及它们如何对新旧代码数据需要共存的系统提供支持。然后将讨论如何使用这些格式进行数据存储和通信:在 Web 服务中,**表述性状态传递(REST)** 和 **远程过程调用(RPC)**,以及 **消息传递系统**(如 Actor 和消息队列)。
 
 ## 编码数据的格式
 
 程序通常(至少)使用两种形式的数据:
 
-1. 在内存中,数据保存在对象,结构体,列表,数组,散列表,树等中。 这些数据结构针对CPU的高效访问和操作进行了优化(通常使用指针)。
-2. 如果要将数据写入文件,或通过网络发送,则必须将其 **编码(encode)** 为某种自包含的字节序列(例如,JSON文档)。 由于每个进程都有自己独立的地址空间,一个进程中的指针对任何其他进程都没有意义,所以这个字节序列表示会与通常在内存中使用的数据结构完全不同[^i]。
+1. 在内存中,数据保存在对象、结构体、列表、数组、散列表、树等中。 这些数据结构针对 CPU 的高效访问和操作进行了优化(通常使用指针)。
+2. 如果要将数据写入文件,或通过网络发送,则必须将其 **编码(encode)** 为某种自包含的字节序列(例如,JSON 文档)。 由于每个进程都有自己独立的地址空间,一个进程中的指针对任何其他进程都没有意义,所以这个字节序列表示会与通常在内存中使用的数据结构完全不同 [^i]。
 
-[^i]: 除一些特殊情况外,例如某些内存映射文件或直接在压缩数据上操作(如“[列压缩](ch4.md#列压缩)”中所述)。
+[^i]: 除一些特殊情况外,例如某些内存映射文件或直接在压缩数据上操作(如 “[列压缩](ch4.md#列压缩)” 中所述)。
 
-所以,需要在两种表示之间进行某种类型的翻译。 从内存中表示到字节序列的转换称为 **编码(Encoding)** (也称为**序列化(serialization)** 或**编组(marshalling)**),反过来称为**解码(Decoding)**[^ii](**解析(Parsing)**,**反序列化(deserialization)**,**反编组( unmarshalling)**)[^译i]。
+所以,需要在两种表示之间进行某种类型的翻译。 从内存中表示到字节序列的转换称为 **编码(Encoding)** (也称为 **序列化(serialization)** 或 **编组(marshalling)**),反过来称为 **解码(Decoding)**[^ii](**解析(Parsing)**,**反序列化(deserialization)**,**反编组 (unmarshalling)**)[^译i]。
 
 [^ii]: 请注意,**编码(encode)**  与 **加密(encryption)** 无关。 本书不讨论加密。
-[^译i]: Marshal与Serialization的区别:Marshal不仅传输对象的状态,而且会一起传输对象的方法(相关代码)。
+[^译i]: Marshal 与 Serialization 的区别:Marshal 不仅传输对象的状态,而且会一起传输对象的方法(相关代码)。
 
 > #### 术语冲突
-> 不幸的是,在[第七章](ch7.md): **事务(Transaction)** 的上下文里,**序列化(Serialization)** 这个术语也出现了,而且具有完全不同的含义。尽管序列化可能是更常见的术语,为了避免术语重载,本书中坚持使用 **编码(Encoding)** 表达此含义。
+> 不幸的是,在 [第七章](ch7.md): **事务(Transaction)** 的上下文里,**序列化(Serialization)** 这个术语也出现了,而且具有完全不同的含义。尽管序列化可能是更常见的术语,为了避免术语重载,本书中坚持使用 **编码(Encoding)** 表达此含义。
 
 这是一个常见的问题,因而有许多库和编码格式可供选择。 首先让我们概览一下。
 
 ### 语言特定的格式
 
-许多编程语言都内建了将内存对象编码为字节序列的支持。例如,Java有`java.io.Serializable` 【1】,Ruby有`Marshal`【2】,Python有`pickle`【3】等等。许多第三方库也存在,例如`Kryo for Java` 【4】。
+许多编程语言都内建了将内存对象编码为字节序列的支持。例如,Java 有 `java.io.Serializable` 【1】,Ruby 有 `Marshal`【2】,Python 有 `pickle`【3】等等。许多第三方库也存在,例如 `Kryo for Java` 【4】。
 
 这些编码库非常方便,可以用很少的额外代码实现内存对象的保存与恢复。但是它们也有一些深层次的问题:
 
 * 这类编码通常与特定的编程语言深度绑定,其他语言很难读取这种数据。如果以这类编码存储或传输数据,那你就和这门语言绑死在一起了。并且很难将系统与其他组织的系统(可能用的是不同的语言)进行集成。
-* 为了恢复相同对象类型的数据,解码过程需要**实例化任意类**的能力,这通常是安全问题的一个来源【5】:如果攻击者可以让应用程序解码任意的字节序列,他们就能实例化任意的类,这会允许他们做可怕的事情,如远程执行任意代码【6,7】。
+* 为了恢复相同对象类型的数据,解码过程需要 **实例化任意类** 的能力,这通常是安全问题的一个来源【5】:如果攻击者可以让应用程序解码任意的字节序列,他们就能实例化任意的类,这会允许他们做可怕的事情,如远程执行任意代码【6,7】。
 * 在这些库中,数据版本控制通常是事后才考虑的。因为它们旨在快速简便地对数据进行编码,所以往往忽略了前向后向兼容性带来的麻烦问题。
-* 效率(编码或解码所花费的CPU时间,以及编码结构的大小)往往也是事后才考虑的。 例如,Java的内置序列化由于其糟糕的性能和臃肿的编码而臭名昭着【8】。
+* 效率(编码或解码所花费的 CPU 时间,以及编码结构的大小)往往也是事后才考虑的。 例如,Java 的内置序列化由于其糟糕的性能和臃肿的编码而臭名昭着【8】。
 
 因此,除非临时使用,采用语言内置编码通常是一个坏主意。
 
 ### JSON、XML和二进制变体
 
-当我们谈到可以被多种编程语言读写的标准编码时,JSON和XML是最显眼的角逐者。它们广为人知,广受支持,也“广受憎恶”。 XML经常收到批评:过于冗长与且过份复杂【9】。 JSON的流行则主要源于(通过成为JavaScript的一个子集)Web浏览器的内置支持,以及相对于XML的简单性。 CSV是另一种流行的与语言无关的格式,尽管其功能相对较弱。
+当我们谈到可以被多种编程语言读写的标准编码时,JSON 和 XML 是最显眼的角逐者。它们广为人知,广受支持,也 “广受憎恶”。 XML 经常收到批评:过于冗长与且过份复杂【9】。 JSON 的流行则主要源于(通过成为 JavaScript 的一个子集)Web 浏览器的内置支持,以及相对于 XML 的简单性。 CSV 是另一种流行的与语言无关的格式,尽管其功能相对较弱。
 
-JSON,XML和CSV属于文本格式,因此具有人类可读性(尽管它们的语法是一个热门争议话题)。除了表面的语法问题之外,它们也存在一些微妙的问题:
+JSON,XML 和 CSV 属于文本格式,因此具有人类可读性(尽管它们的语法是一个热门争议话题)。除了表面的语法问题之外,它们也存在一些微妙的问题:
 
-* **数值(numbers)** 的编码多有歧义之处。XML和CSV不能区分数字和字符串(除非引用一个外部模式)。 JSON虽然区分字符串与数值,但不区分整数和浮点数,而且不能指定精度。
-* 当处理更大的数值时,这个问题显得尤为严重。例如大于$2^{53}$的整数无法使用IEEE 754双精度浮点数精确表示,因此在使用浮点数(例如JavaScript)的语言进行分析时,这些数字会变得不准确。 Twitter有一个关于大于$2^{53}$的数字的例子,它使用64位整数来标识每条推文。 Twitter API返回的JSON包含了两种推特ID,一种是JSON数值,另一种是十进制字符串,以避免JavaScript程序无法正确解析数字的问题【10】。
-* JSON和XML对Unicode字符串(即人类可读的文本)有很好的支持,但是它们不支持二进制数据(即不带 **字符编码(character encoding)** 的字节序列)。二进制串是很有用的功能,人们通过使用Base64将二进制数据编码为文本来绕过此限制。其特有的模式标识着这个值应当被解释为Base64编码的二进制数据。这种方案虽然管用,但比较Hacky,并且会增加三分之一的数据大小。
-*  XML 【11】和 JSON 【12】都有可选的模式支持。这些模式语言相当强大,所以学习和实现起来都相当复杂。 XML模式的使用相当普遍,但许多基于JSON的工具才不会去折腾模式。对数据的正确解读(例如区分数值与二进制串)取决于模式中的信息,因此不使用XML/JSON模式的应用程序可能需要对相应的编码/解码逻辑进行硬编码。
-* CSV没有任何模式,因此每行和每列的含义完全由应用程序自行定义。如果应用程序变更添加了新的行或列,那么这种变更必须通过手工处理。 CSV也是一个相当模糊的格式(如果一个值包含逗号或换行符,会发生什么?)。尽管其转义规则已经被正式指定【13】,但并不是所有的解析器都正确的实现了标准。
+* **数值(numbers)** 的编码多有歧义之处。XML 和 CSV 不能区分数字和字符串(除非引用一个外部模式)。 JSON 虽然区分字符串与数值,但不区分整数和浮点数,而且不能指定精度。
+* 当处理更大的数值时,这个问题显得尤为严重。例如大于 $2^{53}$ 的整数无法使用 IEEE 754 双精度浮点数精确表示,因此在使用浮点数(例如 JavaScript)的语言进行分析时,这些数字会变得不准确。 Twitter 有一个关于大于 $2^{53}$ 的数字的例子,它使用 64 位整数来标识每条推文。 Twitter API 返回的 JSON 包含了两种推特 ID,一种是 JSON 数值,另一种是十进制字符串,以避免 JavaScript 程序无法正确解析数字的问题【10】。
+* JSON 和 XML 对 Unicode 字符串(即人类可读的文本)有很好的支持,但是它们不支持二进制数据(即不带 **字符编码 (character encoding)** 的字节序列)。二进制串是很有用的功能,人们通过使用 Base64 将二进制数据编码为文本来绕过此限制。其特有的模式标识着这个值应当被解释为 Base64 编码的二进制数据。这种方案虽然管用,但比较 Hacky,并且会增加三分之一的数据大小。
+*  XML 【11】和 JSON 【12】都有可选的模式支持。这些模式语言相当强大,所以学习和实现起来都相当复杂。 XML 模式的使用相当普遍,但许多基于 JSON 的工具才不会去折腾模式。对数据的正确解读(例如区分数值与二进制串)取决于模式中的信息,因此不使用 XML/JSON 模式的应用程序可能需要对相应的编码 / 解码逻辑进行硬编码。
+* CSV 没有任何模式,因此每行和每列的含义完全由应用程序自行定义。如果应用程序变更添加了新的行或列,那么这种变更必须通过手工处理。 CSV 也是一个相当模糊的格式(如果一个值包含逗号或换行符,会发生什么?)。尽管其转义规则已经被正式指定【13】,但并不是所有的解析器都正确的实现了标准。
 
-尽管存在这些缺陷,但JSON,XML和CSV对很多需求来说已经足够好了。它们很可能会继续流行下去,特别是作为数据交换格式来说(即将数据从一个组织发送到另一个组织)。在这种情况下,只要人们对格式是什么意见一致,格式有多美观或者效率有多高效就无所谓了。让不同的组织就这些东西达成一致的难度超过了绝大多数问题。
+尽管存在这些缺陷,但 JSON、XML 和 CSV 对很多需求来说已经足够好了。它们很可能会继续流行下去,特别是作为数据交换格式来说(即将数据从一个组织发送到另一个组织)。在这种情况下,只要人们对格式是什么意见一致,格式有多美观或者效率有多高效就无所谓了。让不同的组织就这些东西达成一致的难度超过了绝大多数问题。
 
 #### 二进制编码
 
-对于仅在组织内部使用的数据,使用最小公约数式的编码格式压力较小。例如,可以选择更紧凑或更快的解析格式。虽然对小数据集来说,收益可以忽略不计;但一旦达到TB级别,数据格式的选型就会产生巨大的影响。
+对于仅在组织内部使用的数据,使用最小公约数式的编码格式压力较小。例如,可以选择更紧凑或更快的解析格式。虽然对小数据集来说,收益可以忽略不计;但一旦达到 TB 级别,数据格式的选型就会产生巨大的影响。
 
-JSON比XML简洁,但与二进制格式相比还是太占空间。这一事实导致大量二进制编码版本JSON(MessagePack,BSON,BJSON,UBJSON,BISON和Smile等) 和 XML(例如WBXML和Fast Infoset)的出现。这些格式已经在各种各样的领域中采用,但是没有一个能像文本版JSON和XML那样被广泛采用。
+JSON 比 XML 简洁,但与二进制格式相比还是太占空间。这一事实导致大量二进制编码版本 JSON(MessagePack、BSON、BJSON、UBJSON、BISON 和 Smile 等) 和 XML(例如 WBXML 和 Fast Infoset)的出现。这些格式已经在各种各样的领域中采用,但是没有一个能像文本版 JSON 和 XML 那样被广泛采用。
 
-这些格式中的一些扩展了一组数据类型(例如,区分整数和浮点数,或者增加对二进制字符串的支持),另一方面,它们没有改变JSON / XML的数据模型。特别是由于它们没有规定模式,所以它们需要在编码数据中包含所有的对象字段名称。也就是说,在[例4-1]()中的JSON文档的二进制编码中,需要在某处包含字符串`userName`,`favoriteNumber`和`interests`。
+这些格式中的一些扩展了一组数据类型(例如,区分整数和浮点数,或者增加对二进制字符串的支持),另一方面,它们没有改变 JSON / XML 的数据模型。特别是由于它们没有规定模式,所以它们需要在编码数据中包含所有的对象字段名称。也就是说,在 [例 4-1]() 中的 JSON 文档的二进制编码中,需要在某处包含字符串 `userName`,`favoriteNumber` 和 `interests`。
 
-**例4-1 本章中用于展示二进制编码的示例记录**
+**例 4-1 本章中用于展示二进制编码的示例记录**
 
 ```json
 {
@@ -102,26 +102,26 @@ JSON比XML简洁,但与二进制格式相比还是太占空间。这一事实
 }
 ```
 
-我们来看一个MessagePack的例子,它是一个JSON的二进制编码。图4-1显示了如果使用MessagePack 【14】对[例4-1]()中的JSON文档进行编码,则得到的字节序列。前几个字节如下:
+我们来看一个 MessagePack 的例子,它是一个 JSON 的二进制编码。图 4-1 显示了如果使用 MessagePack 【14】对 [例 4-1]() 中的 JSON 文档进行编码,则得到的字节序列。前几个字节如下:
 
-1. 第一个字节`0x83`表示接下来是**3**个字段(低四位= `0x03`)的**对象 object**(高四位= `0x80`)。 (如果想知道如果一个对象有15个以上的字段会发生什么情况,字段的数量塞不进4个bit里,那么它会用另一个不同的类型标识符,字段的数量被编码两个或四个字节)。
-2. 第二个字节`0xa8`表示接下来是**8**字节长(低四位=0x08)的字符串(高四位= 0x0a)。
-3. 接下来八个字节是ASCII字符串形式的字段名称`userName`。由于之前已经指明长度,不需要任何标记来标识字符串的结束位置(或者任何转义)。
-4. 接下来的七个字节对前缀为`0xa6`的六个字母的字符串值`Martin`进行编码,依此类推。
+1. 第一个字节 `0x83` 表示接下来是 **3** 个字段(低四位 = `0x03`)的 **对象 object**(高四位 = `0x80`)。 (如果想知道如果一个对象有 15 个以上的字段会发生什么情况,字段的数量塞不进 4 个 bit 里,那么它会用另一个不同的类型标识符,字段的数量被编码两个或四个字节)。
+2. 第二个字节 `0xa8` 表示接下来是 **8** 字节长(低四位 = `0x08`)的字符串(高四位 = `0x0a`)。
+3. 接下来八个字节是 ASCII 字符串形式的字段名称 `userName`。由于之前已经指明长度,不需要任何标记来标识字符串的结束位置(或者任何转义)。
+4. 接下来的七个字节对前缀为 `0xa6` 的六个字母的字符串值 `Martin` 进行编码,依此类推。
 
-二进制编码长度为66个字节,仅略小于文本JSON编码所取的81个字节(删除了空白)。所有的JSON的二进制编码在这方面是相似的。空间节省了一丁点(以及解析加速)是否能弥补可读性的损失,谁也说不准。
+二进制编码长度为 66 个字节,仅略小于文本 JSON 编码所取的 81 个字节(删除了空白)。所有的 JSON 的二进制编码在这方面是相似的。空间节省了一丁点(以及解析加速)是否能弥补可读性的损失,谁也说不准。
 
-在下面的章节中,能达到比这好得多的结果,只用32个字节对相同的记录进行编码。
+在下面的章节中,能达到比这好得多的结果,只用 32 个字节对相同的记录进行编码。
 
 
 ![](img/fig4-1.png)
 
-**图4-1 使用MessagePack编码的记录(例4-1)**
+**图 4-1 使用 MessagePack 编码的记录(例 4-1)**
 
 ### Thrift与Protocol Buffers
 
-Apache Thrift 【15】和Protocol Buffers(protobuf)【16】是基于相同原理的二进制编码库。 Protocol Buffers最初是在Google开发的,Thrift最初是在Facebook开发的,并且在2007~2008年都是开源的【17】。
-Thrift和Protocol Buffers都需要一个模式来编码任何数据。要在Thrift的[例4-1]()中对数据进行编码,可以使用Thrift **接口定义语言(IDL)** 来描述模式,如下所示:
+Apache Thrift 【15】和 Protocol Buffers(protobuf)【16】是基于相同原理的二进制编码库。 Protocol Buffers 最初是在 Google 开发的,Thrift 最初是在 Facebook 开发的,并且在 2007~2008 年都是开源的【17】。
+Thrift 和 Protocol Buffers 都需要一个模式来编码任何数据。要在 Thrift 的 [例 4-1]() 中对数据进行编码,可以使用 Thrift **接口定义语言(IDL)** 来描述模式,如下所示:
 
 ```c
 struct Person {
@@ -131,7 +131,7 @@ struct Person {
 }
 ```
 
-Protocol Buffers的等效模式定义看起来非常相似:
+Protocol Buffers 的等效模式定义看起来非常相似:
 
 ```protobuf
 message Person {
@@ -141,38 +141,38 @@ message Person {
 }
 ```
 
-Thrift和Protocol Buffers每一个都带有一个代码生成工具,它采用了类似于这里所示的模式定义,并且生成了以各种编程语言实现模式的类【18】。你的应用程序代码可以调用此生成的代码来对模式的记录进行编码或解码。
-用这个模式编码的数据是什么样的?令人困惑的是,Thrift有两种不同的二进制编码格式[^iii],分别称为BinaryProtocol和CompactProtocol。先来看看BinaryProtocol。使用这种格式的编码来编码[例4-1]()中的消息只需要59个字节,如[图4-2](img/fig4-2.png)所示【19】。
+Thrift 和 Protocol Buffers 每一个都带有一个代码生成工具,它采用了类似于这里所示的模式定义,并且生成了以各种编程语言实现模式的类【18】。你的应用程序代码可以调用此生成的代码来对模式的记录进行编码或解码。
+用这个模式编码的数据是什么样的?令人困惑的是,Thrift 有两种不同的二进制编码格式 [^iii],分别称为 BinaryProtocol 和 CompactProtocol。先来看看 BinaryProtocol。使用这种格式的编码来编码 [例 4-1]() 中的消息只需要 59 个字节,如 [图 4-2](img/fig4-2.png) 所示【19】。
 
 ![](img/fig4-2.png)
 
-**图4-2 使用Thrift二进制协议编码的记录**
+**图 4-2 使用 Thrift 二进制协议编码的记录**
 
-[^iii]: 实际上,Thrift有三种二进制协议:BinaryProtocol、CompactProtocol和DenseProtocol,尽管DenseProtocol只支持C ++实现,所以不算作跨语言【18】。 除此之外,它还有两种不同的基于JSON的编码格式【19】。 真逗!
+[^iii]: 实际上,Thrift 有三种二进制协议:BinaryProtocol、CompactProtocol 和 DenseProtocol,尽管 DenseProtocol 只支持 C ++ 实现,所以不算作跨语言【18】。 除此之外,它还有两种不同的基于 JSON 的编码格式【19】。 真逗!
 
-与[图4-1](Img/fig4-1.png)类似,每个字段都有一个类型注释(用于指示它是一个字符串,整数,列表等),还可以根据需要指定长度(字符串的长度,列表中的项目数) 。出现在数据中的字符串`(“Martin”, “daydreaming”, “hacking”)`也被编码为ASCII(或者说,UTF-8),与之前类似。
+与 [图 4-1](Img/fig4-1.png) 类似,每个字段都有一个类型注释(用于指示它是一个字符串,整数,列表等),还可以根据需要指定长度(字符串的长度,列表中的项目数) 。出现在数据中的字符串 `(“Martin”, “daydreaming”, “hacking”)` 也被编码为 ASCII(或者说,UTF-8),与之前类似。
 
-与[图4-1](img/fig4-1.png)相比,最大的区别是没有字段名`(userName, favoriteNumber, interests)`。相反,编码数据包含字段标签,它们是数字`(1, 2和3)`。这些是模式定义中出现的数字。字段标记就像字段的别名 - 它们是说我们正在谈论的字段的一种紧凑的方式,而不必拼出字段名称。
+与 [图 4-1](img/fig4-1.png) 相比,最大的区别是没有字段名 `(userName, favoriteNumber, interests)`。相反,编码数据包含字段标签,它们是数字 `(1, 2 和 3)`。这些是模式定义中出现的数字。字段标记就像字段的别名 - 它们是说我们正在谈论的字段的一种紧凑的方式,而不必拼出字段名称。
 
-Thrift CompactProtocol编码在语义上等同于BinaryProtocol,但是如[图4-3](img/fig4-3.png)所示,它只将相同的信息打包成只有34个字节。它通过将字段类型和标签号打包到单个字节中,并使用可变长度整数来实现。数字1337不是使用全部八个字节,而是用两个字节编码,每个字节的最高位用来指示是否还有更多的字节。这意味着-64到63之间的数字被编码为一个字节,-8192和8191之间的数字以两个字节编码,等等。较大的数字使用更多的字节。
+Thrift CompactProtocol 编码在语义上等同于 BinaryProtocol,但是如 [图 4-3](img/fig4-3.png) 所示,它只将相同的信息打包成只有 34 个字节。它通过将字段类型和标签号打包到单个字节中,并使用可变长度整数来实现。数字 1337 不是使用全部八个字节,而是用两个字节编码,每个字节的最高位用来指示是否还有更多的字节。这意味着 - 64 到 63 之间的数字被编码为一个字节,-8192 和 8191 之间的数字以两个字节编码,等等。较大的数字使用更多的字节。
 
 ![](img/fig4-3.png)
 
-**图4-3 使用Thrift压缩协议编码的记录**
+**图 4-3 使用 Thrift 压缩协议编码的记录**
 
-最后,Protocol Buffers(只有一种二进制编码格式)对相同的数据进行编码,如[图4-4](img/fig4-4.png)所示。 它的打包方式稍有不同,但与Thrift的CompactProtocol非常相似。 Protobuf将同样的记录塞进了33个字节中。
+最后,Protocol Buffers(只有一种二进制编码格式)对相同的数据进行编码,如 [图 4-4](img/fig4-4.png) 所示。 它的打包方式稍有不同,但与 Thrift 的 CompactProtocol 非常相似。 Protobuf 将同样的记录塞进了 33 个字节中。
 
 ![](img/fig4-4.png)
 
-**图4-4 使用Protobuf编码的记录**
+**图 4-4 使用 Protobuf 编码的记录**
 
 需要注意的一个细节:在前面所示的模式中,每个字段被标记为必需或可选,但是这对字段如何编码没有任何影响(二进制数据中没有任何字段指示某字段是否必须)。区别在于,如果字段设置为 `required`,但未设置该字段,则所需的运行时检查将失败,这对于捕获错误非常有用。
 
 #### 字段标签和模式演变
 
-我们之前说过,模式不可避免地需要随着时间而改变。我们称之为模式演变。 Thrift和Protocol Buffers如何处理模式更改,同时保持向后兼容性?
+我们之前说过,模式不可避免地需要随着时间而改变。我们称之为模式演变。 Thrift 和 Protocol Buffers 如何处理模式更改,同时保持向后兼容性?
 
-从示例中可以看出,编码的记录就是其编码字段的拼接。每个字段由其标签号码(样本模式中的数字1,2,3)标识,并用数据类型(例如字符串或整数)注释。如果没有设置字段值,则简单地从编码记录中省略。从中可以看到,字段标记对编码数据的含义至关重要。你可以更改架构中字段的名称,因为编码的数据永远不会引用字段名称,但不能更改字段的标记,因为这会使所有现有的编码数据无效。
+从示例中可以看出,编码的记录就是其编码字段的拼接。每个字段由其标签号码(样本模式中的数字 1,2,3)标识,并用数据类型(例如字符串或整数)注释。如果没有设置字段值,则简单地从编码记录中省略。从中可以看到,字段标记对编码数据的含义至关重要。你可以更改架构中字段的名称,因为编码的数据永远不会引用字段名称,但不能更改字段的标记,因为这会使所有现有的编码数据无效。
 
 你可以添加新的字段到架构,只要你给每个字段一个新的标签号码。如果旧的代码(不知道你添加的新的标签号码)试图读取新代码写入的数据,包括一个新的字段,其标签号码不能识别,它可以简单地忽略该字段。数据类型注释允许解析器确定需要跳过的字节数。这保持了向前兼容性:旧代码可以读取由新代码编写的记录。
 
@@ -182,19 +182,19 @@ Thrift CompactProtocol编码在语义上等同于BinaryProtocol,但是如[图4
 
 #### 数据类型和模式演变
 
-如何改变字段的数据类型?这也许是可能的——详细信息请查阅相关的文档——但是有一个风险,值将失去精度或被截断。例如,假设你将一个32位的整数变成一个64位的整数。新代码可以轻松读取旧代码写入的数据,因为解析器可以用零填充任何缺失的位。但是,如果旧代码读取由新代码写入的数据,则旧代码仍使用32位变量来保存该值。如果解码的64位值不适合32位,则它将被截断。
+如何改变字段的数据类型?这也许是可能的 —— 详细信息请查阅相关的文档 —— 但是有一个风险,值将失去精度或被截断。例如,假设你将一个 32 位的整数变成一个 64 位的整数。新代码可以轻松读取旧代码写入的数据,因为解析器可以用零填充任何缺失的位。但是,如果旧代码读取由新代码写入的数据,则旧代码仍使用 32 位变量来保存该值。如果解码的 64 位值不适合 32 位,则它将被截断。
 
-Protobuf的一个奇怪的细节是,它没有列表或数组数据类型,而是有一个字段的重复标记(`repeated`,这是除必需和可选之外的第三个选项)。如[图4-4](img/fig4-4.png)所示,重复字段的编码正如它所说的那样:同一个字段标记只是简单地出现在记录中。这具有很好的效果,可以将可选(单值)字段更改为重复(多值)字段。读取旧数据的新代码会看到一个包含零个或一个元素的列表(取决于该字段是否存在)。读取新数据的旧代码只能看到列表的最后一个元素。
+Protobuf 的一个奇怪的细节是,它没有列表或数组数据类型,而是有一个字段的重复标记(`repeated`,这是除必需和可选之外的第三个选项)。如 [图 4-4](img/fig4-4.png) 所示,重复字段的编码正如它所说的那样:同一个字段标记只是简单地出现在记录中。这具有很好的效果,可以将可选(单值)字段更改为重复(多值)字段。读取旧数据的新代码会看到一个包含零个或一个元素的列表(取决于该字段是否存在)。读取新数据的旧代码只能看到列表的最后一个元素。
 
-Thrift有一个专用的列表数据类型,它使用列表元素的数据类型进行参数化。这不允许Protocol Buffers所做的从单值到多值的演变,但是它具有支持嵌套列表的优点。
+Thrift 有一个专用的列表数据类型,它使用列表元素的数据类型进行参数化。这不允许 Protocol Buffers 所做的从单值到多值的演变,但是它具有支持嵌套列表的优点。
 
 ### Avro
 
-Apache Avro 【20】是另一种二进制编码格式,与Protocol Buffers和Thrift有着有趣的不同。 它是作为Hadoop的一个子项目在2009年开始的,因为Thrift不适合Hadoop的用例【21】。
+Apache Avro 【20】是另一种二进制编码格式,与 Protocol Buffers 和 Thrift 有着有趣的不同。 它是作为 Hadoop 的一个子项目在 2009 年开始的,因为 Thrift 不适合 Hadoop 的用例【21】。
 
-Avro也使用模式来指定正在编码的数据的结构。 它有两种模式语言:一种(Avro IDL)用于人工编辑,一种(基于JSON)更易于机器读取。
+Avro 也使用模式来指定正在编码的数据的结构。 它有两种模式语言:一种(Avro IDL)用于人工编辑,一种(基于 JSON)更易于机器读取。
 
-我们用Avro IDL编写的示例模式可能如下所示:
+我们用 Avro IDL 编写的示例模式可能如下所示:
 
 ```c
 record Person {
@@ -204,7 +204,7 @@ record Person {
 }
 ```
 
-等价的JSON表示:
+等价的 JSON 表示:
 
 ```json
 {
@@ -218,102 +218,102 @@ record Person {
 }
 ```
 
-首先,请注意模式中没有标签号码。 如果我们使用这个模式编码我们的例子记录([例4-1]()),Avro二进制编码只有32个字节长,这是我们所见过的所有编码中最紧凑的。 编码字节序列的分解如[图4-5](img/fig4-5.png)所示。
+首先,请注意模式中没有标签号码。 如果我们使用这个模式编码我们的例子记录([例 4-1]()),Avro 二进制编码只有 32 个字节长,这是我们所见过的所有编码中最紧凑的。 编码字节序列的分解如 [图 4-5](img/fig4-5.png) 所示。
 
-如果你检查字节序列,你可以看到没有什么可以识别字段或其数据类型。 编码只是由连在一起的值组成。 一个字符串只是一个长度前缀,后跟UTF-8字节,但是在被包含的数据中没有任何内容告诉你它是一个字符串。 它可以是一个整数,也可以是其他的整数。 整数使用可变长度编码(与Thrift的CompactProtocol相同)进行编码。
+如果你检查字节序列,你可以看到没有什么可以识别字段或其数据类型。 编码只是由连在一起的值组成。 一个字符串只是一个长度前缀,后跟 UTF-8 字节,但是在被包含的数据中没有任何内容告诉你它是一个字符串。 它可以是一个整数,也可以是其他的整数。 整数使用可变长度编码(与 Thrift 的 CompactProtocol 相同)进行编码。
 
 ![](img/fig4-5.png)
 
-**图4-5 使用Avro编码的记录**
+**图 4-5 使用 Avro 编码的记录**
 
-为了解析二进制数据,你按照它们出现在模式中的顺序遍历这些字段,并使用模式来告诉你每个字段的数据类型。这意味着如果读取数据的代码使用与写入数据的代码完全相同的模式,才能正确解码二进制数据。Reader和Writer之间的模式不匹配意味着错误地解码数据。
+为了解析二进制数据,你按照它们出现在模式中的顺序遍历这些字段,并使用模式来告诉你每个字段的数据类型。这意味着如果读取数据的代码使用与写入数据的代码完全相同的模式,才能正确解码二进制数据。Reader 和 Writer 之间的模式不匹配意味着错误地解码数据。
 
-那么,Avro如何支持模式演变呢?
+那么,Avro 如何支持模式演变呢?
 
 #### Writer模式与Reader模式
 
-有了Avro,当应用程序想要编码一些数据(将其写入文件或数据库,通过网络发送等)时,它使用它知道的任何版本的模式编码数据,例如,模式可能被编译到应用程序中。这被称为Writer模式。
+有了 Avro,当应用程序想要编码一些数据(将其写入文件或数据库,通过网络发送等)时,它使用它知道的任何版本的模式编码数据,例如,模式可能被编译到应用程序中。这被称为 Writer 模式。
 
-当一个应用程序想要解码一些数据(从一个文件或数据库读取数据,从网络接收数据等)时,它希望数据在某个模式中,这就是Reader模式。这是应用程序代码所依赖的模式,在应用程序的构建过程中,代码可能已经从该模式生成。
+当一个应用程序想要解码一些数据(从一个文件或数据库读取数据,从网络接收数据等)时,它希望数据在某个模式中,这就是 Reader 模式。这是应用程序代码所依赖的模式,在应用程序的构建过程中,代码可能已经从该模式生成。
 
-Avro的关键思想是Writer模式和Reader模式不必是相同的 - 他们只需要兼容。当数据解码(读取)时,Avro库通过并排查看Writer模式和Reader模式并将数据从Writer模式转换到Reader模式来解决差异。 Avro规范【20】确切地定义了这种解析的工作原理,如[图4-6](img/fig4-6.png)所示。
+Avro 的关键思想是 Writer 模式和 Reader 模式不必是相同的 - 他们只需要兼容。当数据解码(读取)时,Avro 库通过并排查看 Writer 模式和 Reader 模式并将数据从 Writer 模式转换到 Reader 模式来解决差异。 Avro 规范【20】确切地定义了这种解析的工作原理,如 [图 4-6](img/fig4-6.png) 所示。
 
-例如,如果Writer模式和Reader模式的字段顺序不同,这是没有问题的,因为模式解析通过字段名匹配字段。如果读取数据的代码遇到出现在Writer模式中但不在Reader模式中的字段,则忽略它。如果读取数据的代码需要某个字段,但是Writer模式不包含该名称的字段,则使用在Reader模式中声明的默认值填充。
+例如,如果 Writer 模式和 Reader 模式的字段顺序不同,这是没有问题的,因为模式解析通过字段名匹配字段。如果读取数据的代码遇到出现在 Writer 模式中但不在 Reader 模式中的字段,则忽略它。如果读取数据的代码需要某个字段,但是 Writer 模式不包含该名称的字段,则使用在 Reader 模式中声明的默认值填充。
 
 ![](img/fig4-6.png)
 
-**图4-6 一个Avro Reader解决读写模式的差异**
+**图 4-6 一个 Avro Reader 解决读写模式的差异**
 
 #### 模式演变规则
 
-使用Avro,向前兼容性意味着你可以将新版本的模式作为Writer,并将旧版本的模式作为Reader。相反,向后兼容意味着你可以有一个作为Reader的新版本模式和作为Writer的旧版本模式。
+使用 Avro,向前兼容性意味着你可以将新版本的模式作为 Writer,并将旧版本的模式作为 Reader。相反,向后兼容意味着你可以有一个作为 Reader 的新版本模式和作为 Writer 的旧版本模式。
 
-为了保持兼容性,你只能添加或删除具有默认值的字段(我们的Avro模式中的字段`favoriteNumber`的默认值为`null`)。例如,假设你添加了一个有默认值的字段,这个新的字段将存在于新模式而不是旧模式中。当使用新模式的Reader读取使用旧模式写入的记录时,将为缺少的字段填充默认值。
+为了保持兼容性,你只能添加或删除具有默认值的字段(我们的 Avro 模式中的字段 `favoriteNumber` 的默认值为 `null`)。例如,假设你添加了一个有默认值的字段,这个新的字段将存在于新模式而不是旧模式中。当使用新模式的 Reader 读取使用旧模式写入的记录时,将为缺少的字段填充默认值。
 
-如果你要添加一个没有默认值的字段,新的Reader将无法读取旧Writer写的数据,所以你会破坏向后兼容性。如果你要删除没有默认值的字段,旧的Reader将无法读取新Writer写入的数据,因此你会打破向前兼容性。在一些编程语言中,null是任何变量可以接受的默认值,但在Avro中并不是这样:如果要允许一个字段为`null`,则必须使用联合类型。例如,`union {null, long, string} field;`表示field可以是数字或字符串,也可以是`null`。如果要将null作为默认值,则它必须是union的分支之一[^iv]。这样的写法比默认情况下就允许任何变量是`null`显得更加冗长,但是通过明确什么可以和什么不可以是`null`,有助于防止出错【22】。
+如果你要添加一个没有默认值的字段,新的 Reader 将无法读取旧 Writer 写的数据,所以你会破坏向后兼容性。如果你要删除没有默认值的字段,旧的 Reader 将无法读取新 Writer 写入的数据,因此你会打破向前兼容性。在一些编程语言中,null 是任何变量可以接受的默认值,但在 Avro 中并不是这样:如果要允许一个字段为 `null`,则必须使用联合类型。例如,`union {null, long, string} field;` 表示 field 可以是数字或字符串,也可以是 `null`。如果要将 null 作为默认值,则它必须是 union 的分支之一 [^iv]。这样的写法比默认情况下就允许任何变量是 `null` 显得更加冗长,但是通过明确什么可以和什么不可以是 `null`,有助于防止出错【22】。
 
-[^iv]: 确切地说,默认值必须是联合的第一个分支的类型,尽管这是Avro的特定限制,而不是联合类型的一般特征。
+[^iv]: 确切地说,默认值必须是联合的第一个分支的类型,尽管这是 Avro 的特定限制,而不是联合类型的一般特征。
 
-因此,Avro没有像Protocol Buffers和Thrift那样的`optional`和`required`标记(但它有联合类型和默认值)。
+因此,Avro 没有像 Protocol Buffers 和 Thrift 那样的 `optional` 和 `required` 标记(但它有联合类型和默认值)。
 
-只要Avro可以支持相应的类型转换,就可以改变字段的数据类型。更改字段的名称也是可能的,但有点棘手:Reader模式可以包含字段名称的别名,所以它可以匹配旧Writer的模式字段名称与别名。这意味着更改字段名称是向后兼容的,但不能向前兼容。同样,向联合类型添加分支也是向后兼容的,但不能向前兼容。
+只要 Avro 可以支持相应的类型转换,就可以改变字段的数据类型。更改字段的名称也是可能的,但有点棘手:Reader 模式可以包含字段名称的别名,所以它可以匹配旧 Writer 的模式字段名称与别名。这意味着更改字段名称是向后兼容的,但不能向前兼容。同样,向联合类型添加分支也是向后兼容的,但不能向前兼容。
 
 #### 但Writer模式到底是什么?
 
-到目前为止,我们一直跳过了一个重要的问题:对于一段特定的编码数据,Reader如何知道其Writer模式?我们不能只将整个模式包括在每个记录中,因为模式可能比编码的数据大得多,从而使二进制编码节省的所有空间都是徒劳的。
+到目前为止,我们一直跳过了一个重要的问题:对于一段特定的编码数据,Reader 如何知道其 Writer 模式?我们不能只将整个模式包括在每个记录中,因为模式可能比编码的数据大得多,从而使二进制编码节省的所有空间都是徒劳的。
 
-答案取决于Avro使用的上下文。举几个例子:
+答案取决于 Avro 使用的上下文。举几个例子:
 
 * 有很多记录的大文件
 
-  Avro的一个常见用途 - 尤其是在Hadoop环境中 - 用于存储包含数百万条记录的大文件,所有记录都使用相同的模式进行编码(我们将在[第十章](ch10.md)讨论这种情况)。在这种情况下,该文件的作者可以在文件的开头只包含一次Writer模式。 Avro指定了一个文件格式(对象容器文件)来做到这一点。
+  Avro 的一个常见用途 - 尤其是在 Hadoop 环境中 - 用于存储包含数百万条记录的大文件,所有记录都使用相同的模式进行编码(我们将在 [第十章](ch10.md) 讨论这种情况)。在这种情况下,该文件的作者可以在文件的开头只包含一次 Writer 模式。 Avro 指定了一个文件格式(对象容器文件)来做到这一点。
 
 * 支持独立写入的记录的数据库
 
-  在一个数据库中,不同的记录可能会在不同的时间点使用不同的Writer模式来写入 - 你不能假定所有的记录都有相同的模式。最简单的解决方案是在每个编码记录的开始处包含一个版本号,并在数据库中保留一个模式版本列表。Reader可以获取记录,提取版本号,然后从数据库中获取该版本号的Writer模式。使用该Writer模式,它可以解码记录的其余部分(例如Espresso 【23】就是这样工作的)。
+  在一个数据库中,不同的记录可能会在不同的时间点使用不同的 Writer 模式来写入 - 你不能假定所有的记录都有相同的模式。最简单的解决方案是在每个编码记录的开始处包含一个版本号,并在数据库中保留一个模式版本列表。Reader 可以获取记录,提取版本号,然后从数据库中获取该版本号的 Writer 模式。使用该 Writer 模式,它可以解码记录的其余部分(例如 Espresso 【23】就是这样工作的)。
 
 * 通过网络连接发送记录
 
-  当两个进程通过双向网络连接进行通信时,他们可以在连接设置上协商模式版本,然后在连接的生命周期中使用该模式。 Avro RPC协议(请参阅“[服务中的数据流:REST与RPC](#服务中的数据流:REST与RPC)”)就是这样工作的。
+  当两个进程通过双向网络连接进行通信时,他们可以在连接设置上协商模式版本,然后在连接的生命周期中使用该模式。 Avro RPC 协议(请参阅 “[服务中的数据流:REST 与 RPC](#服务中的数据流:REST与RPC)”)就是这样工作的。
 
 具有模式版本的数据库在任何情况下都是非常有用的,因为它充当文档并为你提供了检查模式兼容性的机会【24】。作为版本号,你可以使用一个简单的递增整数,或者你可以使用模式的散列。
 
 #### 动态生成的模式
 
-与Protocol Buffers和Thrift相比,Avro方法的一个优点是架构不包含任何标签号码。但为什么这很重要?在模式中保留一些数字有什么问题?
+与 Protocol Buffers 和 Thrift 相比,Avro 方法的一个优点是架构不包含任何标签号码。但为什么这很重要?在模式中保留一些数字有什么问题?
 
-不同之处在于Avro对动态生成的模式更友善。例如,假如你有一个关系数据库,你想要把它的内容转储到一个文件中,并且你想使用二进制格式来避免前面提到的文本格式(JSON,CSV,SQL)的问题。如果你使用Avro,你可以很容易地从关系模式生成一个Avro模式(在我们之前看到的JSON表示中),并使用该模式对数据库内容进行编码,并将其全部转储到Avro对象容器文件【25】中。你为每个数据库表生成一个记录模式,每个列成为该记录中的一个字段。数据库中的列名称映射到Avro中的字段名称。
+不同之处在于 Avro 对动态生成的模式更友善。例如,假如你有一个关系数据库,你想要把它的内容转储到一个文件中,并且你想使用二进制格式来避免前面提到的文本格式(JSON,CSV,SQL)的问题。如果你使用 Avro,你可以很容易地从关系模式生成一个 Avro 模式(在我们之前看到的 JSON 表示中),并使用该模式对数据库内容进行编码,并将其全部转储到 Avro 对象容器文件【25】中。你为每个数据库表生成一个记录模式,每个列成为该记录中的一个字段。数据库中的列名称映射到 Avro 中的字段名称。
 
-现在,如果数据库模式发生变化(例如,一个表中添加了一列,删除了一列),则可以从更新的数据库模式生成新的Avro模式,并在新的Avro模式中导出数据。数据导出过程不需要注意模式的改变 - 每次运行时都可以简单地进行模式转换。任何读取新数据文件的人都会看到记录的字段已经改变,但是由于字段是通过名字来标识的,所以更新的Writer模式仍然可以与旧的Reader模式匹配。
+现在,如果数据库模式发生变化(例如,一个表中添加了一列,删除了一列),则可以从更新的数据库模式生成新的 Avro 模式,并在新的 Avro 模式中导出数据。数据导出过程不需要注意模式的改变 - 每次运行时都可以简单地进行模式转换。任何读取新数据文件的人都会看到记录的字段已经改变,但是由于字段是通过名字来标识的,所以更新的 Writer 模式仍然可以与旧的 Reader 模式匹配。
 
-相比之下,如果你为此使用Thrift或Protocol Buffers,则字段标签可能必须手动分配:每次数据库模式更改时,管理员都必须手动更新从数据库列名到字段标签的映射(这可能会自动化,但模式生成器必须非常小心,不要分配以前使用的字段标签)。这种动态生成的模式根本不是Thrift或Protocol Buffers的设计目标,而是Avro的。
+相比之下,如果你为此使用 Thrift 或 Protocol Buffers,则字段标签可能必须手动分配:每次数据库模式更改时,管理员都必须手动更新从数据库列名到字段标签的映射(这可能会自动化,但模式生成器必须非常小心,不要分配以前使用的字段标签)。这种动态生成的模式根本不是 Thrift 或 Protocol Buffers 的设计目标,而是 Avro 的。
 
 #### 代码生成和动态类型的语言
 
-Thrift和Protobuf依赖于代码生成:在定义了模式之后,可以使用你选择的编程语言生成实现此模式的代码。这在Java、C++或C#等静态类型语言中很有用,因为它允许将高效的内存中结构用于解码的数据,并且在编写访问数据结构的程序时允许在IDE中进行类型检查和自动完成。
+Thrift 和 Protobuf 依赖于代码生成:在定义了模式之后,可以使用你选择的编程语言生成实现此模式的代码。这在 Java、C++ 或 C# 等静态类型语言中很有用,因为它允许将高效的内存中结构用于解码的数据,并且在编写访问数据结构的程序时允许在 IDE 中进行类型检查和自动完成。
 
-在动态类型编程语言(如JavaScript、Ruby或Python)中,生成代码没有太多意义,因为没有编译时类型检查器来满足。代码生成在这些语言中经常被忽视,因为它们避免了显式的编译步骤。而且,对于动态生成的模式(例如从数据库表生成的Avro模式),代码生成对获取数据是一个不必要的障碍。
+在动态类型编程语言(如 JavaScript、Ruby 或 Python)中,生成代码没有太多意义,因为没有编译时类型检查器来满足。代码生成在这些语言中经常被忽视,因为它们避免了显式的编译步骤。而且,对于动态生成的模式(例如从数据库表生成的 Avro 模式),代码生成对获取数据是一个不必要的障碍。
 
-Avro为静态类型编程语言提供了可选的代码生成功能,但是它也可以在不生成任何代码的情况下使用。如果你有一个对象容器文件(它嵌入了Writer模式),你可以简单地使用Avro库打开它,并以与查看JSON文件相同的方式查看数据。该文件是自描述的,因为它包含所有必要的元数据。
+Avro 为静态类型编程语言提供了可选的代码生成功能,但是它也可以在不生成任何代码的情况下使用。如果你有一个对象容器文件(它嵌入了 Writer 模式),你可以简单地使用 Avro 库打开它,并以与查看 JSON 文件相同的方式查看数据。该文件是自描述的,因为它包含所有必要的元数据。
 
-这个属性特别适用于动态类型的数据处理语言如Apache Pig 【26】。在Pig中,你可以打开一些Avro文件,开始分析它们,并编写派生数据集以Avro格式输出文件,而无需考虑模式。
+这个属性特别适用于动态类型的数据处理语言如 Apache Pig 【26】。在 Pig 中,你可以打开一些 Avro 文件,开始分析它们,并编写派生数据集以 Avro 格式输出文件,而无需考虑模式。
 
 ### 模式的优点
 
-正如我们所看到的,Protocol Buffers,Thrift和Avro都使用模式来描述二进制编码格式。他们的模式语言比XML模式或者JSON模式简单得多,而后者支持更详细的验证规则(例如,“该字段的字符串值必须与该正则表达式匹配”或“该字段的整数值必须在0和100之间“)。由于Protocol Buffers,Thrift和Avro实现起来更简单,使用起来也更简单,所以它们已经发展到支持相当广泛的编程语言。
+正如我们所看到的,Protocol Buffers,Thrift 和 Avro 都使用模式来描述二进制编码格式。他们的模式语言比 XML 模式或者 JSON 模式简单得多,而后者支持更详细的验证规则(例如,“该字段的字符串值必须与该正则表达式匹配” 或 “该字段的整数值必须在 0 和 100 之间 “)。由于 Protocol Buffers,Thrift 和 Avro 实现起来更简单,使用起来也更简单,所以它们已经发展到支持相当广泛的编程语言。
 
-这些编码所基于的想法绝不是新的。例如,它们与ASN.1有很多相似之处,它是1984年首次被标准化的模式定义语言【27】。它被用来定义各种网络协议,例如其二进制编码(DER)仍然被用于编码SSL证书(X.509)【28】。 ASN.1支持使用标签号码的模式演进,类似于Protocol Buffers和Thrift 【29】。然而,它也非常复杂,而且没有好的配套文档,所以ASN.1可能不是新应用程序的好选择。
+这些编码所基于的想法绝不是新的。例如,它们与 ASN.1 有很多相似之处,它是 1984 年首次被标准化的模式定义语言【27】。它被用来定义各种网络协议,例如其二进制编码(DER)仍然被用于编码 SSL 证书(X.509)【28】。 ASN.1 支持使用标签号码的模式演进,类似于 Protocol Buffers 和 Thrift 【29】。然而,它也非常复杂,而且没有好的配套文档,所以 ASN.1 可能不是新应用程序的好选择。
 
-许多数据系统也为其数据实现了某种专有的二进制编码。例如,大多数关系数据库都有一个网络协议,你可以通过该协议向数据库发送查询并获取响应。这些协议通常特定于特定的数据库,并且数据库供应商提供将来自数据库的网络协议的响应解码为内存数据结构的驱动程序(例如使用ODBC或JDBC API)。
+许多数据系统也为其数据实现了某种专有的二进制编码。例如,大多数关系数据库都有一个网络协议,你可以通过该协议向数据库发送查询并获取响应。这些协议通常特定于特定的数据库,并且数据库供应商提供将来自数据库的网络协议的响应解码为内存数据结构的驱动程序(例如使用 ODBC 或 JDBC API)。
 
-所以,我们可以看到,尽管JSON,XML和CSV等文本数据格式非常普遍,但基于模式的二进制编码也是一个可行的选择。他们有一些很好的属性:
+所以,我们可以看到,尽管 JSON,XML 和 CSV 等文本数据格式非常普遍,但基于模式的二进制编码也是一个可行的选择。他们有一些很好的属性:
 
-* 它们可以比各种“二进制JSON”变体更紧凑,因为它们可以省略编码数据中的字段名称。
+* 它们可以比各种 “二进制 JSON” 变体更紧凑,因为它们可以省略编码数据中的字段名称。
 * 模式是一种有价值的文档形式,因为模式是解码所必需的,所以可以确定它是最新的(而手动维护的文档可能很容易偏离现实)。
 * 维护一个模式的数据库允许你在部署任何内容之前检查模式更改的向前和向后兼容性。
 * 对于静态类型编程语言的用户来说,从模式生成代码的能力是有用的,因为它可以在编译时进行类型检查。
 
-总而言之,模式进化允许与JSON数据库提供的无模式/读时模式相同的灵活性(请参阅“[文档模型中的模式灵活性](ch2.md#文档模型中的模式灵活性)”),同时还可以更好地保证数据和更好的工具。
+总而言之,模式进化允许与 JSON 数据库提供的无模式 / 读时模式相同的灵活性(请参阅 “[文档模型中的模式灵活性](ch2.md#文档模型中的模式灵活性)”),同时还可以更好地保证数据和更好的工具。
 
 
 ## 数据流的类型
@@ -324,9 +324,9 @@ Avro为静态类型编程语言提供了可选的代码生成功能,但是它
 
 这是一个相当抽象的概念 - 数据可以通过多种方式从一个流程流向另一个流程。谁编码数据,谁解码?在本章的其余部分中,我们将探讨数据如何在流程之间流动的一些最常见的方式:
 
-* 通过数据库(请参阅“[数据库中的数据流](#数据库中的数据流)”)
-* 通过服务调用(请参阅“[服务中的数据流:REST与RPC](#服务中的数据流:REST与RPC)”)
-* 通过异步消息传递(请参阅“[消息传递中的数据流](#消息传递中的数据流)”)
+* 通过数据库(请参阅 “[数据库中的数据流](#数据库中的数据流)”)
+* 通过服务调用(请参阅 “[服务中的数据流:REST 与 RPC](#服务中的数据流:REST与RPC)”)
+* 通过异步消息传递(请参阅 “[消息传递中的数据流](#消息传递中的数据流)”)
 
 
 ### 数据库中的数据流
@@ -341,11 +341,11 @@ Avro为静态类型编程语言提供了可选的代码生成功能,但是它
 
 但是,还有一个额外的障碍。假设你将一个字段添加到记录模式,并且较新的代码将该新字段的值写入数据库。随后,旧版本的代码(尚不知道新字段)将读取记录,更新记录并将其写回。在这种情况下,理想的行为通常是旧代码保持新的字段不变,即使它不能被解释。
 
-前面讨论的编码格式支持未知字段的保存,但是有时候需要在应用程序层面保持谨慎,如图4-7所示。例如,如果将数据库值解码为应用程序中的模型对象,稍后重新编码这些模型对象,那么未知字段可能会在该翻译过程中丢失。解决这个问题不是一个难题,你只需要意识到它。
+前面讨论的编码格式支持未知字段的保存,但是有时候需要在应用程序层面保持谨慎,如图 4-7 所示。例如,如果将数据库值解码为应用程序中的模型对象,稍后重新编码这些模型对象,那么未知字段可能会在该翻译过程中丢失。解决这个问题不是一个难题,你只需要意识到它。
 
 ![](img/fig4-7.png)
 
-**图4-7 当较旧版本的应用程序更新以前由较新版本的应用程序编写的数据时,如果不小心,数据可能会丢失。**
+**图 4-7 当较旧版本的应用程序更新以前由较新版本的应用程序编写的数据时,如果不小心,数据可能会丢失。**
 
 #### 在不同的时间写入不同的值
 
@@ -353,147 +353,147 @@ Avro为静态类型编程语言提供了可选的代码生成功能,但是它
 
 在部署应用程序的新版本时,也许用不了几分钟就可以将所有的旧版本替换为新版本(至少服务器端应用程序是这样的)。但数据库内容并非如此:对于五年前的数据来说,除非对其进行显式重写,否则它仍然会以原始编码形式存在。这种现象有时被概括为:数据的生命周期超出代码的生命周期。
 
-将数据重写(迁移)到一个新的模式当然是可能的,但是在一个大数据集上执行是一个昂贵的事情,所以大多数数据库如果可能的话就避免它。大多数关系数据库都允许简单的模式更改,例如添加一个默认值为空的新列,而不重写现有数据[^v]。读取旧行时,对于磁盘上的编码数据缺少的任何列,数据库将填充空值。 LinkedIn的文档数据库Espresso使用Avro存储,允许它使用Avro的模式演变规则【23】。
+将数据重写(迁移)到一个新的模式当然是可能的,但是在一个大数据集上执行是一个昂贵的事情,所以大多数数据库如果可能的话就避免它。大多数关系数据库都允许简单的模式更改,例如添加一个默认值为空的新列,而不重写现有数据 [^v]。读取旧行时,对于磁盘上的编码数据缺少的任何列,数据库将填充空值。 LinkedIn 的文档数据库 Espresso 使用 Avro 存储,允许它使用 Avro 的模式演变规则【23】。
 
 因此,模式演变允许整个数据库看起来好像是用单个模式编码的,即使底层存储可能包含用各种历史版本的模式编码的记录。
 
-[^v]: 除了MySQL,即使并非真的必要,它也经常会重写整个表,正如“[文档模型中的模式灵活性](ch3.md#文档模型中的模式灵活性)”中所提到的。
+[^v]: 除了 MySQL,即使并非真的必要,它也经常会重写整个表,正如 “[文档模型中的模式灵活性](ch3.md#文档模型中的模式灵活性)” 中所提到的。
 
 
 #### 归档存储
 
-也许你不时为数据库创建一个快照,例如备份或加载到数据仓库(请参阅“[数据仓库](ch3.md#数据仓库)”)。在这种情况下,即使源数据库中的原始编码包含来自不同时代的模式版本的混合,数据转储通常也将使用最新模式进行编码。既然你不管怎样都要拷贝数据,那么你可以对这个数据拷贝进行一致的编码。
+也许你不时为数据库创建一个快照,例如备份或加载到数据仓库(请参阅 “[数据仓库](ch3.md#数据仓库)”)。在这种情况下,即使源数据库中的原始编码包含来自不同时代的模式版本的混合,数据转储通常也将使用最新模式进行编码。既然你不管怎样都要拷贝数据,那么你可以对这个数据拷贝进行一致的编码。
 
-由于数据转储是一次写入的,而且以后是不可变的,所以Avro对象容器文件等格式非常适合。这也是一个很好的机会,可以将数据编码为面向分析的列式格式,例如Parquet(请参阅“[列压缩](ch3.md#列压缩)”)。
+由于数据转储是一次写入的,而且以后是不可变的,所以 Avro 对象容器文件等格式非常适合。这也是一个很好的机会,可以将数据编码为面向分析的列式格式,例如 Parquet(请参阅 “[列压缩](ch3.md#列压缩)”)。
 
-在[第十章](ch10.md)中,我们将详细讨论使用档案存储中的数据。
+在 [第十章](ch10.md) 中,我们将详细讨论使用档案存储中的数据。
 
 
 ### 服务中的数据流:REST与RPC
 
-当你需要通过网络进行通信的进程时,安排该通信的方式有几种。最常见的安排是有两个角色:客户端和服务器。服务器通过网络公开API,并且客户端可以连接到服务器以向该API发出请求。服务器公开的API被称为服务。
+当你需要通过网络进行通信的进程时,安排该通信的方式有几种。最常见的安排是有两个角色:客户端和服务器。服务器通过网络公开 API,并且客户端可以连接到服务器以向该 API 发出请求。服务器公开的 API 被称为服务。
 
-Web以这种方式工作:客户(Web浏览器)向Web服务器发出请求,通过GET请求下载HTML,CSS,JavaScript,图像等,并通过POST请求提交数据到服务器。 API包含一组标准的协议和数据格式(HTTP,URL,SSL/TLS,HTML等)。由于网络浏览器,网络服务器和网站作者大多同意这些标准,你可以使用任何网络浏览器访问任何网站(至少在理论上!)。
+Web 以这种方式工作:客户(Web 浏览器)向 Web 服务器发出请求,通过 GET 请求下载 HTML、CSS、JavaScript、图像等,并通过 POST 请求提交数据到服务器。 API 包含一组标准的协议和数据格式(HTTP、URL、SSL/TLS、HTML 等)。由于网络浏览器、网络服务器和网站作者大多同意这些标准,你可以使用任何网络浏览器访问任何网站(至少在理论上!)。
 
-Web浏览器不是唯一的客户端类型。例如,在移动设备或桌面计算机上运行的本地应用程序也可以向服务器发出网络请求,并且在Web浏览器内运行的客户端JavaScript应用程序可以使用XMLHttpRequest成为HTTP客户端(该技术被称为Ajax 【30】)。在这种情况下,服务器的响应通常不是用于显示给人的HTML,而是便于客户端应用程序代码进一步处理的编码数据(如JSON)。尽管HTTP可能被用作传输协议,但顶层实现的API是特定于应用程序的,客户端和服务器需要就该API的细节达成一致。
+Web 浏览器不是唯一的客户端类型。例如,在移动设备或桌面计算机上运行的本地应用程序也可以向服务器发出网络请求,并且在 Web 浏览器内运行的客户端 JavaScript 应用程序可以使用 XMLHttpRequest 成为 HTTP 客户端(该技术被称为 Ajax 【30】)。在这种情况下,服务器的响应通常不是用于显示给人的 HTML,而是便于客户端应用程序代码进一步处理的编码数据(如 JSON)。尽管 HTTP 可能被用作传输协议,但顶层实现的 API 是特定于应用程序的,客户端和服务器需要就该 API 的细节达成一致。
 
-此外,服务器本身可以是另一个服务的客户端(例如,典型的Web应用服务器充当数据库的客户端)。这种方法通常用于将大型应用程序按照功能区域分解为较小的服务,这样当一个服务需要来自另一个服务的某些功能或数据时,就会向另一个服务发出请求。这种构建应用程序的方式传统上被称为**面向服务的体系结构(service-oriented architecture,SOA)**,最近被改进和更名为**微服务架构**【31,32】。
+此外,服务器本身可以是另一个服务的客户端(例如,典型的 Web 应用服务器充当数据库的客户端)。这种方法通常用于将大型应用程序按照功能区域分解为较小的服务,这样当一个服务需要来自另一个服务的某些功能或数据时,就会向另一个服务发出请求。这种构建应用程序的方式传统上被称为 **面向服务的体系结构(service-oriented architecture,SOA)**,最近被改进和更名为 **微服务架构**【31,32】。
 
-在某些方面,服务类似于数据库:它们通常允许客户端提交和查询数据。但是,虽然数据库允许使用我们在[第二章](ch2.md)中讨论的查询语言进行任意查询,但是服务公开了一个特定于应用程序的API,它只允许由服务的业务逻辑(应用程序代码)预定的输入和输出【33】。这种限制提供了一定程度的封装:服务能够对客户可以做什么和不可以做什么施加细粒度的限制。
+在某些方面,服务类似于数据库:它们通常允许客户端提交和查询数据。但是,虽然数据库允许使用我们在 [第二章](ch2.md) 中讨论的查询语言进行任意查询,但是服务公开了一个特定于应用程序的 API,它只允许由服务的业务逻辑(应用程序代码)预定的输入和输出【33】。这种限制提供了一定程度的封装:服务能够对客户可以做什么和不可以做什么施加细粒度的限制。
 
-面向服务/微服务架构的一个关键设计目标是通过使服务独立部署和演化来使应用程序更易于更改和维护。例如,每个服务应该由一个团队拥有,并且该团队应该能够经常发布新版本的服务,而不必与其他团队协调。换句话说,我们应该期望服务器和客户端的旧版本和新版本同时运行,因此服务器和客户端使用的数据编码必须在不同版本的服务API之间兼容——这正是我们在本章所一直在谈论的。
+面向服务 / 微服务架构的一个关键设计目标是通过使服务独立部署和演化来使应用程序更易于更改和维护。例如,每个服务应该由一个团队拥有,并且该团队应该能够经常发布新版本的服务,而不必与其他团队协调。换句话说,我们应该期望服务器和客户端的旧版本和新版本同时运行,因此服务器和客户端使用的数据编码必须在不同版本的服务 API 之间兼容 —— 这正是我们在本章所一直在谈论的。
 
 #### Web服务
 
-**当服务使用HTTP作为底层通信协议时,可称之为Web服务**。这可能是一个小错误,因为Web服务不仅在Web上使用,而且在几个不同的环境中使用。例如:
+**当服务使用 HTTP 作为底层通信协议时,可称之为 Web 服务**。这可能是一个小错误,因为 Web 服务不仅在 Web 上使用,而且在几个不同的环境中使用。例如:
 
-1. 运行在用户设备上的客户端应用程序(例如,移动设备上的本地应用程序,或使用Ajax的JavaScript web应用程序)通过HTTP向服务发出请求。这些请求通常通过公共互联网进行。
-2. 一种服务向同一组织拥有的另一项服务提出请求,这些服务通常位于同一数据中心内,作为面向服务/微型架构的一部分。 (支持这种用例的软件有时被称为 **中间件(middleware)** )
-3. 一种服务通过互联网向不同组织所拥有的服务提出请求。这用于不同组织后端系统之间的数据交换。此类别包括由在线服务(如信用卡处理系统)提供的公共API,或用于共享访问用户数据的OAuth。
+1. 运行在用户设备上的客户端应用程序(例如,移动设备上的本地应用程序,或使用 Ajax 的 JavaScript web 应用程序)通过 HTTP 向服务发出请求。这些请求通常通过公共互联网进行。
+2. 一种服务向同一组织拥有的另一项服务提出请求,这些服务通常位于同一数据中心内,作为面向服务 / 微型架构的一部分。 (支持这种用例的软件有时被称为 **中间件(middleware)** )
+3. 一种服务通过互联网向不同组织所拥有的服务提出请求。这用于不同组织后端系统之间的数据交换。此类别包括由在线服务(如信用卡处理系统)提供的公共 API,或用于共享访问用户数据的 OAuth。
 
-有两种流行的Web服务方法:REST和SOAP。他们在哲学方面几乎是截然相反的,往往也是各自支持者之间的激烈辩论的主题[^vi]。
+有两种流行的 Web 服务方法:REST 和 SOAP。他们在哲学方面几乎是截然相反的,往往也是各自支持者之间的激烈辩论的主题 [^vi]。
 
 [^vi]: 即使在每个阵营内也有很多争论。 例如,**HATEOAS(超媒体作为应用程序状态的引擎)** 就经常引发讨论【35】。
 
-REST不是一个协议,而是一个基于HTTP原则的设计哲学【34,35】。它强调简单的数据格式,使用URL来标识资源,并使用HTTP功能进行缓存控制,身份验证和内容类型协商。与SOAP相比,REST已经越来越受欢迎,至少在跨组织服务集成的背景下【36】,并经常与微服务相关【31】。根据REST原则设计的API称为RESTful。
+REST 不是一个协议,而是一个基于 HTTP 原则的设计哲学【34,35】。它强调简单的数据格式,使用 URL 来标识资源,并使用 HTTP 功能进行缓存控制,身份验证和内容类型协商。与 SOAP 相比,REST 已经越来越受欢迎,至少在跨组织服务集成的背景下【36】,并经常与微服务相关【31】。根据 REST 原则设计的 API 称为 RESTful。
 
-相比之下,SOAP是用于制作网络API请求的基于XML的协议[^vii]。虽然它最常用于HTTP,但其目的是独立于HTTP,并避免使用大多数HTTP功能。相反,它带有庞大而复杂的多种相关标准(Web服务框架,称为`WS-*`),它们增加了各种功能【37】。
+相比之下,SOAP 是用于制作网络 API 请求的基于 XML 的协议 [^vii]。虽然它最常用于 HTTP,但其目的是独立于 HTTP,并避免使用大多数 HTTP 功能。相反,它带有庞大而复杂的多种相关标准(Web 服务框架,称为 `WS-*`),它们增加了各种功能【37】。
 
-[^vii]: 尽管首字母缩写词相似,SOAP并不是SOA的要求。 SOAP是一种特殊的技术,而SOA是构建系统的一般方法。
+[^vii]: 尽管首字母缩写词相似,SOAP 并不是 SOA 的要求。 SOAP 是一种特殊的技术,而 SOA 是构建系统的一般方法。
 
-SOAP Web服务的API使用称为Web服务描述语言(WSDL)的基于XML的语言来描述。 WSDL支持代码生成,客户端可以使用本地类和方法调用(编码为XML消息并由框架再次解码)访问远程服务。这在静态类型编程语言中非常有用,但在动态类型编程语言中很少(请参阅“[代码生成和动态类型的语言](#代码生成和动态类型的语言)”)。
+SOAP Web 服务的 API 使用称为 Web 服务描述语言(WSDL)的基于 XML 的语言来描述。 WSDL 支持代码生成,客户端可以使用本地类和方法调用(编码为 XML 消息并由框架再次解码)访问远程服务。这在静态类型编程语言中非常有用,但在动态类型编程语言中很少(请参阅 “[代码生成和动态类型的语言](#代码生成和动态类型的语言)”)。
 
-由于WSDL的设计不是人类可读的,而且由于SOAP消息通常因为过于复杂而无法手动构建,所以SOAP的用户在很大程度上依赖于工具支持,代码生成和IDE【38】。对于SOAP供应商不支持的编程语言的用户来说,与SOAP服务的集成是困难的。
+由于 WSDL 的设计不是人类可读的,而且由于 SOAP 消息通常因为过于复杂而无法手动构建,所以 SOAP 的用户在很大程度上依赖于工具支持,代码生成和 IDE【38】。对于 SOAP 供应商不支持的编程语言的用户来说,与 SOAP 服务的集成是困难的。
 
-尽管SOAP及其各种扩展表面上是标准化的,但是不同厂商的实现之间的互操作性往往会造成问题【39】。由于所有这些原因,尽管许多大型企业仍然使用SOAP,但在大多数小公司中已经不再受到青睐。
+尽管 SOAP 及其各种扩展表面上是标准化的,但是不同厂商的实现之间的互操作性往往会造成问题【39】。由于所有这些原因,尽管许多大型企业仍然使用 SOAP,但在大多数小公司中已经不再受到青睐。
 
-REST风格的API倾向于更简单的方法,通常涉及较少的代码生成和自动化工具。定义格式(如OpenAPI,也称为Swagger 【40】)可用于描述RESTful API并生成文档。
+REST 风格的 API 倾向于更简单的方法,通常涉及较少的代码生成和自动化工具。定义格式(如 OpenAPI,也称为 Swagger 【40】)可用于描述 RESTful API 并生成文档。
 
 #### 远程过程调用(RPC)的问题
 
-Web服务仅仅是通过网络进行API请求的一系列技术的最新版本,其中许多技术受到了大量的炒作,但是存在严重的问题。 Enterprise JavaBeans(EJB)和Java的**远程方法调用(RMI)** 仅限于Java。**分布式组件对象模型(DCOM)** 仅限于Microsoft平台。**公共对象请求代理体系结构(CORBA)** 过于复杂,不提供前向或后向兼容性【41】。
+Web 服务仅仅是通过网络进行 API 请求的一系列技术的最新版本,其中许多技术受到了大量的炒作,但是存在严重的问题。 Enterprise JavaBeans(EJB)和 Java 的 **远程方法调用(RMI)** 仅限于 Java。**分布式组件对象模型(DCOM)** 仅限于 Microsoft 平台。**公共对象请求代理体系结构(CORBA)** 过于复杂,不提供前向或后向兼容性【41】。
 
-所有这些都是基于 **远程过程调用(RPC)** 的思想,该过程调用自20世纪70年代以来一直存在【42】。 RPC模型试图向远程网络服务发出请求,看起来与在同一进程中调用编程语言中的函数或方法相同(这种抽象称为位置透明)。尽管RPC起初看起来很方便,但这种方法根本上是有缺陷的【43,44】。网络请求与本地函数调用非常不同:
+所有这些都是基于 **远程过程调用(RPC)** 的思想,该过程调用自 20 世纪 70 年代以来一直存在【42】。 RPC 模型试图向远程网络服务发出请求,看起来与在同一进程中调用编程语言中的函数或方法相同(这种抽象称为位置透明)。尽管 RPC 起初看起来很方便,但这种方法根本上是有缺陷的【43,44】。网络请求与本地函数调用非常不同:
 
 * 本地函数调用是可预测的,并且成功或失败仅取决于受你控制的参数。网络请求是不可预知的:由于网络问题,请求或响应可能会丢失,或者远程计算机可能很慢或不可用,这些问题完全不在你的控制范围之内。网络问题是常见的,所以你必须预测他们,例如通过重试失败的请求。
-* 本地函数调用要么返回结果,要么抛出异常,或者永远不返回(因为进入无限循环或进程崩溃)。网络请求有另一个可能的结果:由于超时,它可能会返回没有结果。在这种情况下,你根本不知道发生了什么:如果你没有得到来自远程服务的响应,你无法知道请求是否通过(我们将在[第八章](ch8.md)更详细地讨论这个问题)。
-* 如果你重试失败的网络请求,可能会发生请求实际上正在通过,只有响应丢失。在这种情况下,重试将导致该操作被执行多次,除非你在协议中引入去重机制(**幂等**,即idempotence)。本地函数调用没有这个问题。 (在[第十一章](ch11.md)更详细地讨论幂等性)
+* 本地函数调用要么返回结果,要么抛出异常,或者永远不返回(因为进入无限循环或进程崩溃)。网络请求有另一个可能的结果:由于超时,它可能会返回没有结果。在这种情况下,你根本不知道发生了什么:如果你没有得到来自远程服务的响应,你无法知道请求是否通过(我们将在 [第八章](ch8.md) 更详细地讨论这个问题)。
+* 如果你重试失败的网络请求,可能会发生请求实际上正在通过,只有响应丢失。在这种情况下,重试将导致该操作被执行多次,除非你在协议中引入去重机制(**幂等**,即 idempotence)。本地函数调用没有这个问题。 (在 [第十一章](ch11.md) 更详细地讨论幂等性)
 * 每次调用本地功能时,通常需要大致相同的时间来执行。网络请求比函数调用要慢得多,而且其延迟也是非常可变的:好的时候它可能会在不到一毫秒的时间内完成,但是当网络拥塞或者远程服务超载时,可能需要几秒钟的时间完成一样的东西。
 * 调用本地函数时,可以高效地将引用(指针)传递给本地内存中的对象。当你发出一个网络请求时,所有这些参数都需要被编码成可以通过网络发送的一系列字节。如果参数是像数字或字符串这样的基本类型倒是没关系,但是对于较大的对象很快就会变成问题。
 
-- 客户端和服务可以用不同的编程语言实现,所以RPC框架必须将数据类型从一种语言翻译成另一种语言。这可能会捅出大篓子,因为不是所有的语言都具有相同的类型 —— 例如回想一下JavaScript的数字大于$2^{53}$的问题(请参阅“[JSON、XML和二进制变体](#JSON、XML和二进制变体)”)。用单一语言编写的单个进程中不存在此问题。
+- 客户端和服务可以用不同的编程语言实现,所以 RPC 框架必须将数据类型从一种语言翻译成另一种语言。这可能会捅出大篓子,因为不是所有的语言都具有相同的类型 —— 例如回想一下 JavaScript 的数字大于 $2^{53}$ 的问题(请参阅 “[JSON、XML 和二进制变体](#JSON、XML和二进制变体)”)。用单一语言编写的单个进程中不存在此问题。
 
-所有这些因素意味着尝试使远程服务看起来像编程语言中的本地对象一样毫无意义,因为这是一个根本不同的事情。 REST的部分吸引力在于,它并不试图隐藏它是一个网络协议的事实(尽管这似乎并没有阻止人们在REST之上构建RPC库)。
+所有这些因素意味着尝试使远程服务看起来像编程语言中的本地对象一样毫无意义,因为这是一个根本不同的事情。 REST 的部分吸引力在于,它并不试图隐藏它是一个网络协议的事实(尽管这似乎并没有阻止人们在 REST 之上构建 RPC 库)。
 
 #### RPC的当前方向
 
-尽管有这样那样的问题,RPC不会消失。在本章提到的所有编码的基础上构建了各种RPC框架:例如,Thrift和Avro带有RPC支持,gRPC是使用Protocol Buffers的RPC实现,Finagle也使用Thrift,Rest.li使用JSON over HTTP。
+尽管有这样那样的问题,RPC 不会消失。在本章提到的所有编码的基础上构建了各种 RPC 框架:例如,Thrift 和 Avro 带有 RPC 支持,gRPC 是使用 Protocol Buffers 的 RPC 实现,Finagle 也使用 Thrift,Rest.li 使用 JSON over HTTP。
 
-这种新一代的RPC框架更加明确的是,远程请求与本地函数调用不同。例如,Finagle和Rest.li 使用futures(promises)来封装可能失败的异步操作。`Futures`还可以简化需要并行发出多项服务并将其结果合并的情况【45】。 gRPC支持流,其中一个调用不仅包括一个请求和一个响应,还可以是随时间的一系列请求和响应【46】。
+这种新一代的 RPC 框架更加明确的是,远程请求与本地函数调用不同。例如,Finagle 和 Rest.li 使用 futures(promises)来封装可能失败的异步操作。`Futures` 还可以简化需要并行发出多项服务并将其结果合并的情况【45】。 gRPC 支持流,其中一个调用不仅包括一个请求和一个响应,还可以是随时间的一系列请求和响应【46】。
 
-其中一些框架还提供服务发现,即允许客户端找出在哪个IP地址和端口号上可以找到特定的服务。我们将在“[请求路由](ch6.md#请求路由)”中回到这个主题。
+其中一些框架还提供服务发现,即允许客户端找出在哪个 IP 地址和端口号上可以找到特定的服务。我们将在 “[请求路由](ch6.md#请求路由)” 中回到这个主题。
 
-使用二进制编码格式的自定义RPC协议可以实现比通用的JSON over REST更好的性能。但是,RESTful API还有其他一些显著的优点:方便实验和调试(只需使用Web浏览器或命令行工具curl,无需任何代码生成或软件安装即可向其请求),能被所有主流的编程语言和平台所支持,还有大量可用的工具(服务器,缓存,负载平衡器,代理,防火墙,监控,调试工具,测试工具等)的生态系统。
+使用二进制编码格式的自定义 RPC 协议可以实现比通用的 JSON over REST 更好的性能。但是,RESTful API 还有其他一些显著的优点:方便实验和调试(只需使用 Web 浏览器或命令行工具 curl,无需任何代码生成或软件安装即可向其请求),能被所有主流的编程语言和平台所支持,还有大量可用的工具(服务器,缓存,负载平衡器,代理,防火墙,监控,调试工具,测试工具等)的生态系统。
 
-由于这些原因,REST似乎是公共API的主要风格。 RPC框架的主要重点在于同一组织拥有的服务之间的请求,通常在同一数据中心内。
+由于这些原因,REST 似乎是公共 API 的主要风格。 RPC 框架的主要重点在于同一组织拥有的服务之间的请求,通常在同一数据中心内。
 
 #### 数据编码与RPC的演化
 
-对于可演化性,重要的是可以独立更改和部署RPC客户端和服务器。与通过数据库流动的数据相比(如上一节所述),我们可以在通过服务进行数据流的情况下做一个简化的假设:假定所有的服务器都会先更新,其次是所有的客户端。因此,你只需要在请求上具有向后兼容性,并且对响应具有前向兼容性。
+对于可演化性,重要的是可以独立更改和部署 RPC 客户端和服务器。与通过数据库流动的数据相比(如上一节所述),我们可以在通过服务进行数据流的情况下做一个简化的假设:假定所有的服务器都会先更新,其次是所有的客户端。因此,你只需要在请求上具有向后兼容性,并且对响应具有前向兼容性。
 
-RPC方案的前后向兼容性属性从它使用的编码方式中继承:
+RPC 方案的前后向兼容性属性从它使用的编码方式中继承:
 
-* Thrift,gRPC(Protobuf)和Avro RPC可以根据相应编码格式的兼容性规则进行演变。
-* 在SOAP中,请求和响应是使用XML模式指定的。这些可以演变,但有一些微妙的陷阱【47】。
-* RESTful API通常使用JSON(没有正式指定的模式)用于响应,以及用于请求的JSON或URI编码/表单编码的请求参数。添加可选的请求参数并向响应对象添加新的字段通常被认为是保持兼容性的改变。
+* Thrift、gRPC(Protobuf)和 Avro RPC 可以根据相应编码格式的兼容性规则进行演变。
+* 在 SOAP 中,请求和响应是使用 XML 模式指定的。这些可以演变,但有一些微妙的陷阱【47】。
+* RESTful API 通常使用 JSON(没有正式指定的模式)用于响应,以及用于请求的 JSON 或 URI 编码 / 表单编码的请求参数。添加可选的请求参数并向响应对象添加新的字段通常被认为是保持兼容性的改变。
 
-由于RPC经常被用于跨越组织边界的通信,所以服务的兼容性变得更加困难,因此服务的提供者经常无法控制其客户,也不能强迫他们升级。因此,需要长期保持兼容性,也许是无限期的。如果需要进行兼容性更改,则服务提供商通常会并排维护多个版本的服务API。
+由于 RPC 经常被用于跨越组织边界的通信,所以服务的兼容性变得更加困难,因此服务的提供者经常无法控制其客户,也不能强迫他们升级。因此,需要长期保持兼容性,也许是无限期的。如果需要进行兼容性更改,则服务提供商通常会并排维护多个版本的服务 API。
 
-关于API版本化应该如何工作(即,客户端如何指示它想要使用哪个版本的API)没有一致意见【48】)。对于RESTful API,常用的方法是在URL或HTTP Accept头中使用版本号。对于使用API密钥来标识特定客户端的服务,另一种选择是将客户端请求的API版本存储在服务器上,并允许通过单独的管理界面更新该版本选项【49】。
+关于 API 版本化应该如何工作(即,客户端如何指示它想要使用哪个版本的 API)没有一致意见【48】)。对于 RESTful API,常用的方法是在 URL 或 HTTP Accept 头中使用版本号。对于使用 API 密钥来标识特定客户端的服务,另一种选择是将客户端请求的 API 版本存储在服务器上,并允许通过单独的管理界面更新该版本选项【49】。
 
 ### 消息传递中的数据流
 
-我们一直在研究从一个过程到另一个过程的编码数据流的不同方式。到目前为止,我们已经讨论了REST和RPC(其中一个进程通过网络向另一个进程发送请求并期望尽可能快的响应)以及数据库(一个进程写入编码数据,另一个进程在将来再次读取)。
+我们一直在研究从一个过程到另一个过程的编码数据流的不同方式。到目前为止,我们已经讨论了 REST 和 RPC(其中一个进程通过网络向另一个进程发送请求并期望尽可能快的响应)以及数据库(一个进程写入编码数据,另一个进程在将来再次读取)。
 
-在最后一节中,我们将简要介绍一下RPC和数据库之间的异步消息传递系统。它们与RPC类似,因为客户端的请求(通常称为消息)以低延迟传送到另一个进程。它们与数据库类似,不是通过直接的网络连接发送消息,而是通过称为消息代理(也称为消息队列或面向消息的中间件)的中介来临时存储消息。
+在最后一节中,我们将简要介绍一下 RPC 和数据库之间的异步消息传递系统。它们与 RPC 类似,因为客户端的请求(通常称为消息)以低延迟传送到另一个进程。它们与数据库类似,不是通过直接的网络连接发送消息,而是通过称为消息代理(也称为消息队列或面向消息的中间件)的中介来临时存储消息。
 
-与直接RPC相比,使用消息代理有几个优点:
+与直接 RPC 相比,使用消息代理有几个优点:
 
 * 如果收件人不可用或过载,可以充当缓冲区,从而提高系统的可靠性。
 * 它可以自动将消息重新发送到已经崩溃的进程,从而防止消息丢失。
-* 避免发件人需要知道收件人的IP地址和端口号(这在虚拟机经常出入的云部署中特别有用)。
+* 避免发件人需要知道收件人的 IP 地址和端口号(这在虚拟机经常出入的云部署中特别有用)。
 * 它允许将一条消息发送给多个收件人。
 * 将发件人与收件人逻辑分离(发件人只是发布邮件,不关心使用者)。
 
-然而,与RPC相比,差异在于消息传递通信通常是单向的:发送者通常不期望收到其消息的回复。一个进程可能发送一个响应,但这通常是在一个单独的通道上完成的。这种通信模式是异步的:发送者不会等待消息被传递,而只是发送它,然后忘记它。
+然而,与 RPC 相比,差异在于消息传递通信通常是单向的:发送者通常不期望收到其消息的回复。一个进程可能发送一个响应,但这通常是在一个单独的通道上完成的。这种通信模式是异步的:发送者不会等待消息被传递,而只是发送它,然后忘记它。
 
 #### 消息代理
 
-过去,**消息代理(Message Broker)** 主要是TIBCO、IBM WebSphere和webMethods等公司的商业软件的秀场。最近像RabbitMQ、ActiveMQ、HornetQ、NATS和Apache Kafka这样的开源实现已经流行起来。我们将在[第十一章](ch11.md)中对它们进行更详细的比较。
+过去,**消息代理(Message Broker)** 主要是 TIBCO、IBM WebSphere 和 webMethods 等公司的商业软件的秀场。最近像 RabbitMQ、ActiveMQ、HornetQ、NATS 和 Apache Kafka 这样的开源实现已经流行起来。我们将在 [第十一章](ch11.md) 中对它们进行更详细的比较。
 
 详细的交付语义因实现和配置而异,但通常情况下,消息代理的使用方式如下:一个进程将消息发送到指定的队列或主题,代理确保将消息传递给那个队列或主题的一个或多个消费者或订阅者。在同一主题上可以有许多生产者和许多消费者。
 
-一个主题只提供单向数据流。但是,消费者本身可能会将消息发布到另一个主题上(因此,可以将它们链接在一起,就像我们将在[第十一章](ch11.md)中看到的那样),或者发送给原始消息的发送者使用的回复队列(允许请求/响应数据流,类似于RPC)。
+一个主题只提供单向数据流。但是,消费者本身可能会将消息发布到另一个主题上(因此,可以将它们链接在一起,就像我们将在 [第十一章](ch11.md) 中看到的那样),或者发送给原始消息的发送者使用的回复队列(允许请求 / 响应数据流,类似于 RPC)。
 
 消息代理通常不会执行任何特定的数据模型 —— 消息只是包含一些元数据的字节序列,因此你可以使用任何编码格式。如果编码是向后和向前兼容的,你可以灵活地对发布者和消费者的编码进行独立的修改,并以任意顺序进行部署。
 
-如果消费者重新发布消息到另一个主题,则可能需要小心保留未知字段,以防止前面在数据库环境中描述的问题([图4-7](img/fig4-7.png))。
+如果消费者重新发布消息到另一个主题,则可能需要小心保留未知字段,以防止前面在数据库环境中描述的问题([图 4-7](img/fig4-7.png))。
 
 #### 分布式的Actor框架
 
-Actor模型是单个进程中并发的编程模型。逻辑被封装在actor中,而不是直接处理线程(以及竞争条件,锁定和死锁的相关问题)。每个actor通常代表一个客户或实体,它可能有一些本地状态(不与其他任何角色共享),它通过发送和接收异步消息与其他角色通信。不保证消息传送:在某些错误情况下,消息将丢失。由于每个角色一次只能处理一条消息,因此不需要担心线程,每个角色可以由框架独立调度。
+Actor 模型是单个进程中并发的编程模型。逻辑被封装在 actor 中,而不是直接处理线程(以及竞争条件、锁定和死锁的相关问题)。每个 actor 通常代表一个客户或实体,它可能有一些本地状态(不与其他任何角色共享),它通过发送和接收异步消息与其他角色通信。不保证消息传送:在某些错误情况下,消息将丢失。由于每个角色一次只能处理一条消息,因此不需要担心线程,每个角色可以由框架独立调度。
 
-在分布式Actor框架中,此编程模型用于跨多个节点伸缩应用程序。不管发送方和接收方是在同一个节点上还是在不同的节点上,都使用相同的消息传递机制。如果它们在不同的节点上,则该消息被透明地编码成字节序列,通过网络发送,并在另一侧解码。
+在分布式 Actor 框架中,此编程模型用于跨多个节点伸缩应用程序。不管发送方和接收方是在同一个节点上还是在不同的节点上,都使用相同的消息传递机制。如果它们在不同的节点上,则该消息被透明地编码成字节序列,通过网络发送,并在另一侧解码。
 
-位置透明在actor模型中比在RPC中效果更好,因为actor模型已经假定消息可能会丢失,即使在单个进程中也是如此。尽管网络上的延迟可能比同一个进程中的延迟更高,但是在使用actor模型时,本地和远程通信之间的基本不匹配是较少的。
+位置透明在 actor 模型中比在 RPC 中效果更好,因为 actor 模型已经假定消息可能会丢失,即使在单个进程中也是如此。尽管网络上的延迟可能比同一个进程中的延迟更高,但是在使用 actor 模型时,本地和远程通信之间的基本不匹配是较少的。
 
-分布式的Actor框架实质上是将消息代理和actor编程模型集成到一个框架中。但是,如果要执行基于actor的应用程序的滚动升级,则仍然需要担心向前和向后兼容性问题,因为消息可能会从运行新版本的节点发送到运行旧版本的节点,反之亦然。
+分布式的 Actor 框架实质上是将消息代理和 actor 编程模型集成到一个框架中。但是,如果要执行基于 actor 的应用程序的滚动升级,则仍然需要担心向前和向后兼容性问题,因为消息可能会从运行新版本的节点发送到运行旧版本的节点,反之亦然。
 
-三个流行的分布式actor框架处理消息编码如下:
+三个流行的分布式 actor 框架处理消息编码如下:
 
-* 默认情况下,Akka使用Java的内置序列化,不提供前向或后向兼容性。 但是,你可以用类似Prototol Buffers的东西替代它,从而获得滚动升级的能力【50】。
-* Orleans 默认使用不支持滚动升级部署的自定义数据编码格式; 要部署新版本的应用程序,你需要设置一个新的集群,将流量从旧集群迁移到新集群,然后关闭旧集群【51,52】。 像Akka一样,可以使用自定义序列化插件。
-* 在Erlang OTP中,对记录模式进行更改是非常困难的(尽管系统具有许多为高可用性设计的功能)。 滚动升级是可能的,但需要仔细计划【53】。 一个新的实验性的`maps`数据类型(2014年在Erlang R17中引入的类似于JSON的结构)可能使得这个数据类型在未来更容易【54】。
+* 默认情况下,Akka 使用 Java 的内置序列化,不提供前向或后向兼容性。 但是,你可以用类似 Prototol Buffers 的东西替代它,从而获得滚动升级的能力【50】。
+* Orleans 默认使用不支持滚动升级部署的自定义数据编码格式;要部署新版本的应用程序,你需要设置一个新的集群,将流量从旧集群迁移到新集群,然后关闭旧集群【51,52】。 像 Akka 一样,可以使用自定义序列化插件。
+* 在 Erlang OTP 中,对记录模式进行更改是非常困难的(尽管系统具有许多为高可用性设计的功能)。 滚动升级是可能的,但需要仔细计划【53】。 一个新的实验性的 `maps` 数据类型(2014 年在 Erlang R17 中引入的类似于 JSON 的结构)可能使得这个数据类型在未来更容易【54】。
 
 
 ## 本章小结
@@ -507,13 +507,13 @@ Actor模型是单个进程中并发的编程模型。逻辑被封装在actor中
 我们讨论了几种数据编码格式及其兼容性属性:
 
 * 编程语言特定的编码仅限于单一编程语言,并且往往无法提供前向和后向兼容性。
-* JSON、XML和CSV等文本格式非常普遍,其兼容性取决于你如何使用它们。他们有可选的模式语言,这有时是有用的,有时是一个障碍。这些格式对于数据类型有些模糊,所以你必须小心数字和二进制字符串。
-* 像Thrift、Protocol Buffers和Avro这样的二进制模式驱动格式允许使用清晰定义的前向和后向兼容性语义进行紧凑,高效的编码。这些模式可以用于静态类型语言的文档和代码生成。但是,他们有一个缺点,就是在数据可读之前需要对数据进行解码。
+* JSON、XML 和 CSV 等文本格式非常普遍,其兼容性取决于你如何使用它们。他们有可选的模式语言,这有时是有用的,有时是一个障碍。这些格式对于数据类型有些模糊,所以你必须小心数字和二进制字符串。
+* 像 Thrift、Protocol Buffers 和 Avro 这样的二进制模式驱动格式允许使用清晰定义的前向和后向兼容性语义进行紧凑,高效的编码。这些模式可以用于静态类型语言的文档和代码生成。但是,他们有一个缺点,就是在数据可读之前需要对数据进行解码。
 
 我们还讨论了数据流的几种模式,说明了数据编码重要性的不同场景:
 
 * 数据库,写入数据库的进程对数据进行编码,并从数据库读取进程对其进行解码
-* RPC和REST API,客户端对请求进行编码,服务器对请求进行解码并对响应进行编码,客户端最终对响应进行解码
+* RPC 和 REST API,客户端对请求进行编码,服务器对请求进行解码并对响应进行编码,客户端最终对响应进行解码
 * 异步消息传递(使用消息代理或参与者),其中节点之间通过发送消息进行通信,消息由发送者编码并由接收者解码
 
 我们可以小心地得出这样的结论:前向兼容性和滚动升级在某种程度上是可以实现的。愿你的应用程序的演变迅速、敏捷部署。
diff --git a/ch5.md b/ch5.md
index 9ecaad0..d3595b1 100644
--- a/ch5.md
+++ b/ch5.md
@@ -1,74 +1,74 @@
-# 第五章:复制 
+# 第五章:复制
 
 ![](img/ch5.png)
 
-> 与可能出错的东西比,'不可能'出错的东西最显著的特点就是:一旦真的出错,通常就彻底玩完了。
+> 与可能出错的东西比,“不可能”出错的东西最显著的特点就是:一旦真的出错,通常就彻底玩完了。
 >
-> —— 道格拉斯·亚当斯(1992)
+> —— 道格拉斯・亚当斯(1992)
 
 ------
 
 [TOC]
 
-复制意味着在通过网络连接的多台机器上保留相同数据的副本。正如在[第二部分](part-ii.md)的介绍中所讨论的那样,我们希望能复制数据,可能出于各种各样的原因:
+复制意味着在通过网络连接的多台机器上保留相同数据的副本。正如在 [第二部分](part-ii.md) 的介绍中所讨论的那样,我们希望能复制数据,可能出于各种各样的原因:
 
 * 使得数据与用户在地理上接近(从而减少延迟)
 * 即使系统的一部分出现故障,系统也能继续工作(从而提高可用性)
 * 伸缩可以接受读请求的机器数量(从而提高读取吞吐量)
 
-本章将假设你的数据集非常小,每台机器都可以保存整个数据集的副本。在[第六章](ch6.md)中将放宽这个假设,讨论对单个机器来说太大的数据集的分割(分片)。在后面的章节中,我们将讨论复制数据系统中可能发生的各种故障,以及如何处理这些故障。
+本章将假设你的数据集非常小,每台机器都可以保存整个数据集的副本。在 [第六章](ch6.md) 中将放宽这个假设,讨论对单个机器来说太大的数据集的分割(分片)。在后面的章节中,我们将讨论复制数据系统中可能发生的各种故障,以及如何处理这些故障。
 
-如果复制中的数据不会随时间而改变,那复制就很简单:将数据复制到每个节点一次就万事大吉。复制的困难之处在于处理复制数据的**变更(change)**,这就是本章所要讲的。我们将讨论三种流行的变更复制算法:**单领导者(single leader)**,**多领导者(multi leader)** 和**无领导者(leaderless)**。几乎所有分布式数据库都使用这三种方法之一。
+如果复制中的数据不会随时间而改变,那复制就很简单:将数据复制到每个节点一次就万事大吉。复制的困难之处在于处理复制数据的 **变更(change)**,这就是本章所要讲的。我们将讨论三种流行的变更复制算法:**单领导者(single leader)**,**多领导者(multi leader)** 和 **无领导者(leaderless)**。几乎所有分布式数据库都使用这三种方法之一。
 
 在复制时需要进行许多权衡:例如,使用同步复制还是异步复制?如何处理失败的副本?这些通常是数据库中的配置选项,细节因数据库而异,但原理在许多不同的实现中都类似。本章会讨论这些决策的后果。
 
-数据库的复制算得上是老生常谈了 ——70年代研究得出的基本原则至今没有太大变化【1】,因为网络的基本约束仍保持不变。然而在研究之外,许多开发人员仍然假设一个数据库只有一个节点。分布式数据库变为主流只是最近发生的事。许多程序员都是这一领域的新手,因此对于诸如 **最终一致性(eventual consistency)** 等问题存在许多误解。在“[复制延迟问题](#复制延迟问题)”一节,我们将更加精确地了解最终的一致性,并讨论诸如 **读己之写(read-your-writes)** 和 **单调读(monotonic read)** 保证等内容。
+数据库的复制算得上是老生常谈了 ——70 年代研究得出的基本原则至今没有太大变化【1】,因为网络的基本约束仍保持不变。然而在研究之外,许多开发人员仍然假设一个数据库只有一个节点。分布式数据库变为主流只是最近发生的事。许多程序员都是这一领域的新手,因此对于诸如 **最终一致性(eventual consistency)** 等问题存在许多误解。在 “[复制延迟问题](#复制延迟问题)” 一节,我们将更加精确地了解最终的一致性,并讨论诸如 **读己之写(read-your-writes)** 和 **单调读(monotonic read)** 保证等内容。
 
 ## 领导者与追随者
 
 存储数据库副本的每个节点称为 **副本(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)** 。当客户端要向数据库写入时,它必须将请求发送给**领导者**,领导者会将新数据写入其本地存储。
-2. 其他副本被称为**追随者(followers)**,亦称为**只读副本(read replicas)**,**从库(slaves)**,**备库( secondaries)**,**热备(hot-standby)**[^i]。每当领导者将新数据写入本地存储时,它也会将数据变更发送给所有的追随者,称之为**复制日志(replication log)** 记录或**变更流(change stream)**。每个跟随者从领导者拉取日志,并相应更新其本地数据库副本,方法是按照领导者处理的相同顺序应用所有写入。
+1. 副本之一被指定为 **领导者(leader)**,也称为 **主库(master|primary)** 。当客户端要向数据库写入时,它必须将请求发送给 **领导者**,领导者会将新数据写入其本地存储。
+2. 其他副本被称为 **追随者(followers)**,亦称为 **只读副本(read replicas)**,**从库(slaves)**,**备库( secondaries)**,**热备(hot-standby)**[^i]。每当领导者将新数据写入本地存储时,它也会将数据变更发送给所有的追随者,称之为 **复制日志(replication log)** 记录或 **变更流(change stream)**。每个跟随者从领导者拉取日志,并相应更新其本地数据库副本,方法是按照领导者处理的相同顺序应用所有写入。
 3. 当客户想要从数据库中读取数据时,它可以向领导者或追随者查询。 但只有领导者才能接受写操作(从客户端的角度来看从库都是只读的)。
 
-[^i]: 不同的人对 **热(hot)**,**温(warm)**,**冷(cold)** 备份服务器有不同的定义。 例如在PostgreSQL中,**热备(hot standby)** 指的是能接受客户端读请求的副本。而 **温备(warm standby)** 只是追随领导者,但不处理客户端的任何查询。 就本书而言,这些差异并不重要。
+[^i]: 不同的人对 **热(hot)**,**温(warm)**,**冷(cold)** 备份服务器有不同的定义。 例如在 PostgreSQL 中,**热备(hot standby)** 指的是能接受客户端读请求的副本。而 **温备(warm standby)** 只是追随领导者,但不处理客户端的任何查询。 就本书而言,这些差异并不重要。
 
 ![](img/fig5-1.png)
-**图5-1 基于领导者(主-从)的复制**
+**图 5-1 基于领导者 (主 - 从) 的复制**
 
-这种复制模式是许多关系数据库的内置功能,如PostgreSQL(从9.0版本开始),MySQL,Oracle Data Guard 【2】和SQL Server的AlwaysOn可用性组【3】。 它也被用于一些非关系数据库,包括MongoDB,RethinkDB和Espresso 【4】。 最后,基于领导者的复制并不仅限于数据库:像Kafka 【5】和RabbitMQ高可用队列【6】这样的分布式消息代理也使用它。 某些网络文件系统,例如DRBD这样的块复制设备也与之类似。
+这种复制模式是许多关系数据库的内置功能,如 PostgreSQL(从 9.0 版本开始),MySQL,Oracle Data Guard 【2】和 SQL Server 的 AlwaysOn 可用性组【3】。 它也被用于一些非关系数据库,包括 MongoDB,RethinkDB 和 Espresso 【4】。 最后,基于领导者的复制并不仅限于数据库:像 Kafka 【5】和 RabbitMQ 高可用队列【6】这样的分布式消息代理也使用它。 某些网络文件系统,例如 DRBD 这样的块复制设备也与之类似。
 
 ### 同步复制与异步复制
 
 复制系统的一个重要细节是:复制是 **同步(synchronously)** 发生还是 **异步(asynchronously)** 发生。 (在关系型数据库中这通常是一个配置项,其他系统通常硬编码为其中一个)。
 
-想象[图5-1](fig5-1.png)中发生的情况,网站的用户更新他们的个人头像。在某个时间点,客户向主库发送更新请求;不久之后主库就收到了请求。在某个时刻,主库又会将数据变更转发给自己的从库。最后,主库通知客户更新成功。
+想象 [图 5-1](fig5-1.png) 中发生的情况,网站的用户更新他们的个人头像。在某个时间点,客户向主库发送更新请求;不久之后主库就收到了请求。在某个时刻,主库又会将数据变更转发给自己的从库。最后,主库通知客户更新成功。
 
-[图5-2](img/fig5-2.png)显示了系统各个组件之间的通信:用户客户端,主库和两个从库。时间从左到右流动。请求或响应消息用粗箭头表示。
+[图 5-2](img/fig5-2.png) 显示了系统各个组件之间的通信:用户客户端,主库和两个从库。时间从左到右流动。请求或响应消息用粗箭头表示。
 
 ![](img/fig5-2.png)
-**图5-2 基于领导者的复制:一个同步从库和一个异步从库**
+**图 5-2 基于领导者的复制:一个同步从库和一个异步从库**
 
-在[图5-2](img/fig5-2.png)的示例中,从库1的复制是同步的:在向用户报告写入成功,并使结果对其他用户可见之前,主库需要等待从库1的确认,确保从库1已经收到写入操作。以及在使写入对其他客户端可见之前接收到写入。跟随者2的复制是异步的:主库发送消息,但不等待从库的响应。
+在 [图 5-2](img/fig5-2.png) 的示例中,从库 1 的复制是同步的:在向用户报告写入成功,并使结果对其他用户可见之前,主库需要等待从库 1 的确认,确保从库 1 已经收到写入操作。以及在使写入对其他客户端可见之前接收到写入。跟随者 2 的复制是异步的:主库发送消息,但不等待从库的响应。
 
-在这幅图中,从库2处理消息前存在一个显著的延迟。通常情况下,复制的速度相当快:大多数数据库系统能在一秒向从库应用变更,但它们不能提供复制用时的保证。有些情况下,从库可能落后主库几分钟或更久;例如:从库正在从故障中恢复,系统在最大容量附近运行,或者如果节点间存在网络问题。
+在这幅图中,从库 2 处理消息前存在一个显著的延迟。通常情况下,复制的速度相当快:大多数数据库系统能在一秒向从库应用变更,但它们不能提供复制用时的保证。有些情况下,从库可能落后主库几分钟或更久;例如:从库正在从故障中恢复,系统在最大容量附近运行,或者如果节点间存在网络问题。
 
 同步复制的优点是,从库保证有与主库一致的最新数据副本。如果主库突然失效,我们可以确信这些数据仍然能在从库上上找到。缺点是,如果同步从库没有响应(比如它已经崩溃,或者出现网络故障,或其它任何原因),主库就无法处理写入操作。主库必须阻止所有写入,并等待同步副本再次可用。
 
-因此,将所有从库都设置为同步的是不切实际的:任何一个节点的中断都会导致整个系统停滞不前。实际上,如果在数据库上启用同步复制,通常意味着其中**一个**跟随者是同步的,而其他的则是异步的。如果同步从库变得不可用或缓慢,则使一个异步从库同步。这保证你至少在两个节点上拥有最新的数据副本:主库和同步从库。 这种配置有时也被称为 **半同步(semi-synchronous)**【7】。
+因此,将所有从库都设置为同步的是不切实际的:任何一个节点的中断都会导致整个系统停滞不前。实际上,如果在数据库上启用同步复制,通常意味着其中 **一个** 跟随者是同步的,而其他的则是异步的。如果同步从库变得不可用或缓慢,则使一个异步从库同步。这保证你至少在两个节点上拥有最新的数据副本:主库和同步从库。 这种配置有时也被称为 **半同步(semi-synchronous)**【7】。
 
 通常情况下,基于领导者的复制都配置为完全异步。 在这种情况下,如果主库失效且不可恢复,则任何尚未复制给从库的写入都会丢失。 这意味着即使已经向客户端确认成功,写入也不能保证 **持久(Durable)** 。 然而,一个完全异步的配置也有优点:即使所有的从库都落后了,主库也可以继续处理写入。
 
-弱化的持久性可能听起来像是一个坏的折衷,然而异步复制已经被广泛使用了,特别当有很多追随者,或追随者异地分布时。 稍后将在“[复制延迟问题](#复制延迟问题)”中回到这个问题。
+弱化的持久性可能听起来像是一个坏的折衷,然而异步复制已经被广泛使用了,特别当有很多追随者,或追随者异地分布时。 稍后将在 “[复制延迟问题](#复制延迟问题)” 中回到这个问题。
 
 > ### 关于复制的研究
 >
-> 对于异步复制系统而言,主库故障时有可能丢失数据。这可能是一个严重的问题,因此研究人员仍在研究不丢数据但仍能提供良好性能和可用性的复制方法。 例如,**链式复制**【8,9】]是同步复制的一种变体,已经在一些系统(如Microsoft Azure存储【10,11】)中成功实现。
+> 对于异步复制系统而言,主库故障时有可能丢失数据。这可能是一个严重的问题,因此研究人员仍在研究不丢数据但仍能提供良好性能和可用性的复制方法。 例如,**链式复制**【8,9】] 是同步复制的一种变体,已经在一些系统(如 Microsoft Azure 存储【10,11】)中成功实现。
 >
-> 复制的一致性与**共识**(consensus,使几个节点就某个值达成一致)之间有着密切的联系,[第九章](ch9.md)将详细地探讨这一领域的理论。本章主要讨论实践中数据库常用的简单复制形式。
+> 复制的一致性与 **共识**(consensus,使几个节点就某个值达成一致)之间有着密切的联系,[第九章](ch9.md) 将详细地探讨这一领域的理论。本章主要讨论实践中数据库常用的简单复制形式。
 >
 
 ### 设置新从库
@@ -79,9 +79,9 @@
 
 可以通过锁定数据库(使其不可用于写入)来使磁盘上的文件保持一致,但是这会违背高可用的目标。幸运的是,拉起新的从库通常并不需要停机。从概念上讲,过程如下所示:
 
-1. 在某个时刻获取主库的一致性快照(如果可能),而不必锁定整个数据库。大多数数据库都具有这个功能,因为它是备份必需的。对于某些场景,可能需要第三方工具,例如MySQL的innobackupex 【12】。
+1. 在某个时刻获取主库的一致性快照(如果可能),而不必锁定整个数据库。大多数数据库都具有这个功能,因为它是备份必需的。对于某些场景,可能需要第三方工具,例如 MySQL 的 innobackupex 【12】。
 2. 将快照复制到新的从库节点。
-3. 从库连接到主库,并拉取快照之后发生的所有数据变更。这要求快照与主库复制日志中的位置精确关联。该位置有不同的名称:例如,PostgreSQL将其称为 **日志序列号(log sequence number, LSN)**,MySQL将其称为 **二进制日志坐标(binlog coordinates)**。
+3. 从库连接到主库,并拉取快照之后发生的所有数据变更。这要求快照与主库复制日志中的位置精确关联。该位置有不同的名称:例如,PostgreSQL 将其称为 **日志序列号(log sequence number, LSN)**,MySQL 将其称为 **二进制日志坐标(binlog coordinates)**。
 4. 当从库处理完快照之后积压的数据变更,我们说它 **赶上(caught up)** 了主库。现在它可以继续处理主库产生的数据变化了。
 
 建立从库的实际步骤因数据库而异。在某些系统中,这个过程是完全自动化的,而在另外一些系统中,它可能是一个需要由管理员手动执行的,有点神秘的多步骤工作流。
@@ -98,21 +98,21 @@
 
 #### 主库失效:故障切换
 
-主库失效处理起来相当棘手:其中一个从库需要被提升为新的主库,需要重新配置客户端,以将它们的写操作发送给新的主库,其他从库需要开始拉取来自新主库的数据变更。这个过程被称为**故障切换(failover)**。
+主库失效处理起来相当棘手:其中一个从库需要被提升为新的主库,需要重新配置客户端,以将它们的写操作发送给新的主库,其他从库需要开始拉取来自新主库的数据变更。这个过程被称为 **故障切换(failover)**。
 
 故障切换可以手动进行(通知管理员主库挂了,并采取必要的步骤来创建新的主库)或自动进行。自动故障切换过程通常由以下步骤组成:
 
-1. 确认主库失效。有很多事情可能会出错:崩溃,停电,网络问题等等。没有万无一失的方法来检测出现了什么问题,所以大多数系统只是简单使用 **超时(Timeout)** :节点频繁地相互来回传递消息,并且如果一个节点在一段时间内(例如30秒)没有响应,就认为它挂了(因为计划内维护而故意关闭主库不算)。
-2. 选择一个新的主库。这可以通过选举过程(主库由剩余副本以多数选举产生)来完成,或者可以由之前选定的**控制器节点(controller node)** 来指定新的主库。主库的最佳人选通常是拥有旧主库最新数据副本的从库(最小化数据损失)。让所有的节点同意一个新的领导者,是一个**共识**问题,将在[第九章](ch9.md)详细讨论。
-3. 重新配置系统以启用新的主库。客户端现在需要将它们的写请求发送给新主库(将在“[请求路由](ch6.md#请求路由)”中讨论这个问题)。如果旧主库恢复,可能仍然认为自己是主库,而没有意识到其他副本已经让它失去领导权了。系统需要确保旧主库意识到新主库的存在,并成为一个从库。
+1. 确认主库失效。有很多事情可能会出错:崩溃,停电,网络问题等等。没有万无一失的方法来检测出现了什么问题,所以大多数系统只是简单使用 **超时(Timeout)** :节点频繁地相互来回传递消息,并且如果一个节点在一段时间内(例如 30 秒)没有响应,就认为它挂了(因为计划内维护而故意关闭主库不算)。
+2. 选择一个新的主库。这可以通过选举过程(主库由剩余副本以多数选举产生)来完成,或者可以由之前选定的 **控制器节点(controller node)** 来指定新的主库。主库的最佳人选通常是拥有旧主库最新数据副本的从库(最小化数据损失)。让所有的节点同意一个新的领导者,是一个 **共识** 问题,将在 [第九章](ch9.md) 详细讨论。
+3. 重新配置系统以启用新的主库。客户端现在需要将它们的写请求发送给新主库(将在 “[请求路由](ch6.md#请求路由)” 中讨论这个问题)。如果旧主库恢复,可能仍然认为自己是主库,而没有意识到其他副本已经让它失去领导权了。系统需要确保旧主库意识到新主库的存在,并成为一个从库。
 
 故障切换会出现很多大麻烦:
 
 * 如果使用异步复制,则新主库可能没有收到老主库宕机前最后的写入操作。在选出新主库后,如果老主库重新加入集群,新主库在此期间可能会收到冲突的写入,那这些写入该如何处理?最常见的解决方案是简单丢弃老主库未复制的写入,这很可能打破客户对于数据持久性的期望。
 
-* 如果数据库需要和其他外部存储相协调,那么丢弃写入内容是极其危险的操作。例如在GitHub 【13】的一场事故中,一个过时的MySQL从库被提升为主库。数据库使用自增ID作为主键,因为新主库的计数器落后于老主库的计数器,所以新主库重新分配了一些已经被老主库分配掉的ID作为主键。这些主键也在Redis中使用,主键重用使得MySQL和Redis中数据产生不一致,最后导致一些私有数据泄漏到错误的用户手中。
+* 如果数据库需要和其他外部存储相协调,那么丢弃写入内容是极其危险的操作。例如在 GitHub 【13】的一场事故中,一个过时的 MySQL 从库被提升为主库。数据库使用自增 ID 作为主键,因为新主库的计数器落后于老主库的计数器,所以新主库重新分配了一些已经被老主库分配掉的 ID 作为主键。这些主键也在 Redis 中使用,主键重用使得 MySQL 和 Redis 中数据产生不一致,最后导致一些私有数据泄漏到错误的用户手中。
 
-* 发生某些故障时(见[第八章](ch8.md))可能会出现两个节点都以为自己是主库的情况。这种情况称为 **脑裂(split brain)**,非常危险:如果两个主库都可以接受写操作,却没有冲突解决机制(请参阅“[多主复制](#多主复制)”),那么数据就可能丢失或损坏。一些系统采取了安全防范措施:当检测到两个主库节点同时存在时会关闭其中一个节点[^ii],但设计粗糙的机制可能最后会导致两个节点都被关闭【14】。
+* 发生某些故障时(见 [第八章](ch8.md))可能会出现两个节点都以为自己是主库的情况。这种情况称为 **脑裂 (split brain)**,非常危险:如果两个主库都可以接受写操作,却没有冲突解决机制(请参阅 “[多主复制](#多主复制)”),那么数据就可能丢失或损坏。一些系统采取了安全防范措施:当检测到两个主库节点同时存在时会关闭其中一个节点 [^ii],但设计粗糙的机制可能最后会导致两个节点都被关闭【14】。
 
   [^ii]: 这种机制称为 **屏蔽(fencing)**,充满感情的术语是:**爆彼之头(Shoot The Other Node In The Head, STONITH)**。
 
@@ -120,7 +120,7 @@
 
 这些问题没有简单的解决方案。因此,即使软件支持自动故障切换,不少运维团队还是更愿意手动执行故障切换。
 
-节点故障、不可靠的网络、对副本一致性,持久性,可用性和延迟的权衡 ,这些问题实际上是分布式系统中的基本问题。[第八章](ch8.md)和[第九章](ch9.md)将更深入地讨论它们。
+节点故障、不可靠的网络、对副本一致性,持久性,可用性和延迟的权衡 ,这些问题实际上是分布式系统中的基本问题。[第八章](ch8.md) 和 [第九章](ch9.md) 将更深入地讨论它们。
 
 ### 复制日志的实现
 
@@ -128,32 +128,32 @@
 
 #### 基于语句的复制
 
-在最简单的情况下,主库记录下它执行的每个写入请求(**语句**,即statement)并将该语句日志发送给其从库。对于关系数据库来说,这意味着每个`INSERT`、`UPDATE`或`DELETE`语句都被转发给每个从库,每个从库解析并执行该SQL语句,就像从客户端收到一样。
+在最简单的情况下,主库记录下它执行的每个写入请求(**语句**,即 statement)并将该语句日志发送给其从库。对于关系数据库来说,这意味着每个 `INSERT`、`UPDATE` 或 `DELETE` 语句都被转发给每个从库,每个从库解析并执行该 SQL 语句,就像从客户端收到一样。
 
 虽然听上去很合理,但有很多问题会搞砸这种复制方式:
 
-* 任何调用 **非确定性函数(nondeterministic)** 的语句,可能会在每个副本上生成不同的值。例如,使用`NOW()`获取当前日期时间,或使用`RAND()`获取一个随机数。
-* 如果语句使用了**自增列(auto increment)**,或者依赖于数据库中的现有数据(例如,`UPDATE ... WHERE <某些条件>`),则必须在每个副本上按照完全相同的顺序执行它们,否则可能会产生不同的效果。当有多个并发执行的事务时,这可能成为一个限制。
-* 有副作用的语句(例如,触发器,存储过程,用户定义的函数)可能会在每个副本上产生不同的副作用,除非副作用是绝对确定的。
+* 任何调用 **非确定性函数(nondeterministic)** 的语句,可能会在每个副本上生成不同的值。例如,使用 `NOW()` 获取当前日期时间,或使用 `RAND()` 获取一个随机数。
+* 如果语句使用了 **自增列(auto increment)**,或者依赖于数据库中的现有数据(例如,`UPDATE ... WHERE <某些条件>`),则必须在每个副本上按照完全相同的顺序执行它们,否则可能会产生不同的效果。当有多个并发执行的事务时,这可能成为一个限制。
+* 有副作用的语句(例如:触发器、存储过程、用户定义的函数)可能会在每个副本上产生不同的副作用,除非副作用是绝对确定的。
 
-的确有办法绕开这些问题 ——例如,当语句被记录时,主库可以用固定的返回值替换任何不确定的函数调用,以便从库获得相同的值。但是由于边缘情况实在太多了,现在通常会选择其他的复制方法。
+的确有办法绕开这些问题 —— 例如,当语句被记录时,主库可以用固定的返回值替换任何不确定的函数调用,以便从库获得相同的值。但是由于边缘情况实在太多了,现在通常会选择其他的复制方法。
 
-基于语句的复制在5.1版本前的MySQL中使用。因为它相当紧凑,现在有时候也还在用。但现在在默认情况下,如果语句中存在任何不确定性,MySQL会切换到基于行的复制(稍后讨论)。 VoltDB使用了基于语句的复制,但要求事务必须是确定性的,以此来保证安全【15】。
+基于语句的复制在 5.1 版本前的 MySQL 中使用。因为它相当紧凑,现在有时候也还在用。但现在在默认情况下,如果语句中存在任何不确定性,MySQL 会切换到基于行的复制(稍后讨论)。 VoltDB 使用了基于语句的复制,但要求事务必须是确定性的,以此来保证安全【15】。
 
 #### 传输预写式日志(WAL)
 
-在[第三章](ch3.md)中,我们讨论了存储引擎如何在磁盘上表示数据,并且我们发现,通常写操作都是追加到日志中:
+在 [第三章](ch3.md) 中,我们讨论了存储引擎如何在磁盘上表示数据,并且我们发现,通常写操作都是追加到日志中:
 
-* 对于日志结构存储引擎(请参阅“[SSTables和LSM树](ch3.md#SSTables和LSM树)”),日志是主要的存储位置。日志段在后台压缩,并进行垃圾回收。
-* 对于覆写单个磁盘块的[B树](ch3.md#B树),每次修改都会先写入 **预写式日志(Write Ahead Log, WAL)**,以便崩溃后索引可以恢复到一个一致的状态。
+* 对于日志结构存储引擎(请参阅 “[SSTables 和 LSM 树](ch3.md#SSTables和LSM树)”),日志是主要的存储位置。日志段在后台压缩,并进行垃圾回收。
+* 对于覆写单个磁盘块的 [B 树](ch3.md#B树),每次修改都会先写入 **预写式日志(Write Ahead Log, WAL)**,以便崩溃后索引可以恢复到一个一致的状态。
 
 在任何一种情况下,日志都是包含所有数据库写入的仅追加字节序列。可以使用完全相同的日志在另一个节点上构建副本:除了将日志写入磁盘之外,主库还可以通过网络将其发送给其从库。
 
 当从库应用这个日志时,它会建立和主库一模一样数据结构的副本。
 
-PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志记录的数据非常底层:WAL包含哪些磁盘块中的哪些字节发生了更改。这使复制与存储引擎紧密耦合。如果数据库将其存储格式从一个版本更改为另一个版本,通常不可能在主库和从库上运行不同版本的数据库软件。
+PostgreSQL 和 Oracle 等使用这种复制方法【16】。主要缺点是日志记录的数据非常底层:WAL 包含哪些磁盘块中的哪些字节发生了更改。这使复制与存储引擎紧密耦合。如果数据库将其存储格式从一个版本更改为另一个版本,通常不可能在主库和从库上运行不同版本的数据库软件。
 
-看上去这可能只是一个微小的实现细节,但却可能对运维产生巨大的影响。如果复制协议允许从库使用比主库更新的软件版本,则可以先升级从库,然后执行故障切换,使升级后的节点之一成为新的主库,从而执行数据库软件的零停机升级。如果复制协议不允许版本不匹配(传输WAL经常出现这种情况),则此类升级需要停机。
+看上去这可能只是一个微小的实现细节,但却可能对运维产生巨大的影响。如果复制协议允许从库使用比主库更新的软件版本,则可以先升级从库,然后执行故障切换,使升级后的节点之一成为新的主库,从而执行数据库软件的零停机升级。如果复制协议不允许版本不匹配(传输 WAL 经常出现这种情况),则此类升级需要停机。
 
 #### 逻辑日志复制(基于行)
 
@@ -165,36 +165,36 @@ PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志
 * 对于删除的行,日志包含足够的信息来唯一标识已删除的行。通常是主键,但是如果表上没有主键,则需要记录所有列的旧值。
 * 对于更新的行,日志包含足够的信息来唯一标识更新的行,以及所有列的新值(或至少所有已更改的列的新值)。
 
-修改多行的事务会生成多个这样的日志记录,后面跟着一条记录,指出事务已经提交。 MySQL的二进制日志(当配置为使用基于行的复制时)使用这种方法【17】。
+修改多行的事务会生成多个这样的日志记录,后面跟着一条记录,指出事务已经提交。 MySQL 的二进制日志(当配置为使用基于行的复制时)使用这种方法【17】。
 
 由于逻辑日志与存储引擎内部分离,因此可以更容易地保持向后兼容,从而使领导者和跟随者能够运行不同版本的数据库软件甚至不同的存储引擎。
 
-对于外部应用程序来说,逻辑日志格式也更容易解析。如果要将数据库的内容发送到外部系统,这一点很有用,例如复制到数据仓库进行离线分析,或建立自定义索引和缓存【18】。 这种技术被称为 **数据变更捕获(change data capture)**,[第十一章](ch11.md)将重新讲到它。
+对于外部应用程序来说,逻辑日志格式也更容易解析。如果要将数据库的内容发送到外部系统,这一点很有用,例如复制到数据仓库进行离线分析,或建立自定义索引和缓存【18】。 这种技术被称为 **数据变更捕获(change data capture)**,[第十一章](ch11.md) 将重新讲到它。
 
 #### 基于触发器的复制
 
-到目前为止描述的复制方法是由数据库系统实现的,不涉及任何应用程序代码。在很多情况下,这就是你想要的。但在某些情况下需要更多的灵活性。例如,如果你只想复制数据的一个子集,或者想从一种数据库复制到另一种数据库,或者如果你需要冲突解决逻辑(请参阅“[处理写入冲突](#处理写入冲突)”),则可能需要将复制移动到应用程序层。
+到目前为止描述的复制方法是由数据库系统实现的,不涉及任何应用程序代码。在很多情况下,这就是你想要的。但在某些情况下需要更多的灵活性。例如,如果你只想复制数据的一个子集,或者想从一种数据库复制到另一种数据库,或者如果你需要冲突解决逻辑(请参阅 “[处理写入冲突](#处理写入冲突)”),则可能需要将复制移动到应用程序层。
 
-一些工具,如Oracle Golden Gate 【19】,可以通过读取数据库日志,使得其他应用程序可以使用数据。另一种方法是使用许多关系数据库自带的功能:触发器和存储过程。
+一些工具,如 Oracle Golden Gate 【19】,可以通过读取数据库日志,使得其他应用程序可以使用数据。另一种方法是使用许多关系数据库自带的功能:触发器和存储过程。
 
-触发器允许你注册在数据库系统中发生数据更改(写入事务)时自动执行的自定义应用程序代码。触发器有机会将更改记录到一个单独的表中,使用外部程序读取这个表,再加上任何业务逻辑处理,会后将数据变更复制到另一个系统去。例如,Databus for Oracle 【20】和Bucardo for Postgres 【21】就是这样工作的。
+触发器允许你注册在数据库系统中发生数据更改(写入事务)时自动执行的自定义应用程序代码。触发器有机会将更改记录到一个单独的表中,使用外部程序读取这个表,再加上任何业务逻辑处理,会后将数据变更复制到另一个系统去。例如,Databus for Oracle 【20】和 Bucardo for Postgres 【21】就是这样工作的。
 
 基于触发器的复制通常比其他复制方法具有更高的开销,并且比数据库的内置复制更容易出错,也有很多限制。然而由于其灵活性,仍然是很有用的。
 
 
 ## 复制延迟问题
 
-容忍节点故障只是需要复制的一个原因。正如在[第二部分](part-ii.md)的介绍中提到的,另一个原因是可伸缩性(处理比单个机器更多的请求)和延迟(让副本在地理位置上更接近用户)。
+容忍节点故障只是需要复制的一个原因。正如在 [第二部分](part-ii.md) 的介绍中提到的,另一个原因是可伸缩性(处理比单个机器更多的请求)和延迟(让副本在地理位置上更接近用户)。
 
-基于主库的复制要求所有写入都由单个节点处理,但只读查询可以由任何副本处理。所以对于读多写少的场景(Web上的常见模式),一个有吸引力的选择是创建很多从库,并将读请求分散到所有的从库上去。这样能减小主库的负载,并允许向最近的副本发送读请求。
+基于主库的复制要求所有写入都由单个节点处理,但只读查询可以由任何副本处理。所以对于读多写少的场景(Web 上的常见模式),一个有吸引力的选择是创建很多从库,并将读请求分散到所有的从库上去。这样能减小主库的负载,并允许向最近的副本发送读请求。
 
-在这种伸缩体系结构中,只需添加更多的追随者,就可以提高只读请求的服务容量。但是,这种方法实际上只适用于异步复制——如果尝试同步复制到所有追随者,则单个节点故障或网络中断将使整个系统无法写入。而且越多的节点越有可能会被关闭,所以完全同步的配置是非常不可靠的。
+在这种伸缩体系结构中,只需添加更多的追随者,就可以提高只读请求的服务容量。但是,这种方法实际上只适用于异步复制 —— 如果尝试同步复制到所有追随者,则单个节点故障或网络中断将使整个系统无法写入。而且越多的节点越有可能会被关闭,所以完全同步的配置是非常不可靠的。
 
-不幸的是,当应用程序从异步从库读取时,如果从库落后,它可能会看到过时的信息。这会导致数据库中出现明显的不一致:同时对主库和从库执行相同的查询,可能得到不同的结果,因为并非所有的写入都反映在从库中。这种不一致只是一个暂时的状态——如果停止写入数据库并等待一段时间,从库最终会赶上并与主库保持一致。出于这个原因,这种效应被称为 **最终一致性(eventual consistency)**[^iii]【22,23】
+不幸的是,当应用程序从异步从库读取时,如果从库落后,它可能会看到过时的信息。这会导致数据库中出现明显的不一致:同时对主库和从库执行相同的查询,可能得到不同的结果,因为并非所有的写入都反映在从库中。这种不一致只是一个暂时的状态 —— 如果停止写入数据库并等待一段时间,从库最终会赶上并与主库保持一致。出于这个原因,这种效应被称为 **最终一致性(eventual consistency)**[^iii]【22,23】
 
-[^iii]: 道格拉斯·特里(Douglas Terry)等人创造了术语最终一致性。 【24】 并经由Werner Vogels 【22】推广,成为许多NoSQL项目的口号。 然而,不只有NoSQL数据库是最终一致的:关系型数据库中的异步复制追随者也有相同的特性。
+[^iii]: 道格拉斯・特里(Douglas Terry)等人创造了术语最终一致性。 【24】 并经由 Werner Vogels 【22】推广,成为许多 NoSQL 项目的口号。 然而,不只有 NoSQL 数据库是最终一致的:关系型数据库中的异步复制追随者也有相同的特性。
 
-“最终”一词故意含糊不清:总的来说,副本落后的程度是没有限制的。在正常的操作中,**复制延迟(replication lag)**,即写入主库到反映至从库之间的延迟,可能仅仅是几分之一秒,在实践中并不显眼。但如果系统在接近极限的情况下运行,或网络中存在问题,延迟可以轻而易举地超过几秒,甚至几分钟。
+“最终” 一词故意含糊不清:总的来说,副本落后的程度是没有限制的。在正常的操作中,**复制延迟(replication lag)**,即写入主库到反映至从库之间的延迟,可能仅仅是几分之一秒,在实践中并不显眼。但如果系统在接近极限的情况下运行,或网络中存在问题,延迟可以轻而易举地超过几秒,甚至几分钟。
 
 因为滞后时间太长引入的不一致性,可不仅是一个理论问题,更是应用设计中会遇到的真实问题。本节将重点介绍三个由复制延迟问题的例子,并简述解决这些问题的一些方法。
 
@@ -202,27 +202,27 @@ PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志
 
 许多应用让用户提交一些数据,然后查看他们提交的内容。可能是用户数据库中的记录,也可能是对讨论主题的评论,或其他类似的内容。提交新数据时,必须将其发送给领导者,但是当用户查看数据时,可以从追随者读取。如果数据经常被查看,但只是偶尔写入,这是非常合适的。
 
-但对于异步复制,问题就来了。如[图5-3](fig5-3.png)所示:如果用户在写入后马上就查看数据,则新数据可能尚未到达副本。对用户而言,看起来好像是刚提交的数据丢失了,用户会不高兴,可以理解。
+但对于异步复制,问题就来了。如 [图 5-3](fig5-3.png) 所示:如果用户在写入后马上就查看数据,则新数据可能尚未到达副本。对用户而言,看起来好像是刚提交的数据丢失了,用户会不高兴,可以理解。
 
 ![](img/fig5-3.png)
 
-**图5-3 用户写入后从旧副本中读取数据。需要写后读(read-after-write)的一致性来防止这种异常**
+**图 5-3 用户写入后从旧副本中读取数据。需要写后读 (read-after-write) 的一致性来防止这种异常**
 
 在这种情况下,我们需要 **读写一致性(read-after-write consistency)**,也称为 **读己之写一致性(read-your-writes consistency)**【24】。这是一个保证,如果用户重新加载页面,他们总会看到他们自己提交的任何更新。它不会对其他用户的写入做出承诺:其他用户的更新可能稍等才会看到。它保证用户自己的输入已被正确保存。
 
 如何在基于领导者的复制系统中实现读后一致性?有各种可能的技术,这里说一些:
 
-* 读用户**可能已经修改过**的内容时,都从主库读;这就要求有一些方法,不用实际查询就可以知道用户是否修改了某些东西。举个例子,社交网络上的用户个人资料信息通常只能由用户本人编辑,而不能由其他人编辑。因此一个简单的规则是:从主库读取用户自己的档案,在从库读取其他用户的档案。
+* 读用户 **可能已经修改过** 的内容时,都从主库读;这就要求有一些方法,不用实际查询就可以知道用户是否修改了某些东西。举个例子,社交网络上的用户个人资料信息通常只能由用户本人编辑,而不能由其他人编辑。因此一个简单的规则是:从主库读取用户自己的档案,在从库读取其他用户的档案。
 
 * 如果应用中的大部分内容都可能被用户编辑,那这种方法就没用了,因为大部分内容都必须从主库读取(扩容读就没效果了)。在这种情况下可以使用其他标准来决定是否从主库读取。例如可以跟踪上次更新的时间,在上次更新后的一分钟内,从主库读。还可以监控从库的复制延迟,防止对任意比主库滞后超过一分钟的从库发出查询。
 
 * 客户端可以记住最近一次写入的时间戳,系统需要确保从库为该用户提供任何查询时,该时间戳前的变更都已经传播到了本从库中。如果当前从库不够新,则可以从另一个从库读,或者等待从库追赶上来。
 
-  时间戳可以是逻辑时间戳(指示写入顺序的东西,例如日志序列号)或实际系统时钟(在这种情况下,时钟同步变得至关重要;请参阅“[不可靠的时钟](ch8.md#不可靠的时钟)”)。
+  时间戳可以是逻辑时间戳(指示写入顺序的东西,例如日志序列号)或实际系统时钟(在这种情况下,时钟同步变得至关重要;请参阅 “[不可靠的时钟](ch8.md#不可靠的时钟)”)。
 
 * 如果你的副本分布在多个数据中心(出于可用性目的与用户尽量在地理上接近),则会增加复杂性。任何需要由领导者提供服务的请求都必须路由到包含主库的数据中心。
 
-另一种复杂的情况是:如果同一个用户从多个设备请求服务,例如桌面浏览器和移动APP。这种情况下可能就需要提供跨设备的写后读一致性:如果用户在某个设备上输入了一些信息,然后在另一个设备上查看,则应该看到他们刚输入的信息。
+另一种复杂的情况是:如果同一个用户从多个设备请求服务,例如桌面浏览器和移动 APP。这种情况下可能就需要提供跨设备的写后读一致性:如果用户在某个设备上输入了一些信息,然后在另一个设备上查看,则应该看到他们刚输入的信息。
 
 在这种情况下,还有一些需要考虑的问题:
 
@@ -234,57 +234,57 @@ PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志
 
 从异步从库读取第二个异常例子是,用户可能会遇到 **时光倒流(moving backward in time)**。
 
-如果用户从不同从库进行多次读取,就可能发生这种情况。例如,[图5-4](img/fig5-4.png)显示了用户2345两次进行相同的查询,首先查询了一个延迟很小的从库,然后是一个延迟较大的从库(如果用户刷新网页,而每个请求被路由到一个随机的服务器,这种情况是很有可能的)。第一个查询返回最近由用户1234添加的评论,但是第二个查询不返回任何东西,因为滞后的从库还没有拉取写入内容。在效果上相比第一个查询,第二个查询是在更早的时间点来观察系统。如果第一个查询没有返回任何内容,那问题并不大,因为用户2345可能不知道用户1234最近添加了评论。但如果用户2345先看见用户1234的评论,然后又看到它消失,那么对于用户2345,就很让人头大了。
+如果用户从不同从库进行多次读取,就可能发生这种情况。例如,[图 5-4](img/fig5-4.png) 显示了用户 2345 两次进行相同的查询,首先查询了一个延迟很小的从库,然后是一个延迟较大的从库(如果用户刷新网页,而每个请求被路由到一个随机的服务器,这种情况是很有可能的)。第一个查询返回最近由用户 1234 添加的评论,但是第二个查询不返回任何东西,因为滞后的从库还没有拉取写入内容。在效果上相比第一个查询,第二个查询是在更早的时间点来观察系统。如果第一个查询没有返回任何内容,那问题并不大,因为用户 2345 可能不知道用户 1234 最近添加了评论。但如果用户 2345 先看见用户 1234 的评论,然后又看到它消失,那么对于用户 2345,就很让人头大了。
 
 ![](img/fig5-4.png)
 
-**图5-4 用户首先从新副本读取,然后从旧副本读取。时光倒流。为了防止这种异常,我们需要单调的读取。**
+**图 5-4 用户首先从新副本读取,然后从旧副本读取。时光倒流。为了防止这种异常,我们需要单调的读取。**
 
 **单调读(Monotonic reads)**【23】保证这种异常不会发生。这是一个比 **强一致性(strong consistency)** 更弱,但比 **最终一致性(eventual consistency)** 更强的保证。当读取数据时,你可能会看到一个旧值;单调读取仅意味着如果一个用户顺序地进行多次读取,则他们不会看到时间后退,即,如果先前读取到较新的数据,后续读取不会得到更旧的数据。
 
-实现单调读取的一种方式是确保每个用户总是从同一个副本进行读取(不同的用户可以从不同的副本读取)。例如,可以基于用户ID的散列来选择副本,而不是随机选择副本。但是,如果该副本失败,用户的查询将需要重新路由到另一个副本。
+实现单调读取的一种方式是确保每个用户总是从同一个副本进行读取(不同的用户可以从不同的副本读取)。例如,可以基于用户 ID 的散列来选择副本,而不是随机选择副本。但是,如果该副本失败,用户的查询将需要重新路由到另一个副本。
 
 
 ### 一致前缀读
 
-第三个复制延迟例子违反了因果律。 想象一下Poons先生和Cake夫人之间的以下简短对话:
+第三个复制延迟例子违反了因果律。 想象一下 Poons 先生和 Cake 夫人之间的以下简短对话:
 
 *Mr. Poons*
 > Mrs. Cake,你能看到多远的未来?
 
 *Mrs. Cake*
-> 通常约十秒钟,Mr. Poons. 
+> 通常约十秒钟,Mr. Poons.
 
-这两句话之间有因果关系:Cake夫人听到了Poons先生的问题并回答了这个问题。
+这两句话之间有因果关系:Cake 夫人听到了 Poons 先生的问题并回答了这个问题。
 
-现在,想象第三个人正在通过从库来听这个对话。 Cake夫人说的内容是从一个延迟很低的从库读取的,但Poons先生所说的内容,从库的延迟要大的多(见[图5-5](img/fig5-5.png))。 于是,这个观察者会听到以下内容:
+现在,想象第三个人正在通过从库来听这个对话。 Cake 夫人说的内容是从一个延迟很低的从库读取的,但 Poons 先生所说的内容,从库的延迟要大的多(见 [图 5-5](img/fig5-5.png))。 于是,这个观察者会听到以下内容:
 
 *Mrs. Cake*
-> 通常约十秒钟,Mr. Poons. 
+> 通常约十秒钟,Mr. Poons.
 
 *Mr. Poons*
 > Mrs. Cake,你能看到多远的未来?
 
-对于观察者来说,看起来好像Cake夫人在Poons先生提问前就回答了这个问题。
+对于观察者来说,看起来好像 Cake 夫人在 Poons 先生提问前就回答了这个问题。
  这种超能力让人印象深刻,但也会把人搞糊涂。【25】。
 
 ![](img/fig5-5.png)
 
-**图5-5 如果某些分区的复制速度慢于其他分区,那么观察者在看到问题之前可能会看到答案。**
+**图 5-5 如果某些分区的复制速度慢于其他分区,那么观察者在看到问题之前可能会看到答案。**
 
 防止这种异常,需要另一种类型的保证:**一致前缀读(consistent prefix reads)**【23】。 这个保证说:如果一系列写入按某个顺序发生,那么任何人读取这些写入时,也会看见它们以同样的顺序出现。
 
-这是**分区(partitioned)** 或 **分片(sharded)** 数据库中的一个特殊问题,将在[第六章](ch6.md)中讨论。如果数据库总是以相同的顺序应用写入,则读取总是会看到一致的前缀,所以这种异常不会发生。但是在许多分布式数据库中,不同的分区独立运行,因此不存在**全局写入顺序**:当用户从数据库中读取数据时,可能会看到数据库的某些部分处于较旧的状态,而某些处于较新的状态。
+这是 **分区(partitioned)** 或 **分片(sharded)** 数据库中的一个特殊问题,将在 [第六章](ch6.md) 中讨论。如果数据库总是以相同的顺序应用写入,则读取总是会看到一致的前缀,所以这种异常不会发生。但是在许多分布式数据库中,不同的分区独立运行,因此不存在 **全局写入顺序**:当用户从数据库中读取数据时,可能会看到数据库的某些部分处于较旧的状态,而某些处于较新的状态。
 
-一种解决方案是,确保任何因果相关的写入都写入相同的分区。对于某些无法高效完成这种操作的应用,还有一些显式跟踪因果依赖关系的算法,本书将在“[“此前发生”的关系和并发](#“此前发生”的关系和并发)”一节中返回这个主题。
+一种解决方案是,确保任何因果相关的写入都写入相同的分区。对于某些无法高效完成这种操作的应用,还有一些显式跟踪因果依赖关系的算法,本书将在 “[“此前发生” 的关系和并发](#“此前发生”的关系和并发)” 一节中返回这个主题。
 
 ### 复制延迟的解决方案
 
-在使用最终一致的系统时,如果复制延迟增加到几分钟甚至几小时,则应该考虑应用程序的行为。如果答案是“没问题”,那很好。但如果结果对于用户来说是不好体验,那么设计系统来提供更强的保证是很重要的,例如**写后读**。明明是异步复制却假设复制是同步的,这是很多麻烦的根源。
+在使用最终一致的系统时,如果复制延迟增加到几分钟甚至几小时,则应该考虑应用程序的行为。如果答案是 “没问题”,那很好。但如果结果对于用户来说是不好体验,那么设计系统来提供更强的保证是很重要的,例如 **写后读**。明明是异步复制却假设复制是同步的,这是很多麻烦的根源。
 
 如前所述,应用程序可以提供比底层数据库更强有力的保证,例如通过主库进行某种读取。但在应用程序代码中处理这些问题是复杂的,容易出错。
 
-如果应用程序开发人员不必担心微妙的复制问题,并可以信赖他们的数据库“做了正确的事情”,那该多好呀。这就是 **事务(transaction)** 存在的原因:**数据库通过事务提供强大的保证**,所以应用程序可以更加简单。
+如果应用程序开发人员不必担心微妙的复制问题,并可以信赖他们的数据库 “做了正确的事情”,那该多好呀。这就是 **事务(transaction)** 存在的原因:**数据库通过事务提供强大的保证**,所以应用程序可以更加简单。
 
 单节点事务已经存在了很长时间。然而在走向分布式(复制和分区)数据库时,许多系统放弃了事务。声称事务在性能和可用性上的代价太高,并断言在可伸缩系统中最终一致性是不可避免的。这个叙述有一些道理,但过于简单了,本书其余部分将提出更为细致的观点。第七章和第九章将回到事务的话题,并讨论一些替代机制。
 
@@ -293,11 +293,11 @@ PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志
 
 本章到目前为止,我们只考虑使用单个领导者的复制架构。 虽然这是一种常见的方法,但也有一些有趣的选择。
 
-基于领导者的复制有一个主要的缺点:只有一个主库,而所有的写入都必须通过它[^iv]。如果出于任何原因(例如和主库之间的网络连接中断)无法连接到主库, 就无法向数据库写入。
+基于领导者的复制有一个主要的缺点:只有一个主库,而所有的写入都必须通过它 [^iv]。如果出于任何原因(例如和主库之间的网络连接中断)无法连接到主库, 就无法向数据库写入。
 
-[^iv]: 如果数据库被分区(见[第六章](ch6.md)),每个分区都有一个领导。 不同的分区可能在不同的节点上有其领导者,但是每个分区必须有一个领导者节点。
+[^iv]: 如果数据库被分区(见 [第六章](ch6.md)),每个分区都有一个领导。 不同的分区可能在不同的节点上有其领导者,但是每个分区必须有一个领导者节点。
 
-基于领导者的复制模型的自然延伸是允许多个节点接受写入。 复制仍然以同样的方式发生:处理写入的每个节点都必须将该数据更改转发给所有其他节点。 称之为**多领导者配置**(也称多主、多活复制)。 在这种情况下,每个领导者同时扮演其他领导者的追随者。
+基于领导者的复制模型的自然延伸是允许多个节点接受写入。 复制仍然以同样的方式发生:处理写入的每个节点都必须将该数据更改转发给所有其他节点。 称之为 **多领导者配置**(也称多主、多活复制)。 在这种情况下,每个领导者同时扮演其他领导者的追随者。
 
 ### 多主复制的应用场景
 
@@ -307,11 +307,11 @@ PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志
 
 假如你有一个数据库,副本分散在好几个不同的数据中心(也许这样可以容忍单个数据中心的故障,或地理上更接近用户)。 使用常规的基于领导者的复制设置,主库必须位于其中一个数据中心,且所有写入都必须经过该数据中心。
 
-多领导者配置中可以在每个数据中心都有主库。 [图5-6](img/fig5-6.png)展示了这个架构的样子。 在每个数据中心内使用常规的主从复制;在数据中心之间,每个数据中心的主库都会将其更改复制到其他数据中心的主库中。 
+多领导者配置中可以在每个数据中心都有主库。 [图 5-6](img/fig5-6.png) 展示了这个架构的样子。 在每个数据中心内使用常规的主从复制;在数据中心之间,每个数据中心的主库都会将其更改复制到其他数据中心的主库中。
 
 ![](img/fig5-6.png)
 
-**图5-6 跨多个数据中心的多主复制**
+**图 5-6 跨多个数据中心的多主复制**
 
 我们来比较一下在运维多个数据中心时,单主和多主的适应情况。
 
@@ -327,9 +327,9 @@ PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志
 
   数据中心之间的通信通常穿过公共互联网,这可能不如数据中心内的本地网络可靠。单主配置对这数据中心间的连接问题非常敏感,因为通过这个连接进行的写操作是同步的。采用异步复制功能的多主配置通常能更好地承受网络问题:临时的网络中断并不会妨碍正在处理的写入。
 
-有些数据库默认情况下支持多主配置,但使用外部工具实现也很常见,例如用于MySQL的Tungsten Replicator 【26】,用于PostgreSQL的BDR【27】以及用于Oracle的GoldenGate 【19】。
+有些数据库默认情况下支持多主配置,但使用外部工具实现也很常见,例如用于 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】。
 
@@ -341,13 +341,13 @@ PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志
 
 在这种情况下,每个设备都有一个充当领导者的本地数据库(它接受写请求),并且在所有设备上的日历副本之间同步时,存在异步的多主复制过程。复制延迟可能是几小时甚至几天,具体取决于何时可以访问互联网。
 
-从架构的角度来看,这种设置实际上与数据中心之间的多领导者复制类似,每个设备都是一个“数据中心”,而它们之间的网络连接是极度不可靠的。从历史上各类日历同步功能的破烂实现可以看出,想把多活配好是多么困难的一件事。
+从架构的角度来看,这种设置实际上与数据中心之间的多领导者复制类似,每个设备都是一个 “数据中心”,而它们之间的网络连接是极度不可靠的。从历史上各类日历同步功能的破烂实现可以看出,想把多活配好是多么困难的一件事。
 
-有一些工具旨在使这种多领导者配置更容易。例如,CouchDB就是为这种操作模式而设计的【29】。
+有一些工具旨在使这种多领导者配置更容易。例如,CouchDB 就是为这种操作模式而设计的【29】。
 
 #### 协同编辑
 
-实时协作编辑应用程序允许多个人同时编辑文档。例如,Etherpad 【30】和Google Docs 【31】允许多人同时编辑文本文档或电子表格(该算法在“[自动冲突解决](#自动冲突解决)”中简要讨论)。我们通常不会将协作式编辑视为数据库复制问题,但与前面提到的离线编辑用例有许多相似之处。当一个用户编辑文档时,所做的更改将立即应用到其本地副本(Web浏览器或客户端应用程序中的文档状态),并异步复制到服务器和编辑同一文档的任何其他用户。
+实时协作编辑应用程序允许多个人同时编辑文档。例如,Etherpad 【30】和 Google Docs 【31】允许多人同时编辑文本文档或电子表格(该算法在 “[自动冲突解决](#自动冲突解决)” 中简要讨论)。我们通常不会将协作式编辑视为数据库复制问题,但与前面提到的离线编辑用例有许多相似之处。当一个用户编辑文档时,所做的更改将立即应用到其本地副本(Web 浏览器或客户端应用程序中的文档状态),并异步复制到服务器和编辑同一文档的任何其他用户。
 
 如果要保证不会发生编辑冲突,则应用程序必须先取得文档的锁定,然后用户才能对其进行编辑。如果另一个用户想要编辑同一个文档,他们首先必须等到第一个用户提交修改并释放锁定。这种协作模式相当于主从复制模型下在主节点上执行事务操作。
 
@@ -357,11 +357,11 @@ PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志
 
 多领导者复制的最大问题是可能发生写冲突,这意味着需要解决冲突。
 
-例如,考虑一个由两个用户同时编辑的维基页面,如[图5-7](img/fig5-7.png)所示。用户1将页面的标题从A更改为B,并且用户2同时将标题从A更改为C。每个用户的更改已成功应用到其本地主库。但当异步复制时,会发现冲突【33】。单主数据库中不会出现此问题。
+例如,考虑一个由两个用户同时编辑的维基页面,如 [图 5-7](img/fig5-7.png) 所示。用户 1 将页面的标题从 A 更改为 B,并且用户 2 同时将标题从 A 更改为 C。每个用户的更改已成功应用到其本地主库。但当异步复制时,会发现冲突【33】。单主数据库中不会出现此问题。
 
 ![](img/fig5-7.png)
 
-**图5-7 两个主库同时更新同一记录引起的写入冲突**
+**图 5-7 两个主库同时更新同一记录引起的写入冲突**
 
 #### 同步与异步冲突检测
 
@@ -373,23 +373,23 @@ PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志
 
 处理冲突的最简单的策略就是避免它们:如果应用程序可以确保特定记录的所有写入都通过同一个领导者,那么冲突就不会发生。由于许多的多领导者复制实现在处理冲突时处理得相当不好,避免冲突是一个经常推荐的方法【34】。
 
-例如,在用户可以编辑自己的数据的应用程序中,可以确保来自特定用户的请求始终路由到同一数据中心,并使用该数据中心的领导者进行读写。不同的用户可能有不同的“家庭”数据中心(可能根据用户的地理位置选择),但从任何用户的角度来看,配置基本上都是单一的领导者。
+例如,在用户可以编辑自己的数据的应用程序中,可以确保来自特定用户的请求始终路由到同一数据中心,并使用该数据中心的领导者进行读写。不同的用户可能有不同的 “家庭” 数据中心(可能根据用户的地理位置选择),但从任何用户的角度来看,配置基本上都是单一的领导者。
 
-但是,有时你可能需要更改指定的记录的主库——可能是因为一个数据中心出现故障,你需要将流量重新路由到另一个数据中心,或者可能是因为用户已经迁移到另一个位置,现在更接近不同的数据中心。在这种情况下,冲突避免会中断,你必须处理不同主库同时写入的可能性。
+但是,有时你可能需要更改指定的记录的主库 —— 可能是因为一个数据中心出现故障,你需要将流量重新路由到另一个数据中心,或者可能是因为用户已经迁移到另一个位置,现在更接近不同的数据中心。在这种情况下,冲突避免会中断,你必须处理不同主库同时写入的可能性。
 
 #### 收敛至一致的状态
 
 单主数据库按顺序进行写操作:如果同一个字段有多个更新,则最后一个写操作将决定该字段的最终值。
 
-在多主配置中,没有明确的写入顺序,所以最终值应该是什么并不清楚。在[图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)** 的方式解决冲突,这意味着所有副本必须在所有变更复制完成时收敛至一个相同的最终值。
 
 实现冲突合并解决有多种途径:
 
-* 给每个写入一个唯一的ID(例如,一个时间戳,一个长的随机数,一个UUID或者一个键和值的哈希),挑选最高ID的写入作为胜利者,并丢弃其他写入。如果使用时间戳,这种技术被称为**最后写入胜利(LWW, last write wins)**。虽然这种方法很流行,但是很容易造成数据丢失【35】。我们将在本章末尾的[检测并发写入](#检测并发写入)更详细地讨论LWW。
-* 为每个副本分配一个唯一的ID,ID编号更高的写入具有更高的优先级。这种方法也意味着数据丢失。
-* 以某种方式将这些值合并在一起 - 例如,按字母顺序排序,然后连接它们(在[图5-7](img/fig5-7.png)中,合并的标题可能类似于“B/C”)。
+* 给每个写入一个唯一的 ID(例如,一个时间戳,一个长的随机数,一个 UUID 或者一个键和值的哈希),挑选最高 ID 的写入作为胜利者,并丢弃其他写入。如果使用时间戳,这种技术被称为 **最后写入胜利(LWW, last write wins)**。虽然这种方法很流行,但是很容易造成数据丢失【35】。我们将在本章末尾的 [检测并发写入](#检测并发写入) 更详细地讨论 LWW。
+* 为每个副本分配一个唯一的 ID,ID 编号更高的写入具有更高的优先级。这种方法也意味着数据丢失。
+* 以某种方式将这些值合并在一起 - 例如,按字母顺序排序,然后连接它们(在 [图 5-7](img/fig5-7.png) 中,合并的标题可能类似于 “B/C”)。
 * 用一种可保留所有信息的显式数据结构来记录冲突,并编写解决冲突的应用程序代码(也许通过提示用户的方式)。
 
 
@@ -399,13 +399,13 @@ PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志
 
 * 写时执行
 
-  只要数据库系统检测到复制更改日志中存在冲突,就会调用冲突处理程序。例如,Bucardo允许你为此编写一段Perl代码。这个处理程序通常不能提示用户——它在后台进程中运行,并且必须快速执行。
+  只要数据库系统检测到复制更改日志中存在冲突,就会调用冲突处理程序。例如,Bucardo 允许你为此编写一段 Perl 代码。这个处理程序通常不能提示用户 —— 它在后台进程中运行,并且必须快速执行。
 
 * 读时执行
 
-  当检测到冲突时,所有冲突写入被存储。下一次读取数据时,会将这些多个版本的数据返回给应用程序。应用程序可能会提示用户或自动解决冲突,并将结果写回数据库。例如,CouchDB以这种方式工作。
+  当检测到冲突时,所有冲突写入被存储。下一次读取数据时,会将这些多个版本的数据返回给应用程序。应用程序可能会提示用户或自动解决冲突,并将结果写回数据库。例如,CouchDB 以这种方式工作。
 
-请注意,冲突解决通常适用于单个行或文档层面,而不是整个事务【36】。因此,如果你有一个事务会原子性地进行几次不同的写入(请参阅[第七章](ch7.md),对于冲突解决而言,每个写入仍需分开单独考虑。
+请注意,冲突解决通常适用于单个行或文档层面,而不是整个事务【36】。因此,如果你有一个事务会原子性地进行几次不同的写入(请参阅 [第七章](ch7.md),对于冲突解决而言,每个写入仍需分开单独考虑。
 
 
 > #### 自动冲突解决
@@ -414,9 +414,9 @@ PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志
 >
 > 已经有一些有趣的研究来自动解决由于数据修改引起的冲突。有几行研究值得一提:
 >
-> * **无冲突复制数据类型(Conflict-free replicated datatypes,CRDT)**【32,38】是可以由多个用户同时编辑的集合,映射,有序列表,计数器等的一系列数据结构,它们以合理的方式自动解决冲突。一些CRDT已经在Riak 2.0中实现【39,40】。
-> * **可合并的持久数据结构(Mergeable persistent data structures)**【41】显式跟踪历史记录,类似于Git版本控制系统,并使用三向合并功能(而CRDT使用双向合并)。
-> * **可执行的转换(operational transformation)**[42]是Etherpad 【30】和Google Docs 【31】等合作编辑应用背后的冲突解决算法。它是专为同时编辑项目的有序列表而设计的,例如构成文本文档的字符列表。
+> * **无冲突复制数据类型(Conflict-free replicated datatypes,CRDT)**【32,38】是可以由多个用户同时编辑的集合,映射,有序列表,计数器等的一系列数据结构,它们以合理的方式自动解决冲突。一些 CRDT 已经在 Riak 2.0 中实现【39,40】。
+> * **可合并的持久数据结构(Mergeable persistent data structures)**【41】显式跟踪历史记录,类似于 Git 版本控制系统,并使用三向合并功能(而 CRDT 使用双向合并)。
+> * **可执行的转换(operational transformation)**[42] 是 Etherpad 【30】和 Google Docs 【31】等合作编辑应用背后的冲突解决算法。它是专为同时编辑项目的有序列表而设计的,例如构成文本文档的字符列表。
 >
 > 这些算法在数据库中的实现还很年轻,但很可能将来它们将被集成到更多的复制数据系统中。自动冲突解决方案可以使应用程序处理多领导者数据同步更为简单。
 >
@@ -424,51 +424,51 @@ PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志
 
 #### 什么是冲突?
 
-有些冲突是显而易见的。在[图5-7](img/fig5-7.png)的例子中,两个写操作并发地修改了同一条记录中的同一个字段,并将其设置为两个不同的值。毫无疑问这是一个冲突。
+有些冲突是显而易见的。在 [图 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]。一个指定的根节点将写入转发给所有其他节点。星形拓扑可以推广到树。
+最普遍的拓扑是全部到全部([图 5-8 (c)](img/fig5-8.png)),其中每个领导者将其写入每个其他领导。但是,也会使用更多受限制的拓扑:例如,默认情况下,MySQL 仅支持 **环形拓扑(circular topology)**【34】,其中每个节点接收来自一个节点的写入,并将这些写入(加上自己的任何写入)转发给另一个节点。另一种流行的拓扑结构具有星形的形状 [^v]。一个指定的根节点将写入转发给所有其他节点。星形拓扑可以推广到树。
 
-[^v]: 不要与星型模式混淆(请参阅“[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”),其中描述了数据模型的结构,而不是节点之间的通信拓扑。
+[^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】。
 
 如果你正在使用具有多领导者复制功能的系统,那么应该了解这些问题,仔细阅读文档,并彻底测试你的数据库,以确保它确实提供了你认为具有的保证。
 
 
 ## 无主复制
 
-我们在本章到目前为止所讨论的复制方法 ——单主复制、多主复制——都是这样的想法:客户端向一个主库发送写请求,而数据库系统负责将写入复制到其他副本。主库决定写入的顺序,而从库按相同顺序应用主库的写入。
+我们在本章到目前为止所讨论的复制方法 —— 单主复制、多主复制 —— 都是这样的想法:客户端向一个主库发送写请求,而数据库系统负责将写入复制到其他副本。主库决定写入的顺序,而从库按相同顺序应用主库的写入。
 
-一些数据存储系统采用不同的方法,放弃主库的概念,并允许任何副本直接接受来自客户端的写入。最早的一些的复制数据系统是**无领导的(leaderless)**【1,44】,但是在关系数据库主导的时代,这个想法几乎已被忘却。在亚马逊将其用于其内部的Dynamo系统[^vi]之后,它再一次成为数据库的一种时尚架构【37】。 Riak,Cassandra和Voldemort是由Dynamo启发的无领导复制模型的开源数据存储,所以这类数据库也被称为*Dynamo风格*。
+一些数据存储系统采用不同的方法,放弃主库的概念,并允许任何副本直接接受来自客户端的写入。最早的一些的复制数据系统是 **无领导的(leaderless)**【1,44】,但是在关系数据库主导的时代,这个想法几乎已被忘却。在亚马逊将其用于其内部的 Dynamo 系统 [^vi] 之后,它再一次成为数据库的一种时尚架构【37】。 Riak,Cassandra 和 Voldemort 是由 Dynamo 启发的无领导复制模型的开源数据存储,所以这类数据库也被称为 *Dynamo 风格*。
 
-[^vi]: Dynamo不适用于Amazon以外的用户。 令人困惑的是,AWS提供了一个名为DynamoDB的托管数据库产品,它使用了完全不同的体系结构:它基于单领导者复制。
+[^vi]: Dynamo 不适用于 Amazon 以外的用户。 令人困惑的是,AWS 提供了一个名为 DynamoDB 的托管数据库产品,它使用了完全不同的体系结构:它基于单领导者复制。
 
 在一些无领导者的实现中,客户端直接将写入发送到几个副本中,而另一些情况下,一个 **协调者(coordinator)** 节点代表客户端进行写入。但与主库数据库不同,协调者不执行特定的写入顺序。我们将会看到,这种设计上的差异对数据库的使用方式有着深远的影响。
 
@@ -476,84 +476,84 @@ PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志
 
 假设你有一个带有三个副本的数据库,而其中一个副本目前不可用,或许正在重新启动以安装系统更新。在基于主机的配置中,如果要继续处理写入,则可能需要执行故障切换(请参阅「[处理节点宕机](#处理节点宕机)」)。
 
-另一方面,在无领导配置中,故障切换不存在。[图5-10](img/fig5-10.png)显示了发生了什么事情:客户端(用户1234)并行发送写入到所有三个副本,并且两个可用副本接受写入,但是不可用副本错过了它。假设三个副本中的两个承认写入是足够的:在用户1234已经收到两个确定的响应之后,我们认为写入成功。客户简单地忽略了其中一个副本错过了写入的事实。
+另一方面,在无领导配置中,故障切换不存在。[图 5-10](img/fig5-10.png) 显示了发生了什么事情:客户端(用户 1234)并行发送写入到所有三个副本,并且两个可用副本接受写入,但是不可用副本错过了它。假设三个副本中的两个承认写入是足够的:在用户 1234 已经收到两个确定的响应之后,我们认为写入成功。客户简单地忽略了其中一个副本错过了写入的事实。
 
 ![](img/fig5-10.png)
 
-**图5-10 法定写入,法定读取,并在节点中断后读修复。**
+**图 5-10 法定写入,法定读取,并在节点中断后读修复。**
 
 现在想象一下,不可用的节点重新联机,客户端开始读取它。节点关闭时发生的任何写入都从该节点丢失。因此,如果你从该节点读取数据,则可能会将陈旧(过时)值视为响应。
 
-为了解决这个问题,当一个客户端从数据库中读取数据时,它不仅仅发送它的请求到一个副本:读请求也被并行地发送到多个节点。客户可能会从不同的节点获得不同的响应。即来自一个节点的最新值和来自另一个节点的陈旧值。版本号用于确定哪个值更新(请参阅“[检测并发写入](#检测并发写入)”)。
+为了解决这个问题,当一个客户端从数据库中读取数据时,它不仅仅发送它的请求到一个副本:读请求也被并行地发送到多个节点。客户可能会从不同的节点获得不同的响应。即来自一个节点的最新值和来自另一个节点的陈旧值。版本号用于确定哪个值更新(请参阅 “[检测并发写入](#检测并发写入)”)。
 
 #### 读修复和反熵
 
 复制方案应确保最终将所有数据复制到每个副本。在一个不可用的节点重新联机之后,它如何赶上它错过的写入?
 
-在Dynamo风格的数据存储中经常使用两种机制:
- 
+在 Dynamo 风格的数据存储中经常使用两种机制:
+
 * 读修复(Read repair)
 
-  当客户端并行读取多个节点时,它可以检测到任何陈旧的响应。例如,在[图5-10](img/fig5-10.png)中,用户2345获得了来自副本3的版本6值和来自副本1和2的版本7值。客户端发现副本3具有陈旧值,并将新值写回到该副本。这种方法适用于读频繁的值。
+  当客户端并行读取多个节点时,它可以检测到任何陈旧的响应。例如,在 [图 5-10](img/fig5-10.png) 中,用户 2345 获得了来自副本 3 的版本 6 值和来自副本 1 和 2 的版本 7 值。客户端发现副本 3 具有陈旧值,并将新值写回到该副本。这种方法适用于读频繁的值。
 
 * 反熵过程(Anti-entropy process)
 
   此外,一些数据存储具有后台进程,该进程不断查找副本之间的数据差异,并将任何缺少的数据从一个副本复制到另一个副本。与基于领导者的复制中的复制日志不同,此反熵过程不会以任何特定的顺序复制写入,并且在复制数据之前可能会有显著的延迟。
 
-并不是所有的系统都实现了这两个,例如,Voldemort目前没有反熵过程。请注意,如果没有反熵过程,某些副本中很少读取的值可能会丢失,从而降低了持久性,因为只有在应用程序读取值时才执行读修复。
+并不是所有的系统都实现了这两个,例如,Voldemort 目前没有反熵过程。请注意,如果没有反熵过程,某些副本中很少读取的值可能会丢失,从而降低了持久性,因为只有在应用程序读取值时才执行读修复。
 
 #### 读写的法定人数
 
-在[图5-10](img/fig5-10.png)的示例中,我们认为即使仅在三个副本中的两个上进行处理,写入仍然是成功的。如果三个副本中只有一个接受了写入,会怎样?以此类推,究竟多少个副本完成才可以认为写成功?
+在 [图 5-10](img/fig5-10.png) 的示例中,我们认为即使仅在三个副本中的两个上进行处理,写入仍然是成功的。如果三个副本中只有一个接受了写入,会怎样?以此类推,究竟多少个副本完成才可以认为写成功?
 
 如果我们知道,每个成功的写操作意味着在三个副本中至少有两个出现,这意味着至多有一个副本可能是陈旧的。因此,如果我们从至少两个副本读取,我们可以确定至少有一个是最新的。如果第三个副本停机或响应速度缓慢,则读取仍可以继续返回最新值。
 
-更一般地说,如果有n个副本,每个写入必须由w节点确认才能被认为是成功的,并且我们必须至少为每个读取查询r个节点。 (在我们的例子中,$n = 3,w = 2,r = 2$)。只要$w + r> n$,我们期望在读取时获得最新的值,因为r个读取中至少有一个节点是最新的。遵循这些r值,w值的读写称为**法定人数(quorum)**[^vii]的读和写【44】。你可以认为,r和w是有效读写所需的最低票数。
+更一般地说,如果有 n 个副本,每个写入必须由 w 节点确认才能被认为是成功的,并且我们必须至少为每个读取查询 r 个节点。 (在我们的例子中,$n = 3,w = 2,r = 2$)。只要 $w + r> n$,我们期望在读取时获得最新的值,因为 r 个读取中至少有一个节点是最新的。遵循这些 r 值,w 值的读写称为 **法定人数(quorum)**[^vii] 的读和写【44】。你可以认为,r 和 w 是有效读写所需的最低票数。
 
-[^vii]: 有时候这种法定人数被称为严格的法定人数,相对“宽松的法定人数”而言(见“[宽松的法定人数与提示移交](#宽松的法定人数与提示移交)”)
+[^vii]: 有时候这种法定人数被称为严格的法定人数,相对 “宽松的法定人数” 而言(见 “[宽松的法定人数与提示移交](#宽松的法定人数与提示移交)”)
 
-在Dynamo风格的数据库中,参数n,w和r通常是可配置的。一个常见的选择是使n为奇数(通常为3或5)并设置 $w = r =(n + 1)/ 2$(向上取整)。但是可以根据需要更改数字。例如,设置$w = n$和$r = 1$的写入很少且读取次数较多的工作负载可能会受益。这使得读取速度更快,但具有只有一个失败节点导致所有数据库写入失败的缺点。
+在 Dynamo 风格的数据库中,参数 n,w 和 r 通常是可配置的。一个常见的选择是使 n 为奇数(通常为 3 或 5)并设置 $w = r =(n + 1)/ 2$(向上取整)。但是可以根据需要更改数字。例如,设置 $w = n$ 和 $r = 1$ 的写入很少且读取次数较多的工作负载可能会受益。这使得读取速度更快,但具有只有一个失败节点导致所有数据库写入失败的缺点。
 
-> 集群中可能有多于n的节点。(集群的机器数可能多于副本数目),但是任何给定的值只能存储在n个节点上。这允许对数据集进行分区,从而可以支持比单个节点的存储能力更大的数据集。我们将在[第六章](ch6.md)继续讨论分区。
+> 集群中可能有多于 n 的节点。(集群的机器数可能多于副本数目),但是任何给定的值只能存储在 n 个节点上。这允许对数据集进行分区,从而可以支持比单个节点的存储能力更大的数据集。我们将在 [第六章](ch6.md) 继续讨论分区。
 >
 
-法定人数条件$w + r> n$允许系统容忍不可用的节点,如下所示:
+法定人数条件 $w + r> n$ 允许系统容忍不可用的节点,如下所示:
 
-* 如果$w <n$,如果节点不可用,我们仍然可以处理写入。
-* 如果$r <n$,如果节点不可用,我们仍然可以处理读取。
-* 对于$n = 3,w = 2,r = 2$,我们可以容忍一个不可用的节点。
-* 对于$n = 5,w = 3,r = 3$,我们可以容忍两个不可用的节点。 这个案例如[图5-11](img/fig5-11.png)所示。
-* 通常,读取和写入操作始终并行发送到所有n个副本。 参数w和r决定我们等待多少个节点,即在我们认为读或写成功之前,有多少个节点需要报告成功。
+* 如果 $w <n$,如果节点不可用,我们仍然可以处理写入。
+* 如果 $r <n$,如果节点不可用,我们仍然可以处理读取。
+* 对于 $n = 3,w = 2,r = 2$,我们可以容忍一个不可用的节点。
+* 对于 $n = 5,w = 3,r = 3$,我们可以容忍两个不可用的节点。 这个案例如 [图 5-11](img/fig5-11.png) 所示。
+* 通常,读取和写入操作始终并行发送到所有 n 个副本。 参数 w 和 r 决定我们等待多少个节点,即在我们认为读或写成功之前,有多少个节点需要报告成功。
 
 ![](img/fig5-11.png)
 
-**图5-11  如果$w + r > n$,读取r个副本,至少有一个r副本必然包含了最近的成功写入**
+**图 5-11  如果 $w + r > n$,读取 r 个副本,至少有一个 r 副本必然包含了最近的成功写入**
 
-如果少于所需的w或r节点可用,则写入或读取将返回错误。 由于许多原因,节点可能不可用:因为执行操作的错误(由于磁盘已满而无法写入),因为节点关闭(崩溃,关闭电源),由于客户端和服务器节点之间的网络中断,或任何其他原因。 我们只关心节点是否返回了成功的响应,而不需要区分不同类型的错误。
+如果少于所需的 w 或 r 节点可用,则写入或读取将返回错误。 由于许多原因,节点可能不可用:因为执行操作的错误(由于磁盘已满而无法写入),因为节点关闭(崩溃,关闭电源),由于客户端和服务器节点之间的网络中断,或任何其他原因。 我们只关心节点是否返回了成功的响应,而不需要区分不同类型的错误。
 
 
 ### 法定人数一致性的局限性
 
-如果你有n个副本,并且你选择w和r,使得$w + r> n$,你通常可以期望每个键的读取都能返回最近写入的值。情况就是这样,因为你写入的节点集合和你读取的节点集合必须重叠。也就是说,你读取的节点中必须至少有一个具有最新值的节点(如[图5-11](img/fig5-11.png)所示)。
+如果你有 n 个副本,并且你选择 w 和 r,使得 $w + r> n$,你通常可以期望每个键的读取都能返回最近写入的值。情况就是这样,因为你写入的节点集合和你读取的节点集合必须重叠。也就是说,你读取的节点中必须至少有一个具有最新值的节点(如 [图 5-11](img/fig5-11.png) 所示)。
 
-通常,r和w被选为多数(超过 $n/2$ )节点,因为这确保了$w + r> n$,同时仍然容忍多达$n/2$个节点故障。但是,法定人数不一定必须是大多数,只是读写使用的节点交集至少需要包括一个节点。其他法定人数的配置是可能的,这使得分布式算法的设计有一定的灵活性【45】。
+通常,r 和 w 被选为多数(超过 $n/2$ )节点,因为这确保了 $w + r> n$,同时仍然容忍多达 $n/2$ 个节点故障。但是,法定人数不一定必须是大多数,只是读写使用的节点交集至少需要包括一个节点。其他法定人数的配置是可能的,这使得分布式算法的设计有一定的灵活性【45】。
 
-你也可以将w和r设置为较小的数字,以使$w + r≤n$(即法定条件不满足)。在这种情况下,读取和写入操作仍将被发送到n个节点,但操作成功只需要少量的成功响应。
+你也可以将 w 和 r 设置为较小的数字,以使 $w + r≤n$(即法定条件不满足)。在这种情况下,读取和写入操作仍将被发送到 n 个节点,但操作成功只需要少量的成功响应。
 
-较小的w和r更有可能会读取过时的数据,因为你的读取更有可能不包含具有最新值的节点。另一方面,这种配置允许更低的延迟和更高的可用性:如果存在网络中断,并且许多副本变得无法访问,则可以继续处理读取和写入的机会更大。只有当可达副本的数量低于w或r时,数据库才分别变得不可用于写入或读取。
+较小的 w 和 r 更有可能会读取过时的数据,因为你的读取更有可能不包含具有最新值的节点。另一方面,这种配置允许更低的延迟和更高的可用性:如果存在网络中断,并且许多副本变得无法访问,则可以继续处理读取和写入的机会更大。只有当可达副本的数量低于 w 或 r 时,数据库才分别变得不可用于写入或读取。
 
-但是,即使在$w + r> n$的情况下,也可能存在返回陈旧值的边缘情况。这取决于实现,但可能的情况包括:
+但是,即使在 $w + r> n$ 的情况下,也可能存在返回陈旧值的边缘情况。这取决于实现,但可能的情况包括:
 
-* 如果使用宽松的法定人数(见“[宽松的法定人数与提示移交](#宽松的法定人数与提示移交)”),w个写入和r个读取落在完全不同的节点上,因此r节点和w之间不再保证有重叠节点【46】。
-* 如果两个写入同时发生,不清楚哪一个先发生。在这种情况下,唯一安全的解决方案是合并并发写入(请参阅“[处理写入冲突](#处理写入冲突)”)。如果根据时间戳(最后写入胜利)挑选出一个胜者,则由于时钟偏差【35】,写入可能会丢失。我们将在“[检测并发写入](#检测并发写入)”继续讨论此话题。
+* 如果使用宽松的法定人数(见 “[宽松的法定人数与提示移交](#宽松的法定人数与提示移交)”),w 个写入和 r 个读取落在完全不同的节点上,因此 r 节点和 w 之间不再保证有重叠节点【46】。
+* 如果两个写入同时发生,不清楚哪一个先发生。在这种情况下,唯一安全的解决方案是合并并发写入(请参阅 “[处理写入冲突](#处理写入冲突)”)。如果根据时间戳(最后写入胜利)挑选出一个胜者,则由于时钟偏差【35】,写入可能会丢失。我们将在 “[检测并发写入](#检测并发写入)” 继续讨论此话题。
 * 如果写操作与读操作同时发生,写操作可能仅反映在某些副本上。在这种情况下,不确定读取是返回旧值还是新值。
-* 如果写操作在某些副本上成功,而在其他节点上失败(例如,因为某些节点上的磁盘已满),在小于w个副本上写入成功。所以整体判定写入失败,但整体写入失败并没有在写入成功的副本上回滚。这意味着如果一个写入虽然报告失败,后续的读取仍然可能会读取这次失败写入的值【47】。
-* 如果携带新值的节点失败,需要读取其他带有旧值的副本。并且其数据从带有旧值的副本中恢复,则存储新值的副本数可能会低于w,从而打破法定人数条件。
-* 即使一切工作正常,有时也会不幸地出现关于**时序(timing)** 的边缘情况,我们将在“[线性一致性和法定人数](ch9.md#线性一致性和法定人数)”中看到这点。
+* 如果写操作在某些副本上成功,而在其他节点上失败(例如,因为某些节点上的磁盘已满),在小于 w 个副本上写入成功。所以整体判定写入失败,但整体写入失败并没有在写入成功的副本上回滚。这意味着如果一个写入虽然报告失败,后续的读取仍然可能会读取这次失败写入的值【47】。
+* 如果携带新值的节点失败,需要读取其他带有旧值的副本。并且其数据从带有旧值的副本中恢复,则存储新值的副本数可能会低于 w,从而打破法定人数条件。
+* 即使一切工作正常,有时也会不幸地出现关于 **时序(timing)** 的边缘情况,我们将在 “[线性一致性和法定人数](ch9.md#线性一致性和法定人数)” 中看到这点。
 
-因此,尽管法定人数似乎保证读取返回最新的写入值,但在实践中并不那么简单。 Dynamo风格的数据库通常针对可以忍受最终一致性的用例进行优化。允许通过参数w和r来调整读取陈旧值的概率,但把它们当成绝对的保证是不明智的。
+因此,尽管法定人数似乎保证读取返回最新的写入值,但在实践中并不那么简单。 Dynamo 风格的数据库通常针对可以忍受最终一致性的用例进行优化。允许通过参数 w 和 r 来调整读取陈旧值的概率,但把它们当成绝对的保证是不明智的。
 
-尤其是,因为通常没有得到“[复制延迟问题](#复制延迟问题)”中讨论的保证(读己之写,单调读,一致前缀读),前面提到的异常可能会发生在应用程序中。更强有力的保证通常需要**事务**或**共识**。我们将在[第七章](ch7.md)和[第九章](ch9.md)回到这些话题。
+尤其是,因为通常没有得到 “[复制延迟问题](#复制延迟问题)” 中讨论的保证(读己之写,单调读,一致前缀读),前面提到的异常可能会发生在应用程序中。更强有力的保证通常需要 **事务** 或 **共识**。我们将在 [第七章](ch7.md) 和 [第九章](ch9.md) 回到这些话题。
 
 #### 监控陈旧度
 
@@ -563,42 +563,42 @@ PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志
 
 然而,在无领导者复制的系统中,没有固定的写入顺序,这使得监控变得更加困难。而且,如果数据库只使用读修复(没有反熵过程),那么对于一个值可能会有多大的限制是没有限制的 - 如果一个值很少被读取,那么由一个陈旧副本返回的值可能是古老的。
 
-已经有一些关于衡量无主复制数据库中的复制陈旧度的研究,并根据参数n,w和r来预测陈旧读取的预期百分比【48】。不幸的是,这还不是很常见的做法,但是将陈旧测量值包含在数据库的度量标准集中是一件好事。虽然最终一致性是一种有意模糊的保证,但是从可操作性角度来说,能够量化“最终”也是很重要的。
+已经有一些关于衡量无主复制数据库中的复制陈旧度的研究,并根据参数 n,w 和 r 来预测陈旧读取的预期百分比【48】。不幸的是,这还不是很常见的做法,但是将陈旧测量值包含在数据库的度量标准集中是一件好事。虽然最终一致性是一种有意模糊的保证,但是从可操作性角度来说,能够量化 “最终” 也是很重要的。
 
 ### 宽松的法定人数与提示移交
 
-合理配置的法定人数可以使数据库无需故障切换即可容忍个别节点的故障。也可以容忍个别节点变慢,因为请求不必等待所有n个节点响应——当w或r节点响应时它们可以返回。对于需要高可用、低延时、且能够容忍偶尔读到陈旧值的应用场景来说,这些特性使无主复制的数据库很有吸引力。
+合理配置的法定人数可以使数据库无需故障切换即可容忍个别节点的故障。也可以容忍个别节点变慢,因为请求不必等待所有 n 个节点响应 —— 当 w 或 r 节点响应时它们可以返回。对于需要高可用、低延时、且能够容忍偶尔读到陈旧值的应用场景来说,这些特性使无主复制的数据库很有吸引力。
 
-然而,法定人数(如迄今为止所描述的)并不像它们可能的那样具有容错性。网络中断可以很容易地将客户端从大量的数据库节点上切断。虽然这些节点是活着的,而其他客户端可能能够连接到它们,但是从数据库节点切断的客户端来看,它们也可能已经死亡。在这种情况下,剩余的可用节点可能会少于w或r,因此客户端不再能达到法定人数。
+然而,法定人数(如迄今为止所描述的)并不像它们可能的那样具有容错性。网络中断可以很容易地将客户端从大量的数据库节点上切断。虽然这些节点是活着的,而其他客户端可能能够连接到它们,但是从数据库节点切断的客户端来看,它们也可能已经死亡。在这种情况下,剩余的可用节点可能会少于 w 或 r,因此客户端不再能达到法定人数。
 
-在一个大型的集群中(节点数量明显多于n个),网络中断期间客户端可能仍能连接到一些数据库节点,但又不足以组成一个特定值的法定人数。在这种情况下,数据库设计人员需要权衡一下:
+在一个大型的集群中(节点数量明显多于 n 个),网络中断期间客户端可能仍能连接到一些数据库节点,但又不足以组成一个特定值的法定人数。在这种情况下,数据库设计人员需要权衡一下:
 
-* 对于所有无法达到w或r节点法定人数的请求,是否返回错误是更好的?
-* 或者我们是否应该接受写入,然后将它们写入一些可达的节点,但不在这些值通常所存在的n个节点上?
+* 对于所有无法达到 w 或 r 节点法定人数的请求,是否返回错误是更好的?
+* 或者我们是否应该接受写入,然后将它们写入一些可达的节点,但不在这些值通常所存在的 n 个节点上?
 
-后者被认为是一个**宽松的法定人数(sloppy quorum)**【37】:写和读仍然需要w和r成功的响应,但这些响应可能来自不在指定的n个“主”节点中的其它节点。比方说,如果你把自己锁在房子外面,你可能会敲开邻居的门,问你是否可以暂时呆在沙发上。
+后者被认为是一个 **宽松的法定人数(sloppy quorum)**【37】:写和读仍然需要 w 和 r 成功的响应,但这些响应可能来自不在指定的 n 个 “主” 节点中的其它节点。比方说,如果你把自己锁在房子外面,你可能会敲开邻居的门,问你是否可以暂时呆在沙发上。
 
-一旦网络中断得到解决,代表另一个节点临时接受的一个节点的任何写入都被发送到适当的“主”节点。这就是所谓的**提示移交(hinted handoff)**(一旦你再次找到你的房子的钥匙,你的邻居礼貌地要求你离开沙发回家)。
+一旦网络中断得到解决,代表另一个节点临时接受的一个节点的任何写入都被发送到适当的 “主” 节点。这就是所谓的 **提示移交(hinted handoff)**(一旦你再次找到你的房子的钥匙,你的邻居礼貌地要求你离开沙发回家)。
 
-宽松的法定人数对写入可用性的提高特别有用:只要有任何w节点可用,数据库就可以接受写入。然而,这意味着即使当$w + r> n$时,也不能确定读取某个键的最新值,因为最新的值可能已经临时写入了n之外的某些节点【47】。
+宽松的法定人数对写入可用性的提高特别有用:只要有任何 w 节点可用,数据库就可以接受写入。然而,这意味着即使当 $w + r> n$ 时,也不能确定读取某个键的最新值,因为最新的值可能已经临时写入了 n 之外的某些节点【47】。
 
-因此,在传统意义上,一个宽松的法定人数实际上不是一个法定人数。这只是一个保证,即数据存储在w节点的地方。但不能保证r节点的读取,直到提示移交已经完成。
+因此,在传统意义上,一个宽松的法定人数实际上不是一个法定人数。这只是一个保证,即数据存储在 w 节点的地方。但不能保证 r 节点的读取,直到提示移交已经完成。
 
-在所有常见的Dynamo实现中,宽松的法定人数是可选的。在Riak中,它们默认是启用的,而在Cassandra和Voldemort中它们默认是禁用的【46,49,50】。
+在所有常见的 Dynamo 实现中,宽松的法定人数是可选的。在 Riak 中,它们默认是启用的,而在 Cassandra 和 Voldemort 中它们默认是禁用的【46,49,50】。
 
 #### 运维多个数据中心
 
-我们先前讨论了跨数据中心复制作为多主复制的用例(请参阅“[多主复制](#多主复制)”)。无主复制也适用于多数据中心操作,因为它旨在容忍冲突的并发写入,网络中断和延迟尖峰。
+我们先前讨论了跨数据中心复制作为多主复制的用例(请参阅 “[多主复制](#多主复制)”)。无主复制也适用于多数据中心操作,因为它旨在容忍冲突的并发写入,网络中断和延迟尖峰。
 
-Cassandra和Voldemort在正常的无主模型中实现了他们的多数据中心支持:副本的数量n包括所有数据中心的节点,在配置中,你可以指定每个数据中心中你想拥有的副本的数量。无论数据中心如何,每个来自客户端的写入都会发送到所有副本,但客户端通常只等待来自其本地数据中心内的法定节点的确认,从而不会受到跨数据中心链路延迟和中断的影响。对其他数据中心的高延迟写入通常被配置为异步发生,尽管配置有一定的灵活性【50,51】。
+Cassandra 和 Voldemort 在正常的无主模型中实现了他们的多数据中心支持:副本的数量 n 包括所有数据中心的节点,在配置中,你可以指定每个数据中心中你想拥有的副本的数量。无论数据中心如何,每个来自客户端的写入都会发送到所有副本,但客户端通常只等待来自其本地数据中心内的法定节点的确认,从而不会受到跨数据中心链路延迟和中断的影响。对其他数据中心的高延迟写入通常被配置为异步发生,尽管配置有一定的灵活性【50,51】。
 
-Riak将客户端和数据库节点之间的所有通信保持在一个数据中心本地,因此n描述了一个数据中心内的副本数量。数据库集群之间的跨数据中心复制在后台异步发生,其风格类似于多领导者复制【52】。
+Riak 将客户端和数据库节点之间的所有通信保持在一个数据中心本地,因此 n 描述了一个数据中心内的副本数量。数据库集群之间的跨数据中心复制在后台异步发生,其风格类似于多领导者复制【52】。
 
 ### 检测并发写入
 
-Dynamo风格的数据库允许多个客户端同时写入相同的Key,这意味着即使使用严格的法定人数也会发生冲突。这种情况与多领导者复制相似(请参阅“[处理写入冲突](#处理写入冲突)”),但在Dynamo样式的数据库中,在**读修复**或**提示移交**期间也可能会产生冲突。
+Dynamo 风格的数据库允许多个客户端同时写入相同的 Key,这意味着即使使用严格的法定人数也会发生冲突。这种情况与多领导者复制相似(请参阅 “[处理写入冲突](#处理写入冲突)”),但在 Dynamo 样式的数据库中,在 **读修复** 或 **提示移交** 期间也可能会产生冲突。
 
-问题在于,由于可变的网络延迟和部分故障,事件可能在不同的节点以不同的顺序到达。例如,[图5-12](img/fig5-12.png)显示了两个客户机A和B同时写入三节点数据存储区中的键X:
+问题在于,由于可变的网络延迟和部分故障,事件可能在不同的节点以不同的顺序到达。例如,[图 5-12](img/fig5-12.png) 显示了两个客户机 A 和 B 同时写入三节点数据存储区中的键 X:
 
 * 节点 1 接收来自 A 的写入,但由于暂时中断,未接收到来自 B 的写入。
 * 节点 2 首先接收来自 A 的写入,然后接收来自 B 的写入。
@@ -606,72 +606,72 @@ Dynamo风格的数据库允许多个客户端同时写入相同的Key,这意
 
 ![](img/fig5-12.png)
 
-**图5-12 并发写入Dynamo风格的数据存储:没有明确定义的顺序。**
+**图 5-12 并发写入 Dynamo 风格的数据存储:没有明确定义的顺序。**
 
-如果每个节点只要接收到来自客户端的写入请求就简单地覆盖了某个键的值,那么节点就会永久地不一致,如[图5-12](img/fig5-12.png)中的最终获取请求所示:节点2认为 X 的最终值是 B,而其他节点认为值是 A 。
+如果每个节点只要接收到来自客户端的写入请求就简单地覆盖了某个键的值,那么节点就会永久地不一致,如 [图 5-12](img/fig5-12.png) 中的最终获取请求所示:节点 2 认为 X 的最终值是 B,而其他节点认为值是 A 。
 
 为了最终达成一致,副本应该趋于相同的值。如何做到这一点?有人可能希望复制的数据库能够自动处理,但不幸的是,大多数的实现都很糟糕:如果你想避免丢失数据,你(应用程序开发人员)需要知道很多有关数据库冲突处理的内部信息。
 
-在“[处理写入冲突](#处理写入冲突)”一节中已经简要介绍了一些解决冲突的技术。在总结本章之前,让我们来更详细地探讨这个问题。
+在 “[处理写入冲突](#处理写入冲突)” 一节中已经简要介绍了一些解决冲突的技术。在总结本章之前,让我们来更详细地探讨这个问题。
 
 #### 最后写入胜利(丢弃并发写入)
 
-实现最终融合的一种方法是声明每个副本只需要存储最 **“最近”** 的值,并允许 **“更旧”** 的值被覆盖和抛弃。然后,只要我们有一种明确的方式来确定哪个写是“最近的”,并且每个写入最终都被复制到每个副本,那么复制最终会收敛到相同的值。
+实现最终融合的一种方法是声明每个副本只需要存储最 **“最近”** 的值,并允许 **“更旧”** 的值被覆盖和抛弃。然后,只要我们有一种明确的方式来确定哪个写是 “最近的”,并且每个写入最终都被复制到每个副本,那么复制最终会收敛到相同的值。
 
-正如 **“最近”** 的引号所表明的,这个想法其实颇具误导性。在[图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# 有序事件的时间戳)” 中讨论。
 
-有一些情况,如缓存,其中丢失的写入可能是可以接受的。如果丢失数据不可接受,LWW是解决冲突的一个很烂的选择。
+有一些情况,如缓存,其中丢失的写入可能是可以接受的。如果丢失数据不可接受,LWW 是解决冲突的一个很烂的选择。
 
-与LWW一起使用数据库的唯一安全方法是确保一个键只写入一次,然后视为不可变,从而避免对同一个键进行并发更新。例如,Cassandra推荐使用的方法是使用UUID作为键,从而为每个写操作提供一个唯一的键【53】。
+与 LWW 一起使用数据库的唯一安全方法是确保一个键只写入一次,然后视为不可变,从而避免对同一个键进行并发更新。例如,Cassandra 推荐使用的方法是使用 UUID 作为键,从而为每个写操作提供一个唯一的键【53】。
 
 #### “此前发生”的关系和并发
 
 我们如何判断两个操作是否是并发的?为了建立一个直觉,让我们看看一些例子:
 
-* 在[图5-9](fig5-9.png)中,两个写入不是并发的:A的插入发生在B的递增之前,因为B递增的值是A插入的值。换句话说,B的操作建立在A的操作上,所以B的操作必须有后来发生。我们也可以说B是 **因果依赖(causally dependent)** 于A。
-* 另一方面,[图5-12](fig5-12.png)中的两个写入是并发的:当每个客户端启动操作时,它不知道另一个客户端也正在执行操作同样的键。因此,操作之间不存在因果关系。
+* 在 [图 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并发。我们需要的是一个算法来告诉我们两个操作是否是并发的。如果一个操作发生在另一个操作之前,则后面的操作应该覆盖较早的操作,但是如果这些操作是并发的,则存在需要解决的冲突。
+因此,只要有两个操作 A 和 B,就有三种可能性:A 在 B 之前发生,或者 B 在 A 之前发生,或者 A 和 B 并发。我们需要的是一个算法来告诉我们两个操作是否是并发的。如果一个操作发生在另一个操作之前,则后面的操作应该覆盖较早的操作,但是如果这些操作是并发的,则存在需要解决的冲突。
 
 
 > #### 并发性,时间和相对性
 >
-> 如果两个操作 **“同时”** 发生,似乎应该称为并发——但事实上,它们在字面时间上重叠与否并不重要。由于分布式系统中的时钟问题,现实中是很难判断两个事件是否**同时**发生的,这个问题我们将在[第八章](ch8.md)中详细讨论。
+> 如果两个操作 **“同时”** 发生,似乎应该称为并发 —— 但事实上,它们在字面时间上重叠与否并不重要。由于分布式系统中的时钟问题,现实中是很难判断两个事件是否 **同时** 发生的,这个问题我们将在 [第八章](ch8.md) 中详细讨论。
 >
-> 为了定义并发性,确切的时间并不重要:如果两个操作都意识不到对方的存在,就称这两个操作**并发**,而不管它们发生的物理时间。人们有时把这个原理和狭义相对论的物理学联系起来【54】,它引入了信息不能比光速更快的思想。因此,如果两个事件发生的时间差小于光通过它们之间的距离所需要的时间,那么这两个事件不可能相互影响。
+> 为了定义并发性,确切的时间并不重要:如果两个操作都意识不到对方的存在,就称这两个操作 **并发**,而不管它们发生的物理时间。人们有时把这个原理和狭义相对论的物理学联系起来【54】,它引入了信息不能比光速更快的思想。因此,如果两个事件发生的时间差小于光通过它们之间的距离所需要的时间,那么这两个事件不可能相互影响。
 >
-> 在计算机系统中,即使光速原则上允许一个操作影响另一个操作,但两个操作也可能是**并行的**。例如,如果网络缓慢或中断,两个操作间可能会出现一段时间间隔,但仍然是并发的,因为网络问题阻止一个操作意识到另一个操作的存在。
+> 在计算机系统中,即使光速原则上允许一个操作影响另一个操作,但两个操作也可能是 **并行的**。例如,如果网络缓慢或中断,两个操作间可能会出现一段时间间隔,但仍然是并发的,因为网络问题阻止一个操作意识到另一个操作的存在。
 
 
 #### 捕获"此前发生"关系
 
 来看一个算法,它确定两个操作是否为并发的,还是一个在另一个之前。为了简单起见,我们从一个只有一个副本的数据库开始。一旦我们已经制定了如何在单个副本上完成这项工作,我们可以将该方法推广到具有多个副本的无领导者数据库。
 
-[图5-13](img/fig5-13.png)显示了两个客户端同时向同一购物车添加项目。 (如果这样的例子让你觉得太麻烦了,那么可以想象,两个空中交通管制员同时把飞机添加到他们正在跟踪的区域)最初,购物车是空的。在它们之间,客户端向数据库发出五次写入:
+[图 5-13](img/fig5-13.png) 显示了两个客户端同时向同一购物车添加项目。 (如果这样的例子让你觉得太麻烦了,那么可以想象,两个空中交通管制员同时把飞机添加到他们正在跟踪的区域)最初,购物车是空的。在它们之间,客户端向数据库发出五次写入:
 
-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:[鸡蛋,牛奶,火腿]
-5. 最后,客户端 1 想要加培根。它以前在v3中从服务器接收[牛奶,面粉]和[鸡蛋],所以它合并这些,添加培根,并将最终值[牛奶,面粉,鸡蛋,培根]连同版本号v3发往服务器。这会覆盖v3[牛奶,面粉](请注意[鸡蛋]已经在最后一步被覆盖),但与v4[鸡蛋,牛奶,火腿]并发,所以服务器保留这两个并发值。
+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:[鸡蛋,牛奶,火腿]
+5. 最后,客户端 1 想要加培根。它以前在 v3 中从服务器接收 [牛奶,面粉] 和 [鸡蛋],所以它合并这些,添加培根,并将最终值 [牛奶,面粉,鸡蛋,培根] 连同版本号 v3 发往服务器。这会覆盖 v3 [牛奶,面粉](请注意 [鸡蛋] 已经在最后一步被覆盖),但与 v4 [鸡蛋,牛奶,火腿] 并发,所以服务器保留这两个并发值。
 
 ![](img/fig5-13.png)
 
-**图5-13  捕获两个客户端之间的因果关系,同时编辑购物车。**
+**图 5-13  捕获两个客户端之间的因果关系,同时编辑购物车。**
 
-[图5-13](img/fig5-13.png)中的操作之间的数据流如[图5-14](img/fig5-14.png)所示。 箭头表示哪个操作发生在其他操作之前,意味着后面的操作知道或依赖于较早的操作。 在这个例子中,客户端永远不会完全掌握服务器上的数据,因为总是有另一个操作同时进行。 但是,旧版本的值最终会被覆盖,并且不会丢失任何写入。
+[图 5-13](img/fig5-13.png) 中的操作之间的数据流如 [图 5-14](img/fig5-14.png) 所示。 箭头表示哪个操作发生在其他操作之前,意味着后面的操作知道或依赖于较早的操作。 在这个例子中,客户端永远不会完全掌握服务器上的数据,因为总是有另一个操作同时进行。 但是,旧版本的值最终会被覆盖,并且不会丢失任何写入。
 
 ![](img/fig5-14.png)
 
-**图5-14 图5-13中的因果依赖关系图。**
+**图 5-14 图 5-13 中的因果依赖关系图。**
 
-请注意,服务器可以通过查看版本号来确定两个操作是否是并发的——它不需要解释该值本身(因此该值可以是任何数据结构)。该算法的工作原理如下:
+请注意,服务器可以通过查看版本号来确定两个操作是否是并发的 —— 它不需要解释该值本身(因此该值可以是任何数据结构)。该算法的工作原理如下:
 
 * 服务器为每个键保留一个版本号,每次写入键时都增加版本号,并将新版本号与写入的值一起存储。
 * 当客户端读取键时,服务器将返回所有未覆盖的值以及最新的版本号。客户端在写入前必须读取。
@@ -682,31 +682,31 @@ LWW实现了最终收敛的目标,但以**持久性**为代价:如果同一
 
 #### 合并同时写入的值
 
-这种算法可以确保没有数据被无声地丢弃,但不幸的是,客户端需要做一些额外的工作:客户端随后必须通过合并并发写入的值来进行清理。 Riak称这些并发值为**兄弟(siblings)**。
+这种算法可以确保没有数据被无声地丢弃,但不幸的是,客户端需要做一些额外的工作:客户端随后必须通过合并并发写入的值来进行清理。 Riak 称这些并发值为 **兄弟(siblings)**。
 
-合并并发值,本质上是与多领导者复制中的冲突解决问题相同,我们先前讨论过(请参阅“[处理写入冲突](#处理写入冲突)”)。一个简单的方法是根据版本号或时间戳(最后写入胜利)来选择一个值,但这意味着丢失数据。所以,你可能需要在应用程序代码中额外做些更聪明的事情。
+合并并发值,本质上是与多领导者复制中的冲突解决问题相同,我们先前讨论过(请参阅 “[处理写入冲突](#处理写入冲突)”)。一个简单的方法是根据版本号或时间戳(最后写入胜利)来选择一个值,但这意味着丢失数据。所以,你可能需要在应用程序代码中额外做些更聪明的事情。
 
-以购物车为例,一种合理的合并值的方法就是做并集。在[图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】可以以合理的方式自动合并,包括保留删除。
 
 #### 版本向量
 
-[图5-13](img/fig5-13.png)中的示例只使用一个副本。当有多个副本但没有领导者时,算法如何修改?
+[图 5-13](img/fig5-13.png) 中的示例只使用一个副本。当有多个副本但没有领导者时,算法如何修改?
 
-[图5-13](img/fig5-13.png)使用单个版本号来捕获操作之间的依赖关系,但是当多个副本并发接受写入时,这是不够的。相反,还需要在**每个副本**以及**每个键**使用版本号。每个副本在处理写入时增加自己的版本号,并且跟踪从其他副本中看到的版本号。这个信息指出了要覆盖哪些并发值,以及保留哪些并发值。
+[图 5-13](img/fig5-13.png) 使用单个版本号来捕获操作之间的依赖关系,但是当多个副本并发接受写入时,这是不够的。相反,还需要在 **每个副本** 以及 **每个键** 使用版本号。每个副本在处理写入时增加自己的版本号,并且跟踪从其他副本中看到的版本号。这个信息指出了要覆盖哪些并发值,以及保留哪些并发值。
 
-所有副本的版本号集合称为**版本向量(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)。版本向量允许数据库区分覆盖写入和并发写入。
 
 另外,就像在单个副本中的情况一样,应用程序可能需要合并并发值。版本向量结构能够确保从一个副本读取并随后写回到另一个副本是安全的。这样做虽然可能会在其他副本上面创建数据,但只要能正确合并就不会丢失数据。
 
 > #### 版本向量和向量时钟
 >
-> 版本向量有时也被称为矢量时钟,即使它们不完全相同。 差别很微妙——细节请参阅参考资料【57,60,61】。 简而言之,在比较副本的状态时,版本向量是正确的数据结构。
+> 版本向量有时也被称为矢量时钟,即使它们不完全相同。 差别很微妙 —— 细节请参阅参考资料【57,60,61】。 简而言之,在比较副本的状态时,版本向量是正确的数据结构。
 >
 
 ## 本章小结
@@ -751,7 +751,7 @@ LWW实现了最终收敛的目标,但以**持久性**为代价:如果同一
 复制可以是同步的,也可以是异步的,这在发生故障时对系统行为有深远的影响。尽管在系统运行平稳时异步复制速度很快,但是要弄清楚在复制滞后增加和服务器故障时会发生什么,这一点很重要。如果一个领导者失败了,并且你提升了一个异步更新的追随者成为新的领导者,那么最近提交的数据可能会丢失。
 
 我们研究了一些可能由复制滞后引起的奇怪效应,我们也讨论了一些有助于决定应用程序在复制滞后时的行为的一致性模型:
- 
+
 * 写后读
 
   用户应该总是看到自己提交的数据。
diff --git a/ch6.md b/ch6.md
index 3a15e76..f001e93 100644
--- a/ch6.md
+++ b/ch6.md
@@ -1,4 +1,4 @@
-# 第六章:分区 
+# 第六章:分区
 
 ![](img/ch6.png)
 
@@ -11,43 +11,44 @@
 
 [TOC]
 
-在[第五章](ch5.md)中,我们讨论了复制——即数据在不同节点上的副本,对于非常大的数据集,或非常高的吞吐量,仅仅进行复制是不够的:我们需要将数据进行**分区(partitions)**,也称为**分片(sharding)**[^i]。
+在 [第五章](ch5.md) 中,我们讨论了复制 —— 即数据在不同节点上的副本,对于非常大的数据集,或非常高的吞吐量,仅仅进行复制是不够的:我们需要将数据进行 **分区(partitions)**,也称为 **分片(sharding)**[^i]。
 
-[^i]: 正如本章所讨论的,分区是一种有意将大型数据库分解成小型数据库的方式。它与 **网络分区(network partitions, netsplits)** 无关,这是节点之间网络故障的一种。我们将在[第八章](ch8.md)讨论这些错误。
+[^i]: 正如本章所讨论的,分区是一种有意将大型数据库分解成小型数据库的方式。它与 **网络分区(network partitions, netsplits)** 无关,这是节点之间网络故障的一种。我们将在 [第八章](ch8.md) 讨论这些错误。
 
 > #### 术语澄清
 >
-> 上文中的**分区(partition)**,在MongoDB,Elasticsearch和Solr Cloud中被称为**分片(shard)**,在HBase中称之为**区域(Region)**,Bigtable中则是 **表块(tablet)**,Cassandra和Riak中是**虚节点(vnode)**,Couchbase中叫做**虚桶(vBucket)**。但是**分区(partitioning)** 是最约定俗成的叫法。
+> 上文中的 **分区 (partition)**,在 MongoDB,Elasticsearch 和 Solr Cloud 中被称为 **分片 (shard)**,在 HBase 中称之为 **区域 (Region)**,Bigtable 中则是 **表块(tablet)**,Cassandra 和 Riak 中是 **虚节点(vnode)**,Couchbase 中叫做 **虚桶 (vBucket)**。但是 **分区 (partitioning)** 是最约定俗成的叫法。
 >
 
 通常情况下,每条数据(每条记录,每行或每个文档)属于且仅属于一个分区。有很多方法可以实现这一点,本章将进行深入讨论。实际上,每个分区都是自己的小型数据库,尽管数据库可能支持同时进行多个分区的操作。
 
-分区主要是为了**可伸缩性**。不同的分区可以放在不共享集群中的不同节点上(请参阅[第二部分](part-ii.md)关于[无共享架构](part-ii.md#无共享架构)的定义)。因此,大数据集可以分布在多个磁盘上,并且查询负载可以分布在多个处理器上。
+分区主要是为了 **可伸缩性**。不同的分区可以放在不共享集群中的不同节点上(请参阅 [第二部分](part-ii.md) 关于 [无共享架构](part-ii.md#无共享架构) 的定义)。因此,大数据集可以分布在多个磁盘上,并且查询负载可以分布在多个处理器上。
 
 对于在单个分区上运行的查询,每个节点可以独立执行对自己的查询,因此可以通过添加更多的节点来扩大查询吞吐量。大型,复杂的查询可能会跨越多个节点并行处理,尽管这也带来了新的困难。
 
-分区数据库在20世纪80年代由Teradata和NonStop SQL【1】等产品率先推出,最近因为NoSQL数据库和基于Hadoop的数据仓库重新被关注。有些系统是为事务性工作设计的,有些系统则用于分析(请参阅“[事务处理还是分析](ch3.md#事务处理还是分析)”):这种差异会影响系统的运作方式,但是分区的基本原理均适用于这两种工作方式。
+分区数据库在 20 世纪 80 年代由 Teradata 和 NonStop SQL【1】等产品率先推出,最近因为 NoSQL 数据库和基于 Hadoop 的数据仓库重新被关注。有些系统是为事务性工作设计的,有些系统则用于分析(请参阅 “[事务处理还是分析](ch3.md#事务处理还是分析)”):这种差异会影响系统的运作方式,但是分区的基本原理均适用于这两种工作方式。
 
-在本章中,我们将首先介绍分割大型数据集的不同方法,并观察索引如何与分区配合。然后我们将讨论[分区再平衡(rebalancing)](#分区再平衡),如果想要添加或删除集群中的节点,则必须进行再平衡。最后,我们将概述数据库如何将请求路由到正确的分区并执行查询。
+在本章中,我们将首先介绍分割大型数据集的不同方法,并观察索引如何与分区配合。然后我们将讨论 [分区再平衡(rebalancing)](#分区再平衡),如果想要添加或删除集群中的节点,则必须进行再平衡。最后,我们将概述数据库如何将请求路由到正确的分区并执行查询。
 
 ## 分区与复制
 
-分区通常与复制结合使用,使得每个分区的副本存储在多个节点上。 这意味着,即使每条记录属于一个分区,它仍然可以存储在多个不同的节点上以获得容错能力。
+分区通常与复制结合使用,使得每个分区的副本存储在多个节点上。这意味着,即使每条记录属于一个分区,它仍然可以存储在多个不同的节点上以获得容错能力。
 
-一个节点可能存储多个分区。 如果使用主从复制模型,则分区和复制的组合如[图6-1](img/fig6-1.png)所示。 每个分区领导者(主)被分配给一个节点,追随者(从)被分配给其他节点。 每个节点可能是某些分区的领导者,同时是其他分区的追随者。
-我们在[第五章](ch5.md)讨论的关于数据库复制的所有内容同样适用于分区的复制。 大多数情况下,分区方案的选择与复制方案的选择是独立的,为简单起见,本章中将忽略复制。
+一个节点可能存储多个分区。如果使用主从复制模型,则分区和复制的组合如 [图 6-1](img/fig6-1.png) 所示。每个分区领导者(主)被分配给一个节点,追随者(从)被分配给其他节点。 每个节点可能是某些分区的领导者,同时是其他分区的追随者。
+
+我们在 [第五章](ch5.md) 讨论的关于数据库复制的所有内容同样适用于分区的复制。大多数情况下,分区方案的选择与复制方案的选择是独立的,为简单起见,本章中将忽略复制。
 
 ![](img/fig6-1.png)
 
-**图6-1 组合使用复制和分区:每个节点充当某些分区的领导者,其他分区充当追随者。**
+**图 6-1 组合使用复制和分区:每个节点充当某些分区的领导者,其他分区充当追随者。**
 
 ## 键值数据的分区
 
 假设你有大量数据并且想要分区,如何决定在哪些节点上存储哪些记录呢?
 
-分区目标是将数据和查询负载均匀分布在各个节点上。如果每个节点公平分享数据和负载,那么理论上10个节点应该能够处理10倍的数据量和10倍的单个节点的读写吞吐量(暂时忽略复制)。
+分区目标是将数据和查询负载均匀分布在各个节点上。如果每个节点公平分享数据和负载,那么理论上 10 个节点应该能够处理 10 倍的数据量和 10 倍的单个节点的读写吞吐量(暂时忽略复制)。
 
-如果分区是不公平的,一些分区比其他分区有更多的数据或查询,我们称之为**偏斜(skew)**。数据偏斜的存在使分区效率下降很多。在极端的情况下,所有的负载可能压在一个分区上,其余9个节点空闲的,瓶颈落在这一个繁忙的节点上。不均衡导致的高负载的分区被称为**热点(hot spot)**。
+如果分区是不公平的,一些分区比其他分区有更多的数据或查询,我们称之为 **偏斜(skew)**。数据偏斜的存在使分区效率下降很多。在极端的情况下,所有的负载可能压在一个分区上,其余 9 个节点空闲的,瓶颈落在这一个繁忙的节点上。不均衡导致的高负载的分区被称为 **热点(hot spot)**。
 
 避免热点最简单的方法是将记录随机分配给节点。这将在所有节点上平均分配数据,但是它有一个很大的缺点:当你试图读取一个特定的值时,你无法知道它在哪个节点上,所以你必须并行地查询所有的节点。
 
@@ -55,19 +56,19 @@
 
 ### 根据键的范围分区
 
-一种分区的方法是为每个分区指定一块连续的键范围(从最小值到最大值),如纸质百科全书的卷([图6-2](img/fig6-2.png))。如果知道范围之间的边界,则可以轻松确定哪个分区包含某个值。如果你还知道分区所在的节点,那么可以直接向相应的节点发出请求(对于百科全书而言,就像从书架上选取正确的书籍)。
+一种分区的方法是为每个分区指定一块连续的键范围(从最小值到最大值),如纸质百科全书的卷([图 6-2](img/fig6-2.png))。如果知道范围之间的边界,则可以轻松确定哪个分区包含某个值。如果你还知道分区所在的节点,那么可以直接向相应的节点发出请求(对于百科全书而言,就像从书架上选取正确的书籍)。
 
 ![](img/fig6-2.png)
 
-**图6-2 印刷版百科全书按照关键字范围进行分区**
+**图 6-2 印刷版百科全书按照关键字范围进行分区**
 
-键的范围不一定均匀分布,因为数据也很可能不均匀分布。例如在[图6-2](img/fig6-2.png)中,第1卷包含以A和B开头的单词,但第12卷则包含以T,U,V,X,Y和Z开头的单词。只是简单的规定每个卷包含两个字母会导致一些卷比其他卷大。为了均匀分配数据,分区边界需要依据数据调整。
+键的范围不一定均匀分布,因为数据也很可能不均匀分布。例如在 [图 6-2](img/fig6-2.png) 中,第 1 卷包含以 A 和 B 开头的单词,但第 12 卷则包含以 T、U、V、X、Y 和 Z 开头的单词。只是简单的规定每个卷包含两个字母会导致一些卷比其他卷大。为了均匀分配数据,分区边界需要依据数据调整。
 
-分区边界可以由管理员手动选择,也可以由数据库自动选择(我们会在“[分区再平衡](#分区再平衡)”中更详细地讨论分区边界的选择)。 Bigtable使用了这种分区策略,以及其开源等价物HBase 【2, 3】,RethinkDB和2.4版本之前的MongoDB 【4】。
+分区边界可以由管理员手动选择,也可以由数据库自动选择(我们会在 “[分区再平衡](#分区再平衡)” 中更详细地讨论分区边界的选择)。 Bigtable 使用了这种分区策略,以及其开源等价物 HBase 【2, 3】,RethinkDB 和 2.4 版本之前的 MongoDB 【4】。
 
-在每个分区中,我们可以按照一定的顺序保存键(请参阅“[SSTables和LSM树](ch3.md#SSTables和LSM树)”)。好处是进行范围扫描非常简单,你可以将键作为联合索引来处理,以便在一次查询中获取多个相关记录(请参阅“[多列索引](ch3.md#多列索引)”)。例如,假设我们有一个程序来存储传感器网络的数据,其中主键是测量的时间戳(年月日时分秒)。范围扫描在这种情况下非常有用,因为我们可以轻松获取某个月份的所有数据。
+在每个分区中,我们可以按照一定的顺序保存键(请参阅 “[SSTables 和 LSM 树](ch3.md#SSTables和LSM树)”)。好处是进行范围扫描非常简单,你可以将键作为联合索引来处理,以便在一次查询中获取多个相关记录(请参阅 “[多列索引](ch3.md#多列索引)”)。例如,假设我们有一个程序来存储传感器网络的数据,其中主键是测量的时间戳(年月日时分秒)。范围扫描在这种情况下非常有用,因为我们可以轻松获取某个月份的所有数据。
 
-然而,Key Range分区的缺点是某些特定的访问模式会导致热点。 如果主键是时间戳,则分区对应于时间范围,例如,给每天分配一个分区。 不幸的是,由于我们在测量发生时将数据从传感器写入数据库,因此所有写入操作都会转到同一个分区(即今天的分区),这样分区可能会因写入而过载,而其他分区则处于空闲状态【5】。
+然而,Key Range 分区的缺点是某些特定的访问模式会导致热点。 如果主键是时间戳,则分区对应于时间范围,例如,给每天分配一个分区。 不幸的是,由于我们在测量发生时将数据从传感器写入数据库,因此所有写入操作都会转到同一个分区(即今天的分区),这样分区可能会因写入而过载,而其他分区则处于空闲状态【5】。
 
 为了避免传感器数据库中的这个问题,需要使用除了时间戳以外的其他东西作为主键的第一个部分。 例如,可以在每个时间戳前添加传感器名称,这样会首先按传感器名称,然后按时间进行分区。 假设有多个传感器同时运行,写入负载将最终均匀分布在不同分区上。 现在,当想要在一个时间范围内获取多个传感器的值时,你需要为每个传感器名称执行一个单独的范围查询。
 
@@ -75,39 +76,39 @@
 
 由于偏斜和热点的风险,许多分布式数据存储使用散列函数来确定给定键的分区。
 
-一个好的散列函数可以将偏斜的数据均匀分布。假设你有一个32位散列函数,无论何时给定一个新的字符串输入,它将返回一个0到$2^{32}$ -1之间的“随机”数。即使输入的字符串非常相似,它们的散列也会均匀分布在这个数字范围内。
+一个好的散列函数可以将偏斜的数据均匀分布。假设你有一个 32 位散列函数,无论何时给定一个新的字符串输入,它将返回一个 0 到 $2^{32}$ -1 之间的 “随机” 数。即使输入的字符串非常相似,它们的散列也会均匀分布在这个数字范围内。
 
-出于分区的目的,散列函数不需要多么强壮的加密算法:例如,Cassandra和MongoDB使用MD5,Voldemort使用Fowler-Noll-Vo函数。许多编程语言都有内置的简单哈希函数(它们用于散列表),但是它们可能不适合分区:例如,在Java的`Object.hashCode()`和Ruby的`Object#hash`,同一个键可能在不同的进程中有不同的哈希值【6】。
+出于分区的目的,散列函数不需要多么强壮的加密算法:例如,Cassandra 和 MongoDB 使用 MD5,Voldemort 使用 Fowler-Noll-Vo 函数。许多编程语言都有内置的简单哈希函数(它们用于散列表),但是它们可能不适合分区:例如,在 Java 的 `Object.hashCode()` 和 Ruby 的 `Object#hash`,同一个键可能在不同的进程中有不同的哈希值【6】。
 
-一旦你有一个合适的键散列函数,你可以为每个分区分配一个散列范围(而不是键的范围),每个通过哈希散列落在分区范围内的键将被存储在该分区中。如[图6-3](img/fig6-3.png)所示。
+一旦你有一个合适的键散列函数,你可以为每个分区分配一个散列范围(而不是键的范围),每个通过哈希散列落在分区范围内的键将被存储在该分区中。如 [图 6-3](img/fig6-3.png) 所示。
 
 ![](img/fig6-3.png)
 
-**图6-3 按哈希键分区**
+**图 6-3 按哈希键分区**
 
-这种技术擅长在分区之间公平地分配键。分区边界可以是均匀间隔的,也可以是伪随机选择的(在这种情况下,该技术有时也被称为**一致性哈希**,即consistent hashing)。
+这种技术擅长在分区之间公平地分配键。分区边界可以是均匀间隔的,也可以是伪随机选择的(在这种情况下,该技术有时也被称为 **一致性哈希**,即 consistent hashing)。
 
 > #### 一致性哈希
 >
-> 一致性哈希由Karger等人定义。【7】 用于跨互联网级别的缓存系统,例如CDN中,是一种能均匀分配负载的方法。它使用随机选择的 **分区边界(partition boundaries)** 来避免中央控制或分布式共识的需要。 请注意,这里的一致性与复制一致性(请参阅[第五章](ch5.md))或ACID一致性(请参阅[第七章](ch7.md))无关,而只是描述了一种重新平衡(reblancing)的特定方法。
+> 一致性哈希由 Karger 等人定义。【7】 用于跨互联网级别的缓存系统,例如 CDN 中,是一种能均匀分配负载的方法。它使用随机选择的 **分区边界(partition boundaries)** 来避免中央控制或分布式共识的需要。 请注意,这里的一致性与复制一致性(请参阅 [第五章](ch5.md))或 ACID 一致性(请参阅 [第七章](ch7.md))无关,而只是描述了一种重新平衡(reblancing)的特定方法。
 >
-> 正如我们将在“[分区再平衡](#分区再平衡)”中所看到的,这种特殊的方法对于数据库实际上并不是很好,所以在实际中很少使用(某些数据库的文档仍然会使用一致性哈希的说法,但是它往往是不准确的)。 因为有可能产生混淆,所以最好避免使用一致性哈希这个术语,而只是把它称为**散列分区(hash partitioning)**。
+> 正如我们将在 “[分区再平衡](#分区再平衡)” 中所看到的,这种特殊的方法对于数据库实际上并不是很好,所以在实际中很少使用(某些数据库的文档仍然会使用一致性哈希的说法,但是它往往是不准确的)。 因为有可能产生混淆,所以最好避免使用一致性哈希这个术语,而只是把它称为 **散列分区(hash partitioning)**。
 
-不幸的是,通过使用键散列进行分区,我们失去了键范围分区的一个很好的属性:高效执行范围查询的能力。曾经相邻的键现在分散在所有分区中,所以它们之间的顺序就丢失了。在MongoDB中,如果你使用了基于散列的分区模式,则任何范围查询都必须发送到所有分区【4】。Riak【9】、Couchbase 【10】或Voldemort不支持主键上的范围查询。
+不幸的是,通过使用键散列进行分区,我们失去了键范围分区的一个很好的属性:高效执行范围查询的能力。曾经相邻的键现在分散在所有分区中,所以它们之间的顺序就丢失了。在 MongoDB 中,如果你使用了基于散列的分区模式,则任何范围查询都必须发送到所有分区【4】。Riak【9】、Couchbase 【10】或 Voldemort 不支持主键上的范围查询。
 
-Cassandra采取了折衷的策略【11, 12, 13】。 Cassandra中的表可以使用由多个列组成的复合主键来声明。键中只有第一列会作为散列的依据,而其他列则被用作Casssandra的SSTables中排序数据的连接索引。尽管查询无法在复合主键的第一列中按范围扫表,但如果第一列已经指定了固定值,则可以对该键的其他列执行有效的范围扫描。
+Cassandra 采取了折衷的策略【11, 12, 13】。 Cassandra 中的表可以使用由多个列组成的复合主键来声明。键中只有第一列会作为散列的依据,而其他列则被用作 Casssandra 的 SSTables 中排序数据的连接索引。尽管查询无法在复合主键的第一列中按范围扫表,但如果第一列已经指定了固定值,则可以对该键的其他列执行有效的范围扫描。
 
-组合索引方法为一对多关系提供了一个优雅的数据模型。例如,在社交媒体网站上,一个用户可能会发布很多更新。如果更新的主键被选择为`(user_id, update_timestamp)`,那么你可以有效地检索特定用户在某个时间间隔内按时间戳排序的所有更新。不同的用户可以存储在不同的分区上,对于每个用户,更新按时间戳顺序存储在单个分区上。
+组合索引方法为一对多关系提供了一个优雅的数据模型。例如,在社交媒体网站上,一个用户可能会发布很多更新。如果更新的主键被选择为 `(user_id, update_timestamp)`,那么你可以有效地检索特定用户在某个时间间隔内按时间戳排序的所有更新。不同的用户可以存储在不同的分区上,对于每个用户,更新按时间戳顺序存储在单个分区上。
 
 ### 负载偏斜与热点消除
 
 如前所述,哈希分区可以帮助减少热点。但是,它不能完全避免它们:在极端情况下,所有的读写操作都是针对同一个键的,所有的请求都会被路由到同一个分区。
 
-这种场景也许并不常见,但并非闻所未闻:例如,在社交媒体网站上,一个拥有数百万追随者的名人用户在做某事时可能会引发一场风暴【14】。这个事件可能导致同一个键的大量写入(键可能是名人的用户ID,或者人们正在评论的动作的ID)。哈希策略不起作用,因为两个相同ID的哈希值仍然是相同的。
+这种场景也许并不常见,但并非闻所未闻:例如,在社交媒体网站上,一个拥有数百万追随者的名人用户在做某事时可能会引发一场风暴【14】。这个事件可能导致同一个键的大量写入(键可能是名人的用户 ID,或者人们正在评论的动作的 ID)。哈希策略不起作用,因为两个相同 ID 的哈希值仍然是相同的。
 
-如今,大多数数据系统无法自动补偿这种高度偏斜的负载,因此应用程序有责任减少偏斜。例如,如果一个主键被认为是非常火爆的,一个简单的方法是在主键的开始或结尾添加一个随机数。只要一个两位数的十进制随机数就可以将主键分散为100种不同的主键,从而存储在不同的分区中。
+如今,大多数数据系统无法自动补偿这种高度偏斜的负载,因此应用程序有责任减少偏斜。例如,如果一个主键被认为是非常火爆的,一个简单的方法是在主键的开始或结尾添加一个随机数。只要一个两位数的十进制随机数就可以将主键分散为 100 种不同的主键,从而存储在不同的分区中。
 
-然而,将主键进行分割之后,任何读取都必须要做额外的工作,因为他们必须从所有100个主键分布中读取数据并将其合并。此技术还需要额外的记录:只需要对少量热点附加随机数;对于写入吞吐量低的绝大多数主键来说是不必要的开销。因此,你还需要一些方法来跟踪哪些键需要被分割。
+然而,将主键进行分割之后,任何读取都必须要做额外的工作,因为他们必须从所有 100 个主键分布中读取数据并将其合并。此技术还需要额外的记录:只需要对少量热点附加随机数;对于写入吞吐量低的绝大多数主键来说是不必要的开销。因此,你还需要一些方法来跟踪哪些键需要被分割。
 
 也许在将来,数据系统将能够自动检测和补偿偏斜的工作负载;但现在,你需要自己来权衡。
 
@@ -117,132 +118,132 @@ Cassandra采取了折衷的策略【11, 12, 13】。 Cassandra中的表可以使
 
 到目前为止,我们讨论的分区方案依赖于键值数据模型。如果只通过主键访问记录,我们可以从该键确定分区,并使用它来将读写请求路由到负责该键的分区。
 
-如果涉及次级索引,情况会变得更加复杂(参考“[其他索引结构](ch3.md#其他索引结构)”)。次级索引通常并不能唯一地标识记录,而是一种搜索记录中出现特定值的方式:查找用户123的所有操作,查找包含词语`hogwash`的所有文章,查找所有颜色为红色的车辆等等。
+如果涉及次级索引,情况会变得更加复杂(参考 “[其他索引结构](ch3.md#其他索引结构)”)。次级索引通常并不能唯一地标识记录,而是一种搜索记录中出现特定值的方式:查找用户 123 的所有操作,查找包含词语 `hogwash` 的所有文章,查找所有颜色为红色的车辆等等。
 
-次级索引是关系型数据库的基础,并且在文档数据库中也很普遍。许多键值存储(如HBase和Volde-mort)为了减少实现的复杂度而放弃了次级索引,但是一些(如Riak)已经开始添加它们,因为它们对于数据模型实在是太有用了。并且次级索引也是Solr和Elasticsearch等搜索服务器的基石。
+次级索引是关系型数据库的基础,并且在文档数据库中也很普遍。许多键值存储(如 HBase 和 Volde-mort)为了减少实现的复杂度而放弃了次级索引,但是一些(如 Riak)已经开始添加它们,因为它们对于数据模型实在是太有用了。并且次级索引也是 Solr 和 Elasticsearch 等搜索服务器的基石。
 
-次级索引的问题是它们不能整齐地映射到分区。有两种用次级索引对数据库进行分区的方法:**基于文档的分区(document-based)** 和**基于关键词(term-based)的分区**。
+次级索引的问题是它们不能整齐地映射到分区。有两种用次级索引对数据库进行分区的方法:**基于文档的分区(document-based)** 和 **基于关键词(term-based)的分区**。
 
 ### 基于文档的次级索引进行分区
 
-假设你正在经营一个销售二手车的网站(如[图6-4](img/fig6-4.png)所示)。 每个列表都有一个唯一的ID——称之为文档ID——并且用文档ID对数据库进行分区(例如,分区0中的ID 0到499,分区1中的ID 500到999等)。
+假设你正在经营一个销售二手车的网站(如 [图 6-4](img/fig6-4.png) 所示)。 每个列表都有一个唯一的 ID—— 称之为文档 ID—— 并且用文档 ID 对数据库进行分区(例如,分区 0 中的 ID 0 到 499,分区 1 中的 ID 500 到 999 等)。
 
-你想让用户搜索汽车,允许他们通过颜色和厂商过滤,所以需要一个在颜色和厂商上的次级索引(文档数据库中这些是**字段(field)**,关系数据库中这些是**列(column)** )。 如果你声明了索引,则数据库可以自动执行索引[^ii]。例如,无论何时将红色汽车添加到数据库,数据库分区都会自动将其添加到索引条目`color:red`的文档ID列表中。
+你想让用户搜索汽车,允许他们通过颜色和厂商过滤,所以需要一个在颜色和厂商上的次级索引(文档数据库中这些是 **字段(field)**,关系数据库中这些是 **列(column)** )。 如果你声明了索引,则数据库可以自动执行索引 [^ii]。例如,无论何时将红色汽车添加到数据库,数据库分区都会自动将其添加到索引条目 `color:red` 的文档 ID 列表中。
 
-[^ii]: 如果数据库仅支持键值模型,则你可能会尝试在应用程序代码中创建从值到文档ID的映射来实现次级索引。 如果沿着这条路线走下去,请万分小心,确保你的索引与底层数据保持一致。 竞争条件和间歇性写入失败(其中一些更改已保存,但其他更改未保存)很容易导致数据不同步 - 请参阅“[多对象事务的需求](ch7.md#多对象事务的需求)”。
+[^ii]: 如果数据库仅支持键值模型,则你可能会尝试在应用程序代码中创建从值到文档 ID 的映射来实现次级索引。 如果沿着这条路线走下去,请万分小心,确保你的索引与底层数据保持一致。 竞争条件和间歇性写入失败(其中一些更改已保存,但其他更改未保存)很容易导致数据不同步 - 请参阅 “[多对象事务的需求](ch7.md#多对象事务的需求)”。
 
 ![](img/fig6-4.png)
 
-**图6-4 基于文档的次级索引进行分区**
+**图 6-4 基于文档的次级索引进行分区**
 
-在这种索引方法中,每个分区是完全独立的:每个分区维护自己的次级索引,仅覆盖该分区中的文档。它不关心存储在其他分区的数据。无论何时你需要写入数据库(添加,删除或更新文档),只需处理包含你正在编写的文档ID的分区即可。出于这个原因,**文档分区索引**也被称为**本地索引**(而不是将在下一节中描述的**全局索引**)。
+在这种索引方法中,每个分区是完全独立的:每个分区维护自己的次级索引,仅覆盖该分区中的文档。它不关心存储在其他分区的数据。无论何时你需要写入数据库(添加,删除或更新文档),只需处理包含你正在编写的文档 ID 的分区即可。出于这个原因,**文档分区索引** 也被称为 **本地索引**(而不是将在下一节中描述的 **全局索引**)。
 
-但是,从文档分区索引中读取需要注意:除非你对文档ID做了特别的处理,否则没有理由将所有具有特定颜色或特定品牌的汽车放在同一个分区中。在[图6-4](img/fig6-4.png)中,红色汽车出现在分区0和分区1中。因此,如果要搜索红色汽车,则需要将查询发送到所有分区,并合并所有返回的结果。
+但是,从文档分区索引中读取需要注意:除非你对文档 ID 做了特别的处理,否则没有理由将所有具有特定颜色或特定品牌的汽车放在同一个分区中。在 [图 6-4](img/fig6-4.png) 中,红色汽车出现在分区 0 和分区 1 中。因此,如果要搜索红色汽车,则需要将查询发送到所有分区,并合并所有返回的结果。
 
 
-这种查询分区数据库的方法有时被称为**分散/聚集(scatter/gather)**,并且可能会使次级索引上的读取查询相当昂贵。即使并行查询分区,分散/聚集也容易导致尾部延迟放大(请参阅“[实践中的百分位点](ch1.md#实践中的百分位点)”)。然而,它被广泛使用:MongoDB,Riak 【15】,Cassandra 【16】,Elasticsearch 【17】,SolrCloud 【18】和VoltDB 【19】都使用文档分区次级索引。大多数数据库供应商建议你构建一个能从单个分区提供次级索引查询的分区方案,但这并不总是可行,尤其是当在单个查询中使用多个次级索引时(例如同时需要按颜色和制造商查询)。
+这种查询分区数据库的方法有时被称为 **分散 / 聚集(scatter/gather)**,并且可能会使次级索引上的读取查询相当昂贵。即使并行查询分区,分散 / 聚集也容易导致尾部延迟放大(请参阅 “[实践中的百分位点](ch1.md#实践中的百分位点)”)。然而,它被广泛使用:MongoDB,Riak 【15】,Cassandra 【16】,Elasticsearch 【17】,SolrCloud 【18】和 VoltDB 【19】都使用文档分区次级索引。大多数数据库供应商建议你构建一个能从单个分区提供次级索引查询的分区方案,但这并不总是可行,尤其是当在单个查询中使用多个次级索引时(例如同时需要按颜色和制造商查询)。
 
 
 ### 基于关键词(Term)的次级索引进行分区
 
-我们可以构建一个覆盖所有分区数据的**全局索引**,而不是给每个分区创建自己的次级索引(本地索引)。但是,我们不能只把这个索引存储在一个节点上,因为它可能会成为瓶颈,违背了分区的目的。全局索引也必须进行分区,但可以采用与主键不同的分区方式。
+我们可以构建一个覆盖所有分区数据的 **全局索引**,而不是给每个分区创建自己的次级索引(本地索引)。但是,我们不能只把这个索引存储在一个节点上,因为它可能会成为瓶颈,违背了分区的目的。全局索引也必须进行分区,但可以采用与主键不同的分区方式。
 
-[图6-5](img/fig6-5.png)描述了这可能是什么样子:来自所有分区的红色汽车在红色索引中,并且索引是分区的,首字母从`a`到`r`的颜色在分区0中,`s`到`z`的在分区1。汽车制造商的索引也与之类似(分区边界在`f`和`h`之间)。
+[图 6-5](img/fig6-5.png) 描述了这可能是什么样子:来自所有分区的红色汽车在红色索引中,并且索引是分区的,首字母从 `a` 到 `r` 的颜色在分区 0 中,`s` 到 `z` 的在分区 1。汽车制造商的索引也与之类似(分区边界在 `f` 和 `h` 之间)。
 
 ![](img/fig6-5.png)
 
-**图6-5 基于关键词对次级索引进行分区**
+**图 6-5 基于关键词对次级索引进行分区**
 
-我们将这种索引称为**关键词分区(term-partitioned)**,因为我们寻找的关键词决定了索引的分区方式。例如,一个关键词可能是:`color:red`。**关键词(Term)** 这个名称来源于全文搜索索引(一种特殊的次级索引),指文档中出现的所有单词。
+我们将这种索引称为 **关键词分区(term-partitioned)**,因为我们寻找的关键词决定了索引的分区方式。例如,一个关键词可能是:`color:red`。**关键词(Term)** 这个名称来源于全文搜索索引(一种特殊的次级索引),指文档中出现的所有单词。
 
-和之前一样,我们可以通过**关键词**本身或者它的散列进行索引分区。根据关键词本身来分区对于范围扫描非常有用(例如对于数值类的属性,像汽车的报价),而对关键词的哈希分区提供了负载均衡的能力。
+和之前一样,我们可以通过 **关键词** 本身或者它的散列进行索引分区。根据关键词本身来分区对于范围扫描非常有用(例如对于数值类的属性,像汽车的报价),而对关键词的哈希分区提供了负载均衡的能力。
 
-关键词分区的全局索引优于文档分区索引的地方点是它可以使读取更有效率:不需要**分散/收集**所有分区,客户端只需要向包含关键词的分区发出请求。全局索引的缺点在于写入速度较慢且较为复杂,因为写入单个文档现在可能会影响索引的多个分区(文档中的每个关键词可能位于不同的分区或者不同的节点上) 。
+关键词分区的全局索引优于文档分区索引的地方点是它可以使读取更有效率:不需要 **分散 / 收集** 所有分区,客户端只需要向包含关键词的分区发出请求。全局索引的缺点在于写入速度较慢且较为复杂,因为写入单个文档现在可能会影响索引的多个分区(文档中的每个关键词可能位于不同的分区或者不同的节点上) 。
 
-理想情况下,索引总是最新的,写入数据库的每个文档都会立即反映在索引中。但是,在关键词分区索引中,这需要跨分区的分布式事务,并不是所有数据库都支持(请参阅[第七章](ch7.md)和[第九章](ch9.md))。
+理想情况下,索引总是最新的,写入数据库的每个文档都会立即反映在索引中。但是,在关键词分区索引中,这需要跨分区的分布式事务,并不是所有数据库都支持(请参阅 [第七章](ch7.md) 和 [第九章](ch9.md))。
 
-在实践中,对全局次级索引的更新通常是**异步**的(也就是说,如果在写入之后不久读取索引,刚才所做的更改可能尚未反映在索引中)。例如,Amazon DynamoDB声称在正常情况下,其全局次级索引会在不到一秒的时间内更新,但在基础架构出现故障的情况下可能会有延迟【20】。
+在实践中,对全局次级索引的更新通常是 **异步** 的(也就是说,如果在写入之后不久读取索引,刚才所做的更改可能尚未反映在索引中)。例如,Amazon DynamoDB 声称在正常情况下,其全局次级索引会在不到一秒的时间内更新,但在基础架构出现故障的情况下可能会有延迟【20】。
 
-全局关键词分区索引的其他用途包括Riak的搜索功能【21】和Oracle数据仓库,它允许你在本地和全局索引之间进行选择【22】。我们将在[第十二章](ch12.md)中继续关键词分区次级索引实现的话题。
+全局关键词分区索引的其他用途包括 Riak 的搜索功能【21】和 Oracle 数据仓库,它允许你在本地和全局索引之间进行选择【22】。我们将在 [第十二章](ch12.md) 中继续关键词分区次级索引实现的话题。
 
 ## 分区再平衡
 
 随着时间的推移,数据库会有各种变化:
 
-* 查询吞吐量增加,所以你想要添加更多的CPU来处理负载。
-* 数据集大小增加,所以你想添加更多的磁盘和RAM来存储它。
+* 查询吞吐量增加,所以你想要添加更多的 CPU 来处理负载。
+* 数据集大小增加,所以你想添加更多的磁盘和 RAM 来存储它。
 * 机器出现故障,其他机器需要接管故障机器的责任。
 
-所有这些更改都需要数据和请求从一个节点移动到另一个节点。 将负载从集群中的一个节点向另一个节点移动的过程称为**再平衡(rebalancing)**。
+所有这些更改都需要数据和请求从一个节点移动到另一个节点。 将负载从集群中的一个节点向另一个节点移动的过程称为 **再平衡(rebalancing)**。
 
 无论使用哪种分区方案,再平衡通常都要满足一些最低要求:
 
 * 再平衡之后,负载(数据存储,读取和写入请求)应该在集群中的节点之间公平地共享。
 * 再平衡发生时,数据库应该继续接受读取和写入。
-* 节点之间只移动必须的数据,以便快速再平衡,并减少网络和磁盘I/O负载。
+* 节点之间只移动必须的数据,以便快速再平衡,并减少网络和磁盘 I/O 负载。
 
 
 ### 再平衡策略
 
-有几种不同的分区分配方法【23】,让我们依次简要讨论一下。
+有几种不同的分区分配方法【23】,让我们依次简要讨论一下。
 
 #### 反面教材:hash mod N
 
-我们在前面说过([图6-3](img/fig6-3.png)),最好将可能的散列分成不同的范围,并将每个范围分配给一个分区(例如,如果$0≤hash(key)<b_0$,则将键分配给分区0,如果$b_0 ≤ hash(key) <b_1$,则分配给分区1)
+我们在前面说过([图 6-3](img/fig6-3.png)),最好将可能的散列分成不同的范围,并将每个范围分配给一个分区(例如,如果 $0 ≤ hash(key)< b_0$,则将键分配给分区 0,如果 $b_0 ≤ hash(key) < b_1$,则分配给分区 1)
 
-也许你想知道为什么我们不使用 ***取模(mod)***(许多编程语言中的%运算符)。例如,`hash(key) mod 10`会返回一个介于0和9之间的数字(如果我们将散列写为十进制数,散列模10将是最后一个数字)。如果我们有10个节点,编号为0到9,这似乎是将每个键分配给一个节点的简单方法。
+也许你想知道为什么我们不使用 ***取模(mod)***(许多编程语言中的 % 运算符)。例如,`hash(key) mod 10` 会返回一个介于 0 和 9 之间的数字(如果我们将散列写为十进制数,散列模 10 将是最后一个数字)。如果我们有 10 个节点,编号为 0 到 9,这似乎是将每个键分配给一个节点的简单方法。
 
-模N($mod N$)方法的问题是,如果节点数量N发生变化,大多数键将需要从一个节点移动到另一个节点。例如,假设$hash(key)=123456$。如果最初有10个节点,那么这个键一开始放在节点6上(因为$123456\ mod\  10 = 6$)。当你增长到11个节点时,键需要移动到节点3($123456\ mod\ 11 = 3$),当你增长到12个节点时,需要移动到节点0($123456\ mod\ 12 = 0$)。这种频繁的举动使得重新平衡过于昂贵。
+模 N($mod N$)方法的问题是,如果节点数量 N 发生变化,大多数键将需要从一个节点移动到另一个节点。例如,假设 $hash(key)=123456$。如果最初有 10 个节点,那么这个键一开始放在节点 6 上(因为 $123456\ mod\  10 = 6$)。当你增长到 11 个节点时,键需要移动到节点 3($123456\ mod\ 11 = 3$),当你增长到 12 个节点时,需要移动到节点 0($123456\ mod\ 12 = 0$)。这种频繁的举动使得重新平衡过于昂贵。
 
 我们需要一种只移动必需数据的方法。
 
 #### 固定数量的分区
 
-幸运的是,有一个相当简单的解决方案:创建比节点更多的分区,并为每个节点分配多个分区。例如,运行在10个节点的集群上的数据库可能会从一开始就被拆分为1,000个分区,因此大约有100个分区被分配给每个节点。
+幸运的是,有一个相当简单的解决方案:创建比节点更多的分区,并为每个节点分配多个分区。例如,运行在 10 个节点的集群上的数据库可能会从一开始就被拆分为 1,000 个分区,因此大约有 100 个分区被分配给每个节点。
 
-现在,如果一个节点被添加到集群中,新节点可以从当前每个节点中**窃取**一些分区,直到分区再次公平分配。这个过程如[图6-6](img/fig6-6.png)所示。如果从集群中删除一个节点,则会发生相反的情况。
+现在,如果一个节点被添加到集群中,新节点可以从当前每个节点中 **窃取** 一些分区,直到分区再次公平分配。这个过程如 [图 6-6](img/fig6-6.png) 所示。如果从集群中删除一个节点,则会发生相反的情况。
 
 只有分区在节点之间的移动。分区的数量不会改变,键所指定的分区也不会改变。唯一改变的是分区所在的节点。这种变更并不是即时的 — 在网络上传输大量的数据需要一些时间 — 所以在传输过程中,原有分区仍然会接受读写操作。
 
 ![](img/fig6-6.png)
 
-**图6-6 将新节点添加到每个节点具有多个分区的数据库集群。**
+**图 6-6 将新节点添加到每个节点具有多个分区的数据库集群。**
 
-原则上,你甚至可以解决集群中的硬件不匹配问题:通过为更强大的节点分配更多的分区,可以强制这些节点承载更多的负载。在Riak 【15】,Elasticsearch 【24】,Couchbase 【10】和Voldemort 【25】中使用了这种再平衡的方法。
+原则上,你甚至可以解决集群中的硬件不匹配问题:通过为更强大的节点分配更多的分区,可以强制这些节点承载更多的负载。在 Riak 【15】、Elasticsearch 【24】、Couchbase 【10】和 Voldemort 【25】中使用了这种再平衡的方法。
 
 在这种配置中,分区的数量通常在数据库第一次建立时确定,之后不会改变。虽然原则上可以分割和合并分区(请参阅下一节),但固定数量的分区在操作上更简单,因此许多固定分区数据库选择不实施分区分割。因此,一开始配置的分区数就是你可以拥有的最大节点数量,所以你需要选择足够多的分区以适应未来的增长。但是,每个分区也有管理开销,所以选择太大的数字会适得其反。
 
-如果数据集的总大小难以预估(例如,可能它开始很小,但随着时间的推移会变得更大),选择正确的分区数是困难的。由于每个分区包含了总数据量固定比率的数据,因此每个分区的大小与集群中的数据总量成比例增长。如果分区非常大,再平衡和从节点故障恢复变得昂贵。但是,如果分区太小,则会产生太多的开销。当分区大小“恰到好处”的时候才能获得很好的性能,如果分区数量固定,但数据量变动很大,则难以达到最佳性能。
+如果数据集的总大小难以预估(例如,可能它开始很小,但随着时间的推移会变得更大),选择正确的分区数是困难的。由于每个分区包含了总数据量固定比率的数据,因此每个分区的大小与集群中的数据总量成比例增长。如果分区非常大,再平衡和从节点故障恢复变得昂贵。但是,如果分区太小,则会产生太多的开销。当分区大小 “恰到好处” 的时候才能获得很好的性能,如果分区数量固定,但数据量变动很大,则难以达到最佳性能。
 
 #### 动态分区
 
-对于使用键范围分区的数据库(请参阅“[根据键的范围分区](#根据键的范围分区)”),具有固定边界的固定数量的分区将非常不便:如果出现边界错误,则可能会导致一个分区中的所有数据或者其他分区中的所有数据为空。手动重新配置分区边界将非常繁琐。
+对于使用键范围分区的数据库(请参阅 “[根据键的范围分区](#根据键的范围分区)”),具有固定边界的固定数量的分区将非常不便:如果出现边界错误,则可能会导致一个分区中的所有数据或者其他分区中的所有数据为空。手动重新配置分区边界将非常繁琐。
 
-出于这个原因,按键的范围进行分区的数据库(如HBase和RethinkDB)会动态创建分区。当分区增长到超过配置的大小时(在HBase上,默认值是10GB),会被分成两个分区,每个分区约占一半的数据【26】。与之相反,如果大量数据被删除并且分区缩小到某个阈值以下,则可以将其与相邻分区合并。此过程与B树顶层发生的过程类似(请参阅“[B树](ch3.md#B树)”)。
+出于这个原因,按键的范围进行分区的数据库(如 HBase 和 RethinkDB)会动态创建分区。当分区增长到超过配置的大小时(在 HBase 上,默认值是 10GB),会被分成两个分区,每个分区约占一半的数据【26】。与之相反,如果大量数据被删除并且分区缩小到某个阈值以下,则可以将其与相邻分区合并。此过程与 B 树顶层发生的过程类似(请参阅 “[B 树](ch3.md#B树)”)。
 
-每个分区分配给一个节点,每个节点可以处理多个分区,就像固定数量的分区一样。大型分区拆分后,可以将其中的一半转移到另一个节点,以平衡负载。在HBase中,分区文件的传输通过HDFS(底层使用的分布式文件系统)来实现【3】。
+每个分区分配给一个节点,每个节点可以处理多个分区,就像固定数量的分区一样。大型分区拆分后,可以将其中的一半转移到另一个节点,以平衡负载。在 HBase 中,分区文件的传输通过 HDFS(底层使用的分布式文件系统)来实现【3】。
 
 动态分区的一个优点是分区数量适应总数据量。如果只有少量的数据,少量的分区就足够了,所以开销很小;如果有大量的数据,每个分区的大小被限制在一个可配置的最大值【23】。
 
-需要注意的是,一个空的数据库从一个分区开始,因为没有关于在哪里绘制分区边界的先验信息。数据集开始时很小,直到达到第一个分区的分割点,所有写入操作都必须由单个节点处理,而其他节点则处于空闲状态。为了解决这个问题,HBase和MongoDB允许在一个空的数据库上配置一组初始分区(这被称为**预分割**,即pre-splitting)。在键范围分区的情况中,预分割需要提前知道键是如何进行分配的【4,26】。
+需要注意的是,一个空的数据库从一个分区开始,因为没有关于在哪里绘制分区边界的先验信息。数据集开始时很小,直到达到第一个分区的分割点,所有写入操作都必须由单个节点处理,而其他节点则处于空闲状态。为了解决这个问题,HBase 和 MongoDB 允许在一个空的数据库上配置一组初始分区(这被称为 **预分割**,即 pre-splitting)。在键范围分区的情况中,预分割需要提前知道键是如何进行分配的【4,26】。
 
-动态分区不仅适用于数据的范围分区,而且也适用于散列分区。从版本2.4开始,MongoDB同时支持范围和散列分区,并且都支持动态分割分区。
+动态分区不仅适用于数据的范围分区,而且也适用于散列分区。从版本 2.4 开始,MongoDB 同时支持范围和散列分区,并且都支持动态分割分区。
 
 #### 按节点比例分区
 
 通过动态分区,分区的数量与数据集的大小成正比,因为拆分和合并过程将每个分区的大小保持在固定的最小值和最大值之间。另一方面,对于固定数量的分区,每个分区的大小与数据集的大小成正比。在这两种情况下,分区的数量都与节点的数量无关。
 
-Cassandra和Ketama使用的第三种方法是使分区数与节点数成正比——换句话说,每个节点具有固定数量的分区【23,27,28】。在这种情况下,每个分区的大小与数据集大小成比例地增长,而节点数量保持不变,但是当增加节点数时,分区将再次变小。由于较大的数据量通常需要较大数量的节点进行存储,因此这种方法也使每个分区的大小较为稳定。
+Cassandra 和 Ketama 使用的第三种方法是使分区数与节点数成正比 —— 换句话说,每个节点具有固定数量的分区【23,27,28】。在这种情况下,每个分区的大小与数据集大小成比例地增长,而节点数量保持不变,但是当增加节点数时,分区将再次变小。由于较大的数据量通常需要较大数量的节点进行存储,因此这种方法也使每个分区的大小较为稳定。
 
-当一个新节点加入集群时,它随机选择固定数量的现有分区进行拆分,然后占有这些拆分分区中每个分区的一半,同时将每个分区的另一半留在原地。随机化可能会产生不公平的分割,但是平均在更大数量的分区上时(在Cassandra中,默认情况下,每个节点有256个分区),新节点最终从现有节点获得公平的负载份额。 Cassandra 3.0引入了另一种再平衡的算法来避免不公平的分割【29】。
+当一个新节点加入集群时,它随机选择固定数量的现有分区进行拆分,然后占有这些拆分分区中每个分区的一半,同时将每个分区的另一半留在原地。随机化可能会产生不公平的分割,但是平均在更大数量的分区上时(在 Cassandra 中,默认情况下,每个节点有 256 个分区),新节点最终从现有节点获得公平的负载份额。 Cassandra 3.0 引入了另一种再平衡的算法来避免不公平的分割【29】。
 
-随机选择分区边界要求使用基于散列的分区(可以从散列函数产生的数字范围中挑选边界)。实际上,这种方法最符合一致性哈希的原始定义【7】(请参阅“[一致性哈希](#一致性哈希)”)。最新的哈希函数可以在较低元数据开销的情况下达到类似的效果【8】。
+随机选择分区边界要求使用基于散列的分区(可以从散列函数产生的数字范围中挑选边界)。实际上,这种方法最符合一致性哈希的原始定义【7】(请参阅 “[一致性哈希](#一致性哈希)”)。最新的哈希函数可以在较低元数据开销的情况下达到类似的效果【8】。
 
 ### 运维:手动还是自动再平衡
 
 关于再平衡有一个重要问题:自动还是手动进行?
 
-在全自动重新平衡(系统自动决定何时将分区从一个节点移动到另一个节点,无须人工干预)和完全手动(分区指派给节点由管理员明确配置,仅在管理员明确重新配置时才会更改)之间有一个权衡。例如,Couchbase,Riak和Voldemort会自动生成建议的分区分配,但需要管理员提交才能生效。
+在全自动重新平衡(系统自动决定何时将分区从一个节点移动到另一个节点,无须人工干预)和完全手动(分区指派给节点由管理员明确配置,仅在管理员明确重新配置时才会更改)之间有一个权衡。例如,Couchbase、Riak 和 Voldemort 会自动生成建议的分区分配,但需要管理员提交才能生效。
 
 全自动重新平衡可以很方便,因为正常维护的操作工作较少。但是,这可能是不可预测的。再平衡是一个昂贵的操作,因为它需要重新路由请求并将大量数据从一个节点移动到另一个节点。如果没有做好,这个过程可能会使网络或节点负载过重,降低其他请求的性能。
 
@@ -252,45 +253,45 @@ Cassandra和Ketama使用的第三种方法是使分区数与节点数成正比
 
 ## 请求路由
 
-现在我们已经将数据集分割到多个机器上运行的多个节点上。但是仍然存在一个悬而未决的问题:当客户想要发出请求时,如何知道要连接哪个节点?随着分区重新平衡,分区对节点的分配也发生变化。为了回答这个问题,需要有人知晓这些变化:如果我想读或写键“foo”,需要连接哪个IP地址和端口号?
+现在我们已经将数据集分割到多个机器上运行的多个节点上。但是仍然存在一个悬而未决的问题:当客户想要发出请求时,如何知道要连接哪个节点?随着分区重新平衡,分区对节点的分配也发生变化。为了回答这个问题,需要有人知晓这些变化:如果我想读或写键 “foo”,需要连接哪个 IP 地址和端口号?
 
-这个问题可以概括为 **服务发现(service discovery)** ,它不仅限于数据库。任何可通过网络访问的软件都有这个问题,特别是如果它的目标是高可用性(在多台机器上运行冗余配置)。许多公司已经编写了自己的内部服务发现工具,其中许多已经作为开源发布【30】。
+这个问题可以概括为 **服务发现(service discovery)** ,它不仅限于数据库。任何可通过网络访问的软件都有这个问题,特别是如果它的目标是高可用性(在多台机器上运行冗余配置)。许多公司已经编写了自己的内部服务发现工具,其中许多已经作为开源发布【30】。
 
-概括来说,这个问题有几种不同的方案(如图6-7所示):
+概括来说,这个问题有几种不同的方案(如图 6-7 所示):
 
-1. 允许客户联系任何节点(例如,通过**循环策略的负载均衡**,即Round-Robin Load Balancer)。如果该节点恰巧拥有请求的分区,则它可以直接处理该请求;否则,它将请求转发到适当的节点,接收回复并传递给客户端。
+1. 允许客户联系任何节点(例如,通过 **循环策略的负载均衡**,即 Round-Robin Load Balancer)。如果该节点恰巧拥有请求的分区,则它可以直接处理该请求;否则,它将请求转发到适当的节点,接收回复并传递给客户端。
 2. 首先将所有来自客户端的请求发送到路由层,它决定了应该处理请求的节点,并相应地转发。此路由层本身不处理任何请求;它仅负责分区的负载均衡。
 3. 要求客户端知道分区和节点的分配。在这种情况下,客户端可以直接连接到适当的节点,而不需要任何中介。
 
-以上所有情况中的关键问题是:作出路由决策的组件(可能是节点之一,还是路由层或客户端)如何了解分区-节点之间的分配关系变化?
+以上所有情况中的关键问题是:作出路由决策的组件(可能是节点之一,还是路由层或客户端)如何了解分区 - 节点之间的分配关系变化?
 
 ![](img/fig6-7.png)
 
-**图6-7 将请求路由到正确节点的三种不同方式。**
+**图 6-7 将请求路由到正确节点的三种不同方式。**
 
-这是一个具有挑战性的问题,因为重要的是所有参与者都达成共识 - 否则请求将被发送到错误的节点,得不到正确的处理。 在分布式系统中有达成共识的协议,但很难正确地实现(见[第九章](ch9.md))。
+这是一个具有挑战性的问题,因为重要的是所有参与者都达成共识 - 否则请求将被发送到错误的节点,得不到正确的处理。 在分布式系统中有达成共识的协议,但很难正确地实现(见 [第九章](ch9.md))。
 
-许多分布式数据系统都依赖于一个独立的协调服务,比如ZooKeeper来跟踪集群元数据,如[图6-8](img/fig6-8.png)所示。 每个节点在ZooKeeper中注册自己,ZooKeeper维护分区到节点的可靠映射。 其他参与者(如路由层或分区感知客户端)可以在ZooKeeper中订阅此信息。 只要分区分配发生了改变,或者集群中添加或删除了一个节点,ZooKeeper就会通知路由层使路由信息保持最新状态。
+许多分布式数据系统都依赖于一个独立的协调服务,比如 ZooKeeper 来跟踪集群元数据,如 [图 6-8](img/fig6-8.png) 所示。 每个节点在 ZooKeeper 中注册自己,ZooKeeper 维护分区到节点的可靠映射。 其他参与者(如路由层或分区感知客户端)可以在 ZooKeeper 中订阅此信息。 只要分区分配发生了改变,或者集群中添加或删除了一个节点,ZooKeeper 就会通知路由层使路由信息保持最新状态。
 
 ![](img/fig6-8.png)
 
-**图6-8 使用ZooKeeper跟踪分区分配给节点。**
+**图 6-8 使用 ZooKeeper 跟踪分区分配给节点。**
 
-例如,LinkedIn的Espresso使用Helix 【31】进行集群管理(依靠ZooKeeper),实现了如[图6-8](img/fig6-8.png)所示的路由层。 HBase,SolrCloud和Kafka也使用ZooKeeper来跟踪分区分配。 MongoDB具有类似的体系结构,但它依赖于自己的**配置服务器(config server)** 实现和mongos守护进程作为路由层。
+例如,LinkedIn的Espresso使用Helix 【31】进行集群管理(依靠ZooKeeper),实现了如[图6-8](img/fig6-8.png)所示的路由层。 HBase、SolrCloud和Kafka也使用ZooKeeper来跟踪分区分配。MongoDB具有类似的体系结构,但它依赖于自己的**配置服务器(config server)** 实现和mongos守护进程作为路由层。
 
-Cassandra和Riak采取不同的方法:他们在节点之间使用**流言协议(gossip protocol)** 来传播集群状态的变化。请求可以发送到任意节点,该节点会转发到包含所请求的分区的适当节点([图6-7](img/fig6-7.png)中的方法1)。这个模型在数据库节点中增加了更多的复杂性,但是避免了对像ZooKeeper这样的外部协调服务的依赖。
+Cassandra 和 Riak 采取不同的方法:他们在节点之间使用 **流言协议(gossip protocol)** 来传播集群状态的变化。请求可以发送到任意节点,该节点会转发到包含所请求的分区的适当节点([图 6-7](img/fig6-7.png) 中的方法 1)。这个模型在数据库节点中增加了更多的复杂性,但是避免了对像 ZooKeeper 这样的外部协调服务的依赖。
 
-Couchbase不会自动重新平衡,这简化了设计。通常情况下,它配置了一个名为moxi的路由层,它会从集群节点了解路由变化【32】。
+Couchbase 不会自动重新平衡,这简化了设计。通常情况下,它配置了一个名为 moxi 的路由层,它会从集群节点了解路由变化【32】。
 
-当使用路由层或向随机节点发送请求时,客户端仍然需要找到要连接的IP地址。这些地址并不像分区的节点分布变化的那么快,所以使用DNS通常就足够了。
+当使用路由层或向随机节点发送请求时,客户端仍然需要找到要连接的 IP 地址。这些地址并不像分区的节点分布变化的那么快,所以使用 DNS 通常就足够了。
 
 ### 执行并行查询
 
-到目前为止,我们只关注读取或写入单个键的非常简单的查询(加上基于文档分区的次级索引场景下的分散/聚集查询)。这也是大多数NoSQL分布式数据存储所支持的访问层级。
+到目前为止,我们只关注读取或写入单个键的非常简单的查询(加上基于文档分区的次级索引场景下的分散 / 聚集查询)。这也是大多数 NoSQL 分布式数据存储所支持的访问层级。
 
-然而,通常用于分析的**大规模并行处理(MPP, Massively parallel processing)** 关系型数据库产品在其支持的查询类型方面要复杂得多。一个典型的数据仓库查询包含多个连接,过滤,分组和聚合操作。 MPP查询优化器将这个复杂的查询分解成许多执行阶段和分区,其中许多可以在数据库集群的不同节点上并行执行。涉及扫描大规模数据集的查询特别受益于这种并行执行。
+然而,通常用于分析的 **大规模并行处理(MPP, Massively parallel processing)** 关系型数据库产品在其支持的查询类型方面要复杂得多。一个典型的数据仓库查询包含多个连接,过滤,分组和聚合操作。 MPP 查询优化器将这个复杂的查询分解成许多执行阶段和分区,其中许多可以在数据库集群的不同节点上并行执行。涉及扫描大规模数据集的查询特别受益于这种并行执行。
 
-数据仓库查询的快速并行执行是一个专门的话题,由于分析有很重要的商业意义,可以带来很多利益。我们将在[第十章](ch10.md)讨论并行查询执行的一些技巧。有关并行数据库中使用的技术的更详细的概述,请参阅参考文献【1,33】。
+数据仓库查询的快速并行执行是一个专门的话题,由于分析有很重要的商业意义,可以带来很多利益。我们将在 [第十章](ch10.md) 讨论并行查询执行的一些技巧。有关并行数据库中使用的技术的更详细的概述,请参阅参考文献【1,33】。
 
 ## 本章小结
 
@@ -314,7 +315,7 @@ Couchbase不会自动重新平衡,这简化了设计。通常情况下,它
 
 我们还讨论了分区和次级索引之间的相互作用。次级索引也需要分区,有两种方法:
 
-* 基于文档分区(本地索引),其中次级索引存储在与主键和值相同的分区中。这意味着只有一个分区需要在写入时更新,但是读取次级索引需要在所有分区之间进行分散/收集。
+* 基于文档分区(本地索引),其中次级索引存储在与主键和值相同的分区中。这意味着只有一个分区需要在写入时更新,但是读取次级索引需要在所有分区之间进行分散 / 收集。
 * 基于关键词分区(全局索引),其中次级索引存在不同的分区中。次级索引中的条目可以包括来自主键的所有分区的记录。当文档写入时,需要更新多个分区中的次级索引;但是可以从单个分区中进行读取。
 
 最后,我们讨论了将查询路由到适当的分区的技术,从简单的分区负载平衡到复杂的并行查询执行引擎。
diff --git a/ch7.md b/ch7.md
index d7c047e..a7eb054 100644
--- a/ch7.md
+++ b/ch7.md
@@ -4,7 +4,7 @@
 
 > 一些作者声称,支持通用的两阶段提交代价太大,会带来性能与可用性的问题。让程序员来处理过度使用事务导致的性能问题,总比缺少事务编程好得多。
 >
-> —— James Corbett等人,Spanner:Google的全球分布式数据库(2012)
+> —— James Corbett 等人,Spanner:Google 的全球分布式数据库(2012)
 
 ------
 
@@ -21,36 +21,36 @@
 
 为了实现可靠性,系统必须处理这些故障,确保它们不会导致整个系统的灾难性故障。但是实现容错机制工作量巨大。需要仔细考虑所有可能出错的事情,并进行大量的测试,以确保解决方案真正管用。
 
-数十年来,**事务(transaction)** 一直是简化这些问题的首选机制。事务是应用程序将多个读写操作组合成一个逻辑单元的一种方式。从概念上讲,事务中的所有读写操作被视作单个操作来执行:整个事务要么成功**提交**(commit),要么失败**中止**(abort)或**回滚**(rollback)。如果失败,应用程序可以安全地重试。对于事务来说,应用程序的错误处理变得简单多了,因为它不用再担心部分失败的情况了,即某些操作成功,某些失败(无论出于何种原因)。
+数十年来,**事务(transaction)** 一直是简化这些问题的首选机制。事务是应用程序将多个读写操作组合成一个逻辑单元的一种方式。从概念上讲,事务中的所有读写操作被视作单个操作来执行:整个事务要么成功 **提交**(commit),要么失败 **中止**(abort)或 **回滚**(rollback)。如果失败,应用程序可以安全地重试。对于事务来说,应用程序的错误处理变得简单多了,因为它不用再担心部分失败的情况了,即某些操作成功,某些失败(无论出于何种原因)。
 
-和事务打交道时间长了,你可能会觉得它显而易见。但我们不应将其视为理所当然。事务不是天然存在的;它们是为了**简化应用编程模型**而创建的。通过使用事务,应用程序可以自由地忽略某些潜在的错误情况和并发问题,因为数据库会替应用处理好这些。(我们称之为**安全保证**,即safety guarantees)。
+和事务打交道时间长了,你可能会觉得它显而易见。但我们不应将其视为理所当然。事务不是天然存在的;它们是为了 **简化应用编程模型** 而创建的。通过使用事务,应用程序可以自由地忽略某些潜在的错误情况和并发问题,因为数据库会替应用处理好这些。(我们称之为 **安全保证**,即 safety guarantees)。
 
 并不是所有的应用都需要事务,有时候弱化事务保证、或完全放弃事务也是有好处的(例如,为了获得更高性能或更高可用性)。一些安全属性也可以在没有事务的情况下实现。
 
 怎样知道你是否需要事务?为了回答这个问题,首先需要确切理解事务可以提供的安全保障,以及它们的代价。尽管乍看事务似乎很简单,但实际上有许多微妙但重要的细节在起作用。
 
-本章将研究许多出错案例,并探索数据库用于防范这些问题的算法。尤其会深入**并发控制**的领域,讨论各种可能发生的竞争条件,以及数据库如何实现**读已提交(read committed)**,**快照隔离(snapshot isolation)** 和**可串行化(serializability)** 等隔离级别。
+本章将研究许多出错案例,并探索数据库用于防范这些问题的算法。尤其会深入 **并发控制** 的领域,讨论各种可能发生的竞争条件,以及数据库如何实现 **读已提交(read committed)**,**快照隔离(snapshot isolation)** 和 **可串行化(serializability)** 等隔离级别。
 
-本章同时适用于单机数据库与分布式数据库;在[第八章](ch8.md)中将重点讨论仅出现在分布式系统中的特殊挑战。
+本章同时适用于单机数据库与分布式数据库;在 [第八章](ch8.md) 中将重点讨论仅出现在分布式系统中的特殊挑战。
 
 
 ## 事务的棘手概念
 
-现今,几乎所有的关系型数据库和一些非关系数据库都支持**事务**。其中大多数遵循IBM System R(第一个SQL数据库)在1975年引入的风格【1,2,3】。40年里,尽管一些实现细节发生了变化,但总体思路大同小异:MySQL,PostgreSQL,Oracle,SQL Server等数据库中的事务支持与System R异乎寻常地相似。
+现今,几乎所有的关系型数据库和一些非关系数据库都支持 **事务**。其中大多数遵循 IBM System R(第一个 SQL 数据库)在 1975 年引入的风格【1,2,3】。40 年里,尽管一些实现细节发生了变化,但总体思路大同小异:MySQL、PostgreSQL、Oracle 和 SQL Server 等数据库中的事务支持与 System R 异乎寻常地相似。
 
-2000年以后,非关系(NoSQL)数据库开始普及。它们的目标是在关系数据库的现状基础上,通过提供新的数据模型选择(请参阅[第二章](ch2.md))并默认包含复制(第五章)和分区(第六章)来进一步提升。事务是这次运动的主要牺牲品:这些新一代数据库中的许多数据库完全放弃了事务,或者重新定义了这个词,描述比以前所理解的更弱得多的一套保证【4】。
+2000 年以后,非关系(NoSQL)数据库开始普及。它们的目标是在关系数据库的现状基础上,通过提供新的数据模型选择(请参阅 [第二章](ch2.md))并默认包含复制(第五章)和分区(第六章)来进一步提升。事务是这次运动的主要牺牲品:这些新一代数据库中的许多数据库完全放弃了事务,或者重新定义了这个词,描述比以前所理解的更弱得多的一套保证【4】。
 
-随着这种新型分布式数据库的炒作,人们普遍认为事务是可伸缩性的对立面,任何大型系统都必须放弃事务以保持良好的性能和高可用性【5,6】。另一方面,数据库厂商有时将事务保证作为“重要应用”和“有价值数据”的基本要求。这两种观点都是**纯粹的夸张**。
+随着这种新型分布式数据库的炒作,人们普遍认为事务是可伸缩性的对立面,任何大型系统都必须放弃事务以保持良好的性能和高可用性【5,6】。另一方面,数据库厂商有时将事务保证作为 “重要应用” 和 “有价值数据” 的基本要求。这两种观点都是 **纯粹的夸张**。
 
-事实并非如此简单:与其他技术设计选择一样,事务有其优势和局限性。为了理解这些权衡,让我们了解事务所提供的保证的细节——无论是在正常运行中还是在各种极端(但是现实存在)的情况下。
+事实并非如此简单:与其他技术设计选择一样,事务有其优势和局限性。为了理解这些权衡,让我们了解事务所提供的保证的细节 —— 无论是在正常运行中还是在各种极端(但是现实存在)的情况下。
 
 ### ACID的含义
 
-事务所提供的安全保证,通常由众所周知的首字母缩略词ACID来描述,ACID代表**原子性(Atomicity)**,**一致性(Consistency)**,**隔离性(Isolation)** 和**持久性(Durability)**。它由Theo Härder和Andreas Reuter于1983年提出,旨在为数据库中的容错机制建立精确的术语。
+事务所提供的安全保证,通常由众所周知的首字母缩略词 ACID 来描述,ACID 代表 **原子性(Atomicity)**,**一致性(Consistency)**,**隔离性(Isolation)** 和 **持久性(Durability)**。它由 Theo Härder 和 Andreas Reuter 于 1983 年提出,旨在为数据库中的容错机制建立精确的术语。
 
-但实际上,不同数据库的ACID实现并不相同。例如,我们将会看到,关于**隔离性**的含义就有许多含糊不清【8】。高层次上的想法很美好,但魔鬼隐藏在细节里。今天,当一个系统声称自己“符合ACID”时,实际上能期待的是什么保证并不清楚。不幸的是,ACID现在几乎已经变成了一个营销术语。
+但实际上,不同数据库的 ACID 实现并不相同。例如,我们将会看到,关于 **隔离性** 的含义就有许多含糊不清【8】。高层次上的想法很美好,但魔鬼隐藏在细节里。今天,当一个系统声称自己 “符合 ACID” 时,实际上能期待的是什么保证并不清楚。不幸的是,ACID 现在几乎已经变成了一个营销术语。
 
-(不符合ACID标准的系统有时被称为BASE,它代表**基本可用性(Basically Available)**,**软状态(Soft State)** 和**最终一致性(Eventual consistency)**【9】,这比ACID的定义更加模糊,似乎BASE的唯一合理的定义是“不是ACID”,即它几乎可以代表任何你想要的东西。)
+(不符合 ACID 标准的系统有时被称为 BASE,它代表 **基本可用性(Basically Available)**,**软状态(Soft State)** 和 **最终一致性(Eventual consistency)**【9】,这比 ACID 的定义更加模糊,似乎 BASE 的唯一合理的定义是 “不是 ACID”,即它几乎可以代表任何你想要的东西。)
 
 让我们深入了解原子性,一致性,隔离性和持久性的定义,这可以让我们提炼出事务的思想。
 
@@ -58,85 +58,85 @@
 
 一般来说,原子是指不能分解成小部分的东西。这个词在计算机的不同领域中意味着相似但又微妙不同的东西。例如,在多线程编程中,如果一个线程执行一个原子操作,这意味着另一个线程无法看到该操作的一半结果。系统只能处于操作之前或操作之后的状态,而不是介于两者之间的状态。
 
-相比之下,ACID的原子性并**不**是关于 **并发(concurrent)** 的。它并不是在描述如果几个进程试图同时访问相同的数据会发生什么情况,这种情况包含在缩写 ***I*** 中,即[**隔离性**](#隔离性)。
+相比之下,ACID 的原子性并 **不** 是关于 **并发(concurrent)** 的。它并不是在描述如果几个进程试图同时访问相同的数据会发生什么情况,这种情况包含在缩写 ***I*** 中,即 [**隔离性**](#隔离性)。
 
-ACID的原子性描述了当客户想进行多次写入,但在一些写操作处理完之后出现故障的情况。例如进程崩溃,网络连接中断,磁盘变满或者某种完整性约束被违反。如果这些写操作被分组到一个原子事务中,并且该事务由于错误而不能完成(提交),则该事务将被中止,并且数据库必须丢弃或撤消该事务中迄今为止所做的任何写入。
+ACID 的原子性描述了当客户想进行多次写入,但在一些写操作处理完之后出现故障的情况。例如进程崩溃,网络连接中断,磁盘变满或者某种完整性约束被违反。如果这些写操作被分组到一个原子事务中,并且该事务由于错误而不能完成(提交),则该事务将被中止,并且数据库必须丢弃或撤消该事务中迄今为止所做的任何写入。
 
-如果没有原子性,在多处更改进行到一半时发生错误,很难知道哪些更改已经生效,哪些没有生效。该应用程序可以再试一次,但冒着进行两次相同变更的风险,可能会导致数据重复或错误的数据。原子性简化了这个问题:如果事务被**中止(abort)**,应用程序可以确定它没有改变任何东西,所以可以安全地重试。
+如果没有原子性,在多处更改进行到一半时发生错误,很难知道哪些更改已经生效,哪些没有生效。该应用程序可以再试一次,但冒着进行两次相同变更的风险,可能会导致数据重复或错误的数据。原子性简化了这个问题:如果事务被 **中止(abort)**,应用程序可以确定它没有改变任何东西,所以可以安全地重试。
 
-ACID原子性的定义特征是:**能够在错误时中止事务,丢弃该事务进行的所有写入变更的能力。** 或许 **可中止性(abortability)** 是更好的术语,但本书将继续使用原子性,因为这是惯用词。
+ACID 原子性的定义特征是:**能够在错误时中止事务,丢弃该事务进行的所有写入变更的能力。** 或许 **可中止性(abortability)** 是更好的术语,但本书将继续使用原子性,因为这是惯用词。
 
 #### 一致性
 
 一致性这个词被赋予太多含义:
 
-* 在[第五章](ch5.md)中,我们讨论了副本一致性,以及异步复制系统中的最终一致性问题(请参阅“[复制延迟问题](ch5.md#复制延迟问题)”)。
-* [一致性哈希(Consistency Hashing)](ch6.md#一致性哈希))是某些系统用于重新分区的一种分区方法。
-* 在[CAP定理](ch9.md#CAP定理)中,一致性一词用于表示[线性一致性](ch9.md#线性一致性)。
-* 在ACID的上下文中,**一致性**是指数据库在应用程序的特定概念中处于“良好状态”。
+* 在 [第五章](ch5.md) 中,我们讨论了副本一致性,以及异步复制系统中的最终一致性问题(请参阅 “[复制延迟问题](ch5.md#复制延迟问题)”)。
+* [一致性哈希](ch6.md#一致性哈希) 是某些系统用于重新分区的一种分区方法。
+* 在 [CAP 定理](ch9.md#CAP 定理) 中,一致性一词用于表示 [线性一致性](ch9.md#线性一致性)。
+* 在 ACID 的上下文中,**一致性** 是指数据库在应用程序的特定概念中处于 “良好状态”。
 
 很不幸,这一个词就至少有四种不同的含义。
 
-ACID一致性的概念是,**对数据的一组特定约束必须始终成立**。即**不变式(invariants)**。例如,在会计系统中,所有账户整体上必须借贷相抵。如果一个事务开始于一个满足这些不变式的有效数据库,且在事务处理期间的任何写入操作都保持这种有效性,那么可以确定,不变式总是满足的。
+ACID 一致性的概念是,**对数据的一组特定约束必须始终成立**。即 **不变式(invariants)**。例如,在会计系统中,所有账户整体上必须借贷相抵。如果一个事务开始于一个满足这些不变式的有效数据库,且在事务处理期间的任何写入操作都保持这种有效性,那么可以确定,不变式总是满足的。
 
 但是,一致性的这种概念取决于应用程序对不变式的理解,应用程序负责正确定义它的事务,并保持一致性。这并不是数据库可以保证的事情:如果你写入违反不变式的脏数据,数据库也无法阻止你(一些特定类型的不变式可以由数据库检查,例如外键约束或唯一约束,但是一般来说,是应用程序来定义什么样的数据是有效的,什么样是无效的。—— 数据库只管存储)。
 
-原子性,隔离性和持久性是数据库的属性,而一致性(在ACID意义上)是应用程序的属性。应用可能依赖数据库的原子性和隔离属性来实现一致性,但这并不仅取决于数据库。因此,字母C不属于ACID[^i]。
+原子性,隔离性和持久性是数据库的属性,而一致性(在 ACID 意义上)是应用程序的属性。应用可能依赖数据库的原子性和隔离属性来实现一致性,但这并不仅取决于数据库。因此,字母 C 不属于 ACID [^i]。
 
-[^i]: 乔·海勒斯坦(Joe Hellerstein)指出,在Härder与Reuter的论文中,“ACID中的C”是被“扔进去凑缩写单词的”【7】,而且那时候大家都不怎么在乎一致性。
+[^i]: 乔・海勒斯坦(Joe Hellerstein)指出,在 Härder 与 Reuter 的论文中,“ACID 中的 C” 是被 “扔进去凑缩写单词的”【7】,而且那时候大家都不怎么在乎一致性。
 
 #### 隔离性
 
-大多数数据库都会同时被多个客户端访问。如果它们各自读写数据库的不同部分,这是没有问题的,但是如果它们访问相同的数据库记录,则可能会遇到**并发**问题(**竞争条件**,即race conditions)。
+大多数数据库都会同时被多个客户端访问。如果它们各自读写数据库的不同部分,这是没有问题的,但是如果它们访问相同的数据库记录,则可能会遇到 **并发** 问题(**竞争条件**,即 race conditions)。
 
-[图7-1](img/fig7-1.png)是这类问题的一个简单例子。假设你有两个客户端同时在数据库中增长一个计数器。(假设数据库没有内建的自增操作)每个客户端需要读取计数器的当前值,加 1 ,再回写新值。[图7-1](img/fig7-1.png) 中,因为发生了两次增长,计数器应该从42增至44;但由于竞态条件,实际上只增至 43 。
+[图 7-1](img/fig7-1.png) 是这类问题的一个简单例子。假设你有两个客户端同时在数据库中增长一个计数器。(假设数据库没有内建的自增操作)每个客户端需要读取计数器的当前值,加 1 ,再回写新值。[图 7-1](img/fig7-1.png) 中,因为发生了两次增长,计数器应该从 42 增至 44;但由于竞态条件,实际上只增至 43 。
 
-ACID意义上的隔离性意味着,**同时执行的事务是相互隔离的**:它们不能相互冒犯。传统的数据库教科书将隔离性形式化为**可串行化(Serializability)**,这意味着每个事务可以假装它是唯一在整个数据库上运行的事务。数据库确保当多个事务被提交时,结果与它们串行运行(一个接一个)是一样的,尽管实际上它们可能是并发运行的【10】。
+ACID 意义上的隔离性意味着,**同时执行的事务是相互隔离的**:它们不能相互冒犯。传统的数据库教科书将隔离性形式化为 **可串行化(Serializability)**,这意味着每个事务可以假装它是唯一在整个数据库上运行的事务。数据库确保当多个事务被提交时,结果与它们串行运行(一个接一个)是一样的,尽管实际上它们可能是并发运行的【10】。
 
 ![](img/fig7-1.png)
 
-**图7-1 两个客户之间的竞争状态同时递增计数器**
+**图 7-1 两个客户之间的竞争状态同时递增计数器**
 
-然而实践中很少会使用可串行的隔离,因为它有性能损失。一些流行的数据库如Oracle 11g,甚至没有实现它。在Oracle中有一个名为“可串行的”隔离级别,但实际上它实现了一种叫做**快照隔离(snapshot isolation)** 的功能,**这是一种比可串行化更弱的保证**【8,11】。我们将在“[弱隔离级别](#弱隔离级别)”中研究快照隔离和其他形式的隔离。
+然而实践中很少会使用可串行的隔离,因为它有性能损失。一些流行的数据库如 Oracle 11g,甚至没有实现它。在 Oracle 中有一个名为 “可串行的” 隔离级别,但实际上它实现了一种叫做 **快照隔离(snapshot isolation)** 的功能,**这是一种比可串行化更弱的保证**【8,11】。我们将在 “[弱隔离级别](#弱隔离级别)” 中研究快照隔离和其他形式的隔离。
 
 #### 持久性
 
 数据库系统的目的是,提供一个安全的地方存储数据,而不用担心丢失。**持久性** 是一个承诺,即一旦事务成功完成,即使发生硬件故障或数据库崩溃,写入的任何数据也不会丢失。
 
-在单节点数据库中,持久性通常意味着数据已被写入非易失性存储设备,如硬盘或SSD。它通常还包括预写日志或类似的文件(请参阅“[让B树更可靠](ch3.md#让B树更可靠)”),以便在磁盘上的数据结构损坏时进行恢复。在带复制的数据库中,持久性可能意味着数据已成功复制到一些节点。为了提供持久性保证,数据库必须等到这些写入或复制完成后,才能报告事务成功提交。
+在单节点数据库中,持久性通常意味着数据已被写入非易失性存储设备,如硬盘或 SSD。它通常还包括预写日志或类似的文件(请参阅 “[让 B 树更可靠](ch3.md#让B树更可靠)”),以便在磁盘上的数据结构损坏时进行恢复。在带复制的数据库中,持久性可能意味着数据已成功复制到一些节点。为了提供持久性保证,数据库必须等到这些写入或复制完成后,才能报告事务成功提交。
 
-如“[可靠性](ch1.md#可靠性)”一节所述,**完美的持久性是不存在的** :如果所有硬盘和所有备份同时被销毁,那显然没有任何数据库能救得了你。
+如 “[可靠性](ch1.md#可靠性)” 一节所述,**完美的持久性是不存在的** :如果所有硬盘和所有备份同时被销毁,那显然没有任何数据库能救得了你。
 
 > #### 复制与持久性
 >
-> 在历史上,持久性意味着写入归档磁带。后来它被理解为写入磁盘或SSD。再后来它又有了新的内涵即“复制(replication)”。哪种实现更好一些?
+> 在历史上,持久性意味着写入归档磁带。后来它被理解为写入磁盘或 SSD。再后来它又有了新的内涵即 “复制(replication)”。哪种实现更好一些?
 >
 > 真相是,没有什么是完美的:
 >
 > * 如果你写入磁盘然后机器宕机,即使数据没有丢失,在修复机器或将磁盘转移到其他机器之前,也是无法访问的。这种情况下,复制系统可以保持可用性。
-> * 一个相关性故障(停电,或一个特定输入导致所有节点崩溃的Bug)可能会一次性摧毁所有副本(请参阅「[可靠性](ch1.md#可靠性)」),任何仅存储在内存中的数据都会丢失,故内存数据库仍然要和磁盘写入打交道。
+> * 一个相关性故障(停电,或一个特定输入导致所有节点崩溃的 Bug)可能会一次性摧毁所有副本(请参阅「[可靠性](ch1.md#可靠性)」),任何仅存储在内存中的数据都会丢失,故内存数据库仍然要和磁盘写入打交道。
 > * 在异步复制系统中,当主库不可用时,最近的写入操作可能会丢失(请参阅「[处理节点宕机](ch5.md#处理节点宕机)」)。
-> * 当电源突然断电时,特别是固态硬盘,有证据显示有时会违反应有的保证:甚至fsync也不能保证正常工作【12】。硬盘固件可能有错误,就像任何其他类型的软件一样【13,14】。
+> * 当电源突然断电时,特别是固态硬盘,有证据显示有时会违反应有的保证:甚至 fsync 也不能保证正常工作【12】。硬盘固件可能有错误,就像任何其他类型的软件一样【13,14】。
 > * 存储引擎和文件系统之间的微妙交互可能会导致难以追踪的错误,并可能导致磁盘上的文件在崩溃后被损坏【15,16】。
 > * 磁盘上的数据可能会在没有检测到的情况下逐渐损坏【17】。如果数据已损坏一段时间,副本和最近的备份也可能损坏。这种情况下,需要尝试从历史备份中恢复数据。
-> * 一项关于固态硬盘的研究发现,在运行的前四年中,30%到80%的硬盘会产生至少一个坏块【18】。相比固态硬盘,磁盘的坏道率较低,但完全失效的概率更高。
-> * 如果SSD断电,可能会在几周内开始丢失数据,具体取决于温度【19】。
+> * 一项关于固态硬盘的研究发现,在运行的前四年中,30% 到 80% 的硬盘会产生至少一个坏块【18】。相比固态硬盘,磁盘的坏道率较低,但完全失效的概率更高。
+> * 如果 SSD 断电,可能会在几周内开始丢失数据,具体取决于温度【19】。
 >
-> 在实践中,没有一种技术可以提供绝对保证。只有各种降低风险的技术,包括写入磁盘,复制到远程机器和备份——它们可以且应该一起使用。与往常一样,最好抱着怀疑的态度接受任何理论上的“保证”。
+> 在实践中,没有一种技术可以提供绝对保证。只有各种降低风险的技术,包括写入磁盘,复制到远程机器和备份 —— 它们可以且应该一起使用。与往常一样,最好抱着怀疑的态度接受任何理论上的 “保证”。
 
 ### 单对象和多对象操作
 
-回顾一下,在ACID中,原子性和隔离性描述了客户端在同一事务中执行多次写入时,数据库应该做的事情:
+回顾一下,在 ACID 中,原子性和隔离性描述了客户端在同一事务中执行多次写入时,数据库应该做的事情:
 
 * 原子性
 
-  如果在一系列写操作的中途发生错误,则应中止事务处理,并丢弃当前事务的所有写入。换句话说,数据库免去了用户对部分失败的担忧——通过提供“**宁为玉碎,不为瓦全(all-or-nothing)**”的保证。
+  如果在一系列写操作的中途发生错误,则应中止事务处理,并丢弃当前事务的所有写入。换句话说,数据库免去了用户对部分失败的担忧 —— 通过提供 “**宁为玉碎,不为瓦全(all-or-nothing)**” 的保证。
 
 * 隔离性
 
   同时运行的事务不应该互相干扰。例如,如果一个事务进行多次写入,则另一个事务要么看到全部写入结果,要么什么都看不到,但不应该是一些子集。
 
-这些定义假设你想同时修改多个对象(行,文档,记录)。通常需要**多对象事务(multi-object transaction)** 来保持多块数据同步。[图7-2](img/fig7-2.png)展示了一个来自电邮应用的例子。执行以下查询来显示用户未读邮件数量:
+这些定义假设你想同时修改多个对象(行,文档,记录)。通常需要 **多对象事务(multi-object transaction)** 来保持多块数据同步。[图 7-2](img/fig7-2.png) 展示了一个来自电邮应用的例子。执行以下查询来显示用户未读邮件数量:
 
 ```sql
 SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
@@ -144,41 +144,41 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
 
 但如果邮件太多,你可能会觉得这个查询太慢,并决定用单独的字段存储未读邮件的数量(一种反规范化)。现在每当一个新消息写入时,必须也增长未读计数器,每当一个消息被标记为已读时,也必须减少未读计数器。
 
-在[图7-2](img/fig7-2.png)中,用户2 遇到异常情况:邮件列表里显示有未读消息,但计数器显示为零未读消息,因为计数器增长还没有发生[^ii]。隔离性可以避免这个问题:通过确保用户2 要么同时看到新邮件和增长后的计数器,要么都看不到,而不是一个前后矛盾的中间结果。
+在 [图 7-2](img/fig7-2.png) 中,用户 2 遇到异常情况:邮件列表里显示有未读消息,但计数器显示为零未读消息,因为计数器增长还没有发生 [^ii]。隔离性可以避免这个问题:通过确保用户 2 要么同时看到新邮件和增长后的计数器,要么都看不到,而不是一个前后矛盾的中间结果。
 
 [^ii]: 可以说邮件应用中的错误计数器并不是什么特别重要的问题。但换种方式来看,你可以把未读计数器换成客户账户余额,把邮件收发看成支付交易。
 
 ![](img/fig7-2.png)
 
-**图7-2 违反隔离性:一个事务读取另一个事务的未被执行的写入(“脏读”)。**
+**图 7-2 违反隔离性:一个事务读取另一个事务的未被执行的写入(“脏读”)。**
 
-[图7-3](img/fig7-3.png)说明了对原子性的需求:如果在事务过程中发生错误,邮箱和未读计数器的内容可能会失去同步。在原子事务中,如果对计数器的更新失败,事务将被中止,并且插入的电子邮件将被回滚。
+[图 7-3](img/fig7-3.png) 说明了对原子性的需求:如果在事务过程中发生错误,邮箱和未读计数器的内容可能会失去同步。在原子事务中,如果对计数器的更新失败,事务将被中止,并且插入的电子邮件将被回滚。
 
 ![](img/fig7-3.png)
 
-**图7-3 原子性确保发生错误时,事务先前的任何写入都会被撤消,以避免状态不一致**
+**图 7-3 原子性确保发生错误时,事务先前的任何写入都会被撤消,以避免状态不一致**
 
-多对象事务需要某种方式来确定哪些读写操作属于同一个事务。在关系型数据库中,通常基于客户端与数据库服务器的TCP连接:在任何特定连接上,`BEGIN TRANSACTION` 和 `COMMIT` 语句之间的所有内容,被认为是同一事务的一部分.[^iii]
+多对象事务需要某种方式来确定哪些读写操作属于同一个事务。在关系型数据库中,通常基于客户端与数据库服务器的 TCP 连接:在任何特定连接上,`BEGIN TRANSACTION` 和 `COMMIT` 语句之间的所有内容,被认为是同一事务的一部分.[^iii]
 
-[^iii]: 这并不完美。如果TCP连接中断,则事务必须中止。如果中断发生在客户端请求提交之后,但在服务器确认提交发生之前,客户端并不知道事务是否已提交。为了解决这个问题,事务管理器可以通过一个唯一事务标识符来对操作进行分组,这个标识符并未绑定到特定TCP连接。后续再“[数据库的端到端原则](ch12.md#数据库的端到端原则)”一节将回到这个主题。
+[^iii]: 这并不完美。如果 TCP 连接中断,则事务必须中止。如果中断发生在客户端请求提交之后,但在服务器确认提交发生之前,客户端并不知道事务是否已提交。为了解决这个问题,事务管理器可以通过一个唯一事务标识符来对操作进行分组,这个标识符并未绑定到特定 TCP 连接。后续再 “[数据库的端到端原则](ch12.md#数据库的端到端原则)” 一节将回到这个主题。
 
-另一方面,许多非关系数据库并没有将这些操作组合在一起的方法。即使存在多对象API(例如,某键值存储可能具有在一个操作中更新几个键的multi-put操作),但这并不一定意味着它具有事务语义:该命令可能在一些键上成功,在其他的键上失败,使数据库处于部分更新的状态。
+另一方面,许多非关系数据库并没有将这些操作组合在一起的方法。即使存在多对象 API(例如,某键值存储可能具有在一个操作中更新几个键的 multi-put 操作),但这并不一定意味着它具有事务语义:该命令可能在一些键上成功,在其他的键上失败,使数据库处于部分更新的状态。
 
 #### 单对象写入
 
-当单个对象发生改变时,原子性和隔离性也是适用的。例如,假设你正在向数据库写入一个 20 KB的 JSON文档:
+当单个对象发生改变时,原子性和隔离性也是适用的。例如,假设你正在向数据库写入一个 20 KB 的 JSON 文档:
 
-- 如果在发送第一个10 KB之后网络连接中断,数据库是否存储了不可解析的10KB JSON片段?
+- 如果在发送第一个 10 KB 之后网络连接中断,数据库是否存储了不可解析的 10KB JSON 片段?
 - 如果在数据库正在覆盖磁盘上的前一个值的过程中电源发生故障,是否最终将新旧值拼接在一起?
 - 如果另一个客户端在写入过程中读取该文档,是否会看到部分更新的值?
 
-这些问题非常让人头大,故存储引擎一个几乎普遍的目标是:对单节点上的单个对象(例如键值对)上提供原子性和隔离性。原子性可以通过使用日志来实现崩溃恢复(请参阅“[让B树更可靠](ch3.md#让B树更可靠)”),并且可以使用每个对象上的锁来实现隔离(每次只允许一个线程访问对象) 。
+这些问题非常让人头大,故存储引擎一个几乎普遍的目标是:对单节点上的单个对象(例如键值对)上提供原子性和隔离性。原子性可以通过使用日志来实现崩溃恢复(请参阅 “[让 B 树更可靠](ch3.md#让B树更可靠)”),并且可以使用每个对象上的锁来实现隔离(每次只允许一个线程访问对象) 。
 
-一些数据库也提供更复杂的原子操作[^iv],例如自增操作,这样就不再需要像 [图7-1](img/fig7-1.png) 那样的读取-修改-写入序列了。同样流行的是 **[比较和设置(CAS, compare-and-set)](#比较并设置(CAS))** 操作,仅当值没有被其他并发修改过时,才允许执行写操作。
+一些数据库也提供更复杂的原子操作 [^iv],例如自增操作,这样就不再需要像 [图 7-1](img/fig7-1.png) 那样的读取 - 修改 - 写入序列了。同样流行的是 **[比较和设置(CAS, compare-and-set)](#比较并设置(CAS))** 操作,仅当值没有被其他并发修改过时,才允许执行写操作。
 
-[^iv]: 严格地说,**原子自增(atomic increment)** 这个术语在多线程编程的意义上使用了原子这个词。 在ACID的情况下,它实际上应该被称为 **隔离的(isolated)** 的或**可串行的(serializable)** 的增量。 但这就太吹毛求疵了。
+[^iv]: 严格地说,**原子自增(atomic increment)** 这个术语在多线程编程的意义上使用了原子这个词。 在 ACID 的情况下,它实际上应该被称为 **隔离的(isolated)** 的或 **可串行的(serializable)** 的增量。 但这就太吹毛求疵了。
 
-这些单对象操作很有用,因为它们可以防止在多个客户端尝试同时写入同一个对象时丢失更新(请参阅“[防止丢失更新](#防止丢失更新)”)。但它们不是通常意义上的事务。CAS以及其他单一对象操作被称为“轻量级事务”,甚至出于营销目的被称为“ACID”【20,21,22】,但是这个术语是误导性的。事务通常被理解为,**将多个对象上的多个操作合并为一个执行单元的机制**。
+这些单对象操作很有用,因为它们可以防止在多个客户端尝试同时写入同一个对象时丢失更新(请参阅 “[防止丢失更新](#防止丢失更新)”)。但它们不是通常意义上的事务。CAS 以及其他单一对象操作被称为 “轻量级事务”,甚至出于营销目的被称为 “ACID”【20,21,22】,但是这个术语是误导性的。事务通常被理解为,**将多个对象上的多个操作合并为一个执行单元的机制**。
 
 #### 多对象事务的需求
 
@@ -189,117 +189,117 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
 有一些场景中,单对象插入,更新和删除是足够的。但是许多其他场景需要协调写入几个不同的对象:
 
 * 在关系数据模型中,一个表中的行通常具有对另一个表中的行的外键引用。(类似的是,在一个图数据模型中,一个顶点有着到其他顶点的边)。多对象事务使你确保这些引用始终有效:当插入几个相互引用的记录时,外键必须是正确的和最新的,不然数据就没有意义。
-* 在文档数据模型中,需要一起更新的字段通常在同一个文档中,这被视为单个对象——更新单个文档时不需要多对象事务。但是,缺乏连接功能的文档数据库会鼓励非规范化(请参阅“[关系型数据库与文档数据库在今日的对比](ch2.md#关系型数据库与文档数据库在今日的对比)”)。当需要更新非规范化的信息时,如 [图7-2](img/fig7-2.png) 所示,需要一次更新多个文档。事务在这种情况下非常有用,可以防止非规范化的数据不同步。
+* 在文档数据模型中,需要一起更新的字段通常在同一个文档中,这被视为单个对象 —— 更新单个文档时不需要多对象事务。但是,缺乏连接功能的文档数据库会鼓励非规范化(请参阅 “[关系型数据库与文档数据库在今日的对比](ch2.md#关系型数据库与文档数据库在今日的对比)”)。当需要更新非规范化的信息时,如 [图 7-2](img/fig7-2.png) 所示,需要一次更新多个文档。事务在这种情况下非常有用,可以防止非规范化的数据不同步。
 * 在具有次级索引的数据库中(除了纯粹的键值存储以外几乎都有),每次更改值时都需要更新索引。从事务角度来看,这些索引是不同的数据库对象:例如,如果没有事务隔离性,记录可能出现在一个索引中,但没有出现在另一个索引中,因为第二个索引的更新还没有发生。
 
-这些应用仍然可以在没有事务的情况下实现。然而,**没有原子性,错误处理就要复杂得多,缺乏隔离性,就会导致并发问题**。我们将在“[弱隔离级别](#弱隔离级别)”中讨论这些问题,并在[第十二章](ch12.md)中探讨其他方法。
+这些应用仍然可以在没有事务的情况下实现。然而,**没有原子性,错误处理就要复杂得多,缺乏隔离性,就会导致并发问题**。我们将在 “[弱隔离级别](#弱隔离级别)” 中讨论这些问题,并在 [第十二章](ch12.md) 中探讨其他方法。
 
 #### 处理错误和中止
 
-事务的一个关键特性是,如果发生错误,它可以中止并安全地重试。 ACID数据库基于这样的哲学:如果数据库有违反其原子性,隔离性或持久性的危险,则宁愿完全放弃事务,而不是留下半成品。
+事务的一个关键特性是,如果发生错误,它可以中止并安全地重试。 ACID 数据库基于这样的哲学:如果数据库有违反其原子性,隔离性或持久性的危险,则宁愿完全放弃事务,而不是留下半成品。
 
-然而并不是所有的系统都遵循这个哲学。特别是具有[无主复制](ch5.md#无主复制)的数据存储,主要是在“尽力而为”的基础上进行工作。可以概括为“数据库将做尽可能多的事,运行遇到错误时,它不会撤消它已经完成的事情“ ——所以,从错误中恢复是应用程序的责任。
+然而并不是所有的系统都遵循这个哲学。特别是具有 [无主复制](ch5.md#无主复制) 的数据存储,主要是在 “尽力而为” 的基础上进行工作。可以概括为 “数据库将做尽可能多的事,运行遇到错误时,它不会撤消它已经完成的事情 “ —— 所以,从错误中恢复是应用程序的责任。
 
-错误发生不可避免,但许多软件开发人员倾向于只考虑乐观情况,而不是错误处理的复杂性。例如,像Rails的ActiveRecord和Django这样的**对象关系映射(ORM, object-relation Mapping)** 框架不会重试中断的事务—— 这个错误通常会导致一个从堆栈向上传播的异常,所以任何用户输入都会被丢弃,用户拿到一个错误信息。这实在是太耻辱了,因为中止的重点就是允许安全的重试。
+错误发生不可避免,但许多软件开发人员倾向于只考虑乐观情况,而不是错误处理的复杂性。例如,像 Rails 的 ActiveRecord 和 Django 这样的 **对象关系映射(ORM, object-relation Mapping)** 框架不会重试中断的事务 —— 这个错误通常会导致一个从堆栈向上传播的异常,所以任何用户输入都会被丢弃,用户拿到一个错误信息。这实在是太耻辱了,因为中止的重点就是允许安全的重试。
 
 尽管重试一个中止的事务是一个简单而有效的错误处理机制,但它并不完美:
 
-- 如果事务实际上成功了,但是在服务器试图向客户端确认提交成功时网络发生故障(所以客户端认为提交失败了),那么重试事务会导致事务被执行两次——除非你有一个额外的应用级去重机制。
+- 如果事务实际上成功了,但是在服务器试图向客户端确认提交成功时网络发生故障(所以客户端认为提交失败了),那么重试事务会导致事务被执行两次 —— 除非你有一个额外的应用级去重机制。
 - 如果错误是由于负载过大造成的,则重试事务将使问题变得更糟,而不是更好。为了避免这种正反馈循环,可以限制重试次数,使用指数退避算法,并单独处理与过载相关的错误(如果允许)。
 - 仅在临时性错误(例如,由于死锁,异常情况,临时性网络中断和故障切换)后才值得重试。在发生永久性错误(例如,违反约束)之后重试是毫无意义的。
-- 如果事务在数据库之外也有副作用,即使事务被中止,也可能发生这些副作用。例如,如果你正在发送电子邮件,那你肯定不希望每次重试事务时都重新发送电子邮件。如果你想确保几个不同的系统一起提交或放弃,**两阶段提交(2PC, two-phase commit)** 可以提供帮助(“[原子提交与两阶段提交](ch9.md#原子提交与两阶段提交)”中将讨论这个问题)。
+- 如果事务在数据库之外也有副作用,即使事务被中止,也可能发生这些副作用。例如,如果你正在发送电子邮件,那你肯定不希望每次重试事务时都重新发送电子邮件。如果你想确保几个不同的系统一起提交或放弃,**两阶段提交(2PC, two-phase commit)** 可以提供帮助(“[原子提交与两阶段提交](ch9.md#原子提交与两阶段提交)” 中将讨论这个问题)。
 - 如果客户端进程在重试中失效,任何试图写入数据库的数据都将丢失。
 
 ## 弱隔离级别
 
-如果两个事务不触及相同的数据,它们可以安全地**并行(parallel)** 运行,因为两者都不依赖于另一个。当一个事务读取由另一个事务同时修改的数据时,或者当两个事务试图同时修改相同的数据时,并发问题(竞争条件)才会出现。
+如果两个事务不触及相同的数据,它们可以安全地 **并行(parallel)** 运行,因为两者都不依赖于另一个。当一个事务读取由另一个事务同时修改的数据时,或者当两个事务试图同时修改相同的数据时,并发问题(竞争条件)才会出现。
 
-并发BUG很难通过测试找到,因为这样的错误只有在特殊时序下才会触发。这样的时序问题可能非常少发生,通常很难重现[^译注i]。并发性也很难推理,特别是在大型应用中,你不一定知道哪些其他代码正在访问数据库。在一次只有一个用户时,应用开发已经很麻烦了,有许多并发用户使得它更加困难,因为任何一个数据都可能随时改变。
+并发 BUG 很难通过测试找到,因为这样的错误只有在特殊时序下才会触发。这样的时序问题可能非常少发生,通常很难重现 [^译注i]。并发性也很难推理,特别是在大型应用中,你不一定知道哪些其他代码正在访问数据库。在一次只有一个用户时,应用开发已经很麻烦了,有许多并发用户使得它更加困难,因为任何一个数据都可能随时改变。
 
-[^译注i]: 轶事:偶然出现的瞬时错误有时称为***Heisenbug***,而确定性的问题对应地称为***Bohrbugs***
+[^译注i]: 轶事:偶然出现的瞬时错误有时称为 ***Heisenbug***,而确定性的问题对应地称为 ***Bohrbugs***
 
-出于这个原因,数据库一直试图通过提供**事务隔离(transaction isolation)** 来隐藏应用程序开发者的并发问题。从理论上讲,隔离可以通过假装没有并发发生,让你的生活更加轻松:**可串行的(serializable)** 隔离等级意味着数据库保证事务的效果如同串行运行(即一次一个,没有任何并发)。
+出于这个原因,数据库一直试图通过提供 **事务隔离(transaction isolation)** 来隐藏应用程序开发者的并发问题。从理论上讲,隔离可以通过假装没有并发发生,让你的生活更加轻松:**可串行的(serializable)** 隔离等级意味着数据库保证事务的效果如同串行运行(即一次一个,没有任何并发)。
 
-实际上不幸的是:隔离并没有那么简单。**可串行的隔离**会有性能损失,许多数据库不愿意支付这个代价【8】。因此,系统通常使用较弱的隔离级别来防止一部分,而不是全部的并发问题。这些隔离级别难以理解,并且会导致微妙的错误,但是它们仍然在实践中被使用【23】。
+实际上不幸的是:隔离并没有那么简单。**可串行的隔离** 会有性能损失,许多数据库不愿意支付这个代价【8】。因此,系统通常使用较弱的隔离级别来防止一部分,而不是全部的并发问题。这些隔离级别难以理解,并且会导致微妙的错误,但是它们仍然在实践中被使用【23】。
 
-弱事务隔离级别导致的并发性错误不仅仅是一个理论问题。它们造成了很多的资金损失【24,25】,耗费了财务审计人员的调查【26】,并导致客户数据被破坏【27】。关于这类问题的一个流行的评论是“如果你正在处理财务数据,请使用ACID数据库!” —— 但是这一点没有提到。即使是很多流行的关系型数据库系统(通常被认为是“ACID”)也使用弱隔离级别,所以它们也不一定能防止这些错误的发生。
+弱事务隔离级别导致的并发性错误不仅仅是一个理论问题。它们造成了很多的资金损失【24,25】,耗费了财务审计人员的调查【26】,并导致客户数据被破坏【27】。关于这类问题的一个流行的评论是 “如果你正在处理财务数据,请使用 ACID 数据库!” —— 但是这一点没有提到。即使是很多流行的关系型数据库系统(通常被认为是 “ACID”)也使用弱隔离级别,所以它们也不一定能防止这些错误的发生。
 
 比起盲目地依赖工具,我们应该对存在的并发问题的种类,以及如何防止这些问题有深入的理解。然后就可以使用我们所掌握的工具来构建可靠和正确的应用程序。
 
-在本节中,我们将看几个在实践中使用的弱(**非串行的**,即nonserializable)隔离级别,并详细讨论哪种竞争条件可能发生也可能不发生,以便你可以决定什么级别适合你的应用程序。一旦我们完成了这个工作,我们将详细讨论可串行化(请参阅“[可串行化](#可串行化)”)。我们讨论的隔离级别将是非正式的,通过示例来进行。如果你需要严格的定义和分析它们的属性,你可以在学术文献中找到它们【28,29,30】。
+在本节中,我们将看几个在实践中使用的弱(**非串行的**,即 nonserializable)隔离级别,并详细讨论哪种竞争条件可能发生也可能不发生,以便你可以决定什么级别适合你的应用程序。一旦我们完成了这个工作,我们将详细讨论可串行化(请参阅 “[可串行化](#可串行化)”)。我们讨论的隔离级别将是非正式的,通过示例来进行。如果你需要严格的定义和分析它们的属性,你可以在学术文献中找到它们【28,29,30】。
 
 ### 读已提交
 
-最基本的事务隔离级别是**读已提交(Read Committed)**[^v],它提供了两个保证:
+最基本的事务隔离级别是 **读已提交(Read Committed)**[^v],它提供了两个保证:
 
-1. 从数据库读时,只能看到已提交的数据(没有**脏读**,即dirty reads)。
-2. 写入数据库时,只会覆盖已经写入的数据(没有**脏写**,即dirty writes)。
+1. 从数据库读时,只能看到已提交的数据(没有 **脏读**,即 dirty reads)。
+2. 写入数据库时,只会覆盖已经写入的数据(没有 **脏写**,即 dirty writes)。
 
 我们来更详细地讨论这两个保证。
 
-[^v]: 某些数据库支持甚至更弱的隔离级别,称为**读未提交(Read uncommitted)**。它可以防止脏写,但不防止脏读。
+[^v]: 某些数据库支持甚至更弱的隔离级别,称为 **读未提交(Read uncommitted)**。它可以防止脏写,但不防止脏读。
 
 #### 没有脏读
 
-设想一个事务已经将一些数据写入数据库,但事务还没有提交或中止。另一个事务可以看到未提交的数据吗?如果是的话,那就叫做**脏读(dirty reads)**【2】。
+设想一个事务已经将一些数据写入数据库,但事务还没有提交或中止。另一个事务可以看到未提交的数据吗?如果是的话,那就叫做 **脏读(dirty reads)**【2】。
 
-在**读已提交**隔离级别运行的事务必须防止脏读。这意味着事务的任何写入操作只有在该事务提交时才能被其他人看到(然后所有的写入操作都会立即变得可见)。如[图7-4](img/fig7-4.png)所示,用户1 设置了`x = 3`,但用户2 的 `get x `仍旧返回旧值2 (当用户1 尚未提交时)。
+在 **读已提交** 隔离级别运行的事务必须防止脏读。这意味着事务的任何写入操作只有在该事务提交时才能被其他人看到(然后所有的写入操作都会立即变得可见)。如 [图 7-4](img/fig7-4.png) 所示,用户 1 设置了 `x = 3`,但用户 2 的 `get x` 仍旧返回旧值 2 (当用户 1 尚未提交时)。
 
 ![](img/fig7-4.png)
 
-**图7-4 没有脏读:用户2只有在用户1的事务已经提交后才能看到x的新值。**
+**图 7-4 没有脏读:用户 2 只有在用户 1 的事务已经提交后才能看到 x 的新值。**
 
 为什么要防止脏读,有几个原因:
 
-- 如果事务需要更新多个对象,脏读取意味着另一个事务可能会只看到一部分更新。例如,在[图7-2](img/fig7-2.png)中,用户看到新的未读电子邮件,但看不到更新的计数器。这就是电子邮件的脏读。看到处于部分更新状态的数据库会让用户感到困惑,并可能导致其他事务做出错误的决定。
-- 如果事务中止,则所有写入操作都需要回滚(如[图7-3](img/fig7-3.png)所示)。如果数据库允许脏读,那就意味着一个事务可能会看到稍后需要回滚的数据,即从未实际提交给数据库的数据。想想后果就让人头大。
+- 如果事务需要更新多个对象,脏读取意味着另一个事务可能会只看到一部分更新。例如,在 [图 7-2](img/fig7-2.png) 中,用户看到新的未读电子邮件,但看不到更新的计数器。这就是电子邮件的脏读。看到处于部分更新状态的数据库会让用户感到困惑,并可能导致其他事务做出错误的决定。
+- 如果事务中止,则所有写入操作都需要回滚(如 [图 7-3](img/fig7-3.png) 所示)。如果数据库允许脏读,那就意味着一个事务可能会看到稍后需要回滚的数据,即从未实际提交给数据库的数据。想想后果就让人头大。
 
 #### 没有脏写
 
 如果两个事务同时尝试更新数据库中的相同对象,会发生什么情况?我们不知道写入的顺序是怎样的,但是我们通常认为后面的写入会覆盖前面的写入。
 
-但是,如果先前的写入是尚未提交事务的一部分,又会发生什么情况,后面的写入会覆盖一个尚未提交的值?这被称作**脏写(dirty write)**【28】。在**读已提交**的隔离级别上运行的事务必须防止脏写,通常是延迟第二次写入,直到第一次写入事务提交或中止为止。
+但是,如果先前的写入是尚未提交事务的一部分,又会发生什么情况,后面的写入会覆盖一个尚未提交的值?这被称作 **脏写(dirty write)**【28】。在 **读已提交** 的隔离级别上运行的事务必须防止脏写,通常是延迟第二次写入,直到第一次写入事务提交或中止为止。
 
 通过防止脏写,这个隔离级别避免了一些并发问题:
 
-- 如果事务更新多个对象,脏写会导致不好的结果。例如,考虑 [图7-5](img/fig7-5.png),以一个二手车销售网站为例,Alice和Bob两个人同时试图购买同一辆车。购买汽车需要两次数据库写入:网站上的商品列表需要更新,以反映买家的购买,销售发票需要发送给买家。在[图7-5](img/fig7-5.png)的情况下,销售是属于Bob的(因为他成功更新了商品列表),但发票却寄送给了爱丽丝(因为她成功更新了发票表)。读已提交会阻止这样的事故。
-- 但是,读已提交并不能防止[图7-1](img/fig7-1.png)中两个计数器增量之间的竞争状态。在这种情况下,第二次写入发生在第一个事务提交后,所以它不是一个脏写。这仍然是不正确的,但是出于不同的原因,在“[防止更新丢失](#防止丢失更新)”中将讨论如何使这种计数器增量安全。
+- 如果事务更新多个对象,脏写会导致不好的结果。例如,考虑 [图 7-5](img/fig7-5.png),以一个二手车销售网站为例,Alice 和 Bob 两个人同时试图购买同一辆车。购买汽车需要两次数据库写入:网站上的商品列表需要更新,以反映买家的购买,销售发票需要发送给买家。在 [图 7-5](img/fig7-5.png) 的情况下,销售是属于 Bob 的(因为他成功更新了商品列表),但发票却寄送给了爱丽丝(因为她成功更新了发票表)。读已提交会阻止这样的事故。
+- 但是,读已提交并不能防止 [图 7-1](img/fig7-1.png) 中两个计数器增量之间的竞争状态。在这种情况下,第二次写入发生在第一个事务提交后,所以它不是一个脏写。这仍然是不正确的,但是出于不同的原因,在 “[防止更新丢失](#防止丢失更新)” 中将讨论如何使这种计数器增量安全。
 
 ![](img/fig7-5.png)
 
-**图7-5 如果存在脏写,来自不同事务的冲突写入可能会混淆在一起**
+**图 7-5 如果存在脏写,来自不同事务的冲突写入可能会混淆在一起**
 
 #### 实现读已提交
 
-**读已提交**是一个非常流行的隔离级别。这是Oracle 11g,PostgreSQL,SQL Server 2012,MemSQL和其他许多数据库的默认设置【8】。
+**读已提交** 是一个非常流行的隔离级别。这是 Oracle 11g、PostgreSQL、SQL Server 2012、MemSQL 和其他许多数据库的默认设置【8】。
 
-最常见的情况是,数据库通过使用**行锁(row-level lock)** 来防止脏写:当事务想要修改特定对象(行或文档)时,它必须首先获得该对象的锁。然后必须持有该锁直到事务被提交或中止。一次只有一个事务可持有任何给定对象的锁;如果另一个事务要写入同一个对象,则必须等到第一个事务提交或中止后,才能获取该锁并继续。这种锁定是读已提交模式(或更强的隔离级别)的数据库自动完成的。
+最常见的情况是,数据库通过使用 **行锁(row-level lock)** 来防止脏写:当事务想要修改特定对象(行或文档)时,它必须首先获得该对象的锁。然后必须持有该锁直到事务被提交或中止。一次只有一个事务可持有任何给定对象的锁;如果另一个事务要写入同一个对象,则必须等到第一个事务提交或中止后,才能获取该锁并继续。这种锁定是读已提交模式(或更强的隔离级别)的数据库自动完成的。
 
 如何防止脏读?一种选择是使用相同的锁,并要求任何想要读取对象的事务来简单地获取该锁,然后在读取之后立即再次释放该锁。这能确保在读取进行时,对象不会在脏的、有未提交的值的状态(因为在那段时间锁会被写入该对象的事务持有)。
 
 但是要求读锁的办法在实践中效果并不好。因为一个长时间运行的写入事务会迫使许多只读事务等到这个慢写入事务完成。这会损失只读事务的响应时间,并且不利于可操作性:因为等待锁,应用某个部分的迟缓可能由于连锁效应,导致其他部分出现问题。
 
-出于这个原因,大多数数据库[^vi]使用[图7-4](img/fig7-4.png)的方式防止脏读:对于写入的每个对象,数据库都会记住旧的已提交值,和由当前持有写入锁的事务设置的新值。当事务正在进行时,任何其他读取对象的事务都会拿到旧值。 只有当新值提交后,事务才会切换到读取新值。
+出于这个原因,大多数数据库 [^vi] 使用 [图 7-4](img/fig7-4.png) 的方式防止脏读:对于写入的每个对象,数据库都会记住旧的已提交值,和由当前持有写入锁的事务设置的新值。当事务正在进行时,任何其他读取对象的事务都会拿到旧值。 只有当新值提交后,事务才会切换到读取新值。
 
-[^vi]: 在撰写本文时,唯一在读已提交隔离级别使用读锁的主流数据库是使用`read_committed_snapshot = off`配置的IBM DB2和Microsoft SQL Server 【23,36】。
+[^vi]: 在撰写本文时,唯一在读已提交隔离级别使用读锁的主流数据库是使用 `read_committed_snapshot = off` 配置的 IBM DB2 和 Microsoft SQL Server 【23,36】。
 
 ### 快照隔离和可重复读
 
-如果只从表面上看读已提交隔离级别你就认为它完成了事务所需的一切,那是可以原谅的。它允许**中止**(原子性的要求);它防止读取不完整的事务结果,并且防止并发写入造成的混乱。事实上这些功能非常有用,比起没有事务的系统来,可以提供更多的保证。
+如果只从表面上看读已提交隔离级别你就认为它完成了事务所需的一切,那是可以原谅的。它允许 **中止**(原子性的要求);它防止读取不完整的事务结果,并且防止并发写入造成的混乱。事实上这些功能非常有用,比起没有事务的系统来,可以提供更多的保证。
 
-但是在使用此隔离级别时,仍然有很多地方可能会产生并发错误。例如[图7-6](img/fig7-6.png)说明了读已提交时可能发生的问题。
+但是在使用此隔离级别时,仍然有很多地方可能会产生并发错误。例如 [图 7-6](img/fig7-6.png) 说明了读已提交时可能发生的问题。
 
 ![](img/fig7-6.png)
 
-**图7-6 读取偏差:Alice观察数据库处于不一致的状态**
+**图 7-6 读取偏差:Alice 观察数据库处于不一致的状态**
 
-爱丽丝在银行有1000美元的储蓄,分为两个账户,每个500美元。现在有一笔事务从她的一个账户转移了100美元到另一个账户。如果她非常不幸地在事务处理的过程中查看其账户余额列表,她可能会在收到付款之前先看到一个账户的余额(收款账户,余额仍为500美元),在发出转账之后再看到另一个账户的余额(付款账户,新余额为400美元)。对爱丽丝来说,现在她的账户似乎总共只有900美元——看起来有100美元已经凭空消失了。
+爱丽丝在银行有 1000 美元的储蓄,分为两个账户,每个 500 美元。现在有一笔事务从她的一个账户转移了 100 美元到另一个账户。如果她非常不幸地在事务处理的过程中查看其账户余额列表,她可能会在收到付款之前先看到一个账户的余额(收款账户,余额仍为 500 美元),在发出转账之后再看到另一个账户的余额(付款账户,新余额为 400 美元)。对爱丽丝来说,现在她的账户似乎总共只有 900 美元 —— 看起来有 100 美元已经凭空消失了。
 
-这种异常被称为**不可重复读(nonrepeatable read)** 或**读取偏差(read skew)**:如果Alice在事务结束时再次读取账户1的余额,她将看到与她之前的查询中看到的不同的值(600美元)。在读已提交的隔离条件下,**不可重复读**被认为是可接受的:Alice看到的帐户余额时确实在阅读时已经提交了。
+这种异常被称为 **不可重复读(nonrepeatable read)** 或 **读取偏差(read skew)**:如果 Alice 在事务结束时再次读取账户 1 的余额,她将看到与她之前的查询中看到的不同的值(600 美元)。在读已提交的隔离条件下,**不可重复读** 被认为是可接受的:Alice 看到的帐户余额时确实在阅读时已经提交了。
 
-> 不幸的是,术语**偏差(skew)** 这个词是过载的:以前使用它是因为热点的不平衡工作量(请参阅“[负载偏斜与热点消除](ch6.md#负载偏斜与热点消除)”),而这里偏差意味着异常的时序。
+> 不幸的是,术语 **偏差(skew)** 这个词是过载的:以前使用它是因为热点的不平衡工作量(请参阅 “[负载偏斜与热点消除](ch6.md#负载偏斜与热点消除)”),而这里偏差意味着异常的时序。
 
-对于Alice的情况,这不是一个长期持续的问题。因为如果她几秒钟后刷新银行网站的页面,她很可能会看到一致的帐户余额。但是有些情况下,不能容忍这种暂时的不一致:
+对于 Alice 的情况,这不是一个长期持续的问题。因为如果她几秒钟后刷新银行网站的页面,她很可能会看到一致的帐户余额。但是有些情况下,不能容忍这种暂时的不一致:
 
 * 备份
 
@@ -307,46 +307,46 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
 
 * 分析查询和完整性检查
 
-  有时,你可能需要运行一个查询,扫描大部分的数据库。这样的查询在分析中很常见(请参阅“[事务处理还是分析?](ch3.md#事务处理还是分析?)”),也可能是定期完整性检查(即监视数据损坏)的一部分。如果这些查询在不同时间点观察数据库的不同部分,则可能会返回毫无意义的结果。
+  有时,你可能需要运行一个查询,扫描大部分的数据库。这样的查询在分析中很常见(请参阅 “[事务处理还是分析?](ch3.md#事务处理还是分析?)”),也可能是定期完整性检查(即监视数据损坏)的一部分。如果这些查询在不同时间点观察数据库的不同部分,则可能会返回毫无意义的结果。
 
-**快照隔离(snapshot isolation)**【28】是这个问题最常见的解决方案。想法是,每个事务都从数据库的**一致快照(consistent snapshot)** 中读取——也就是说,事务可以看到事务开始时在数据库中提交的所有数据。即使这些数据随后被另一个事务更改,每个事务也只能看到该特定时间点的旧数据。
+**快照隔离(snapshot isolation)**【28】是这个问题最常见的解决方案。想法是,每个事务都从数据库的 **一致快照(consistent snapshot)** 中读取 —— 也就是说,事务可以看到事务开始时在数据库中提交的所有数据。即使这些数据随后被另一个事务更改,每个事务也只能看到该特定时间点的旧数据。
 
 快照隔离对长时间运行的只读查询(如备份和分析)非常有用。如果查询的数据在查询执行的同时发生变化,则很难理解查询的含义。当一个事务可以看到数据库在某个特定时间点冻结时的一致快照,理解起来就很容易了。
 
-快照隔离是一个流行的功能:PostgreSQL,使用InnoDB引擎的MySQL,Oracle,SQL Server等都支持【23,31,32】。
+快照隔离是一个流行的功能:PostgreSQL、使用 InnoDB 引擎的 MySQL、Oracle、SQL Server 等都支持【23,31,32】。
 
 #### 实现快照隔离
 
-与读取提交的隔离类似,快照隔离的实现通常使用写锁来防止脏写(请参阅“[读已提交](#读已提交)”),这意味着进行写入的事务会阻止另一个事务修改同一个对象。但是读取不需要任何锁定。从性能的角度来看,快照隔离的一个关键原则是:**读不阻塞写,写不阻塞读**。这允许数据库在处理一致性快照上的长时间查询时,可以正常地同时处理写入操作。且两者间没有任何锁定争用。
+与读取提交的隔离类似,快照隔离的实现通常使用写锁来防止脏写(请参阅 “[读已提交](#读已提交)”),这意味着进行写入的事务会阻止另一个事务修改同一个对象。但是读取不需要任何锁定。从性能的角度来看,快照隔离的一个关键原则是:**读不阻塞写,写不阻塞读**。这允许数据库在处理一致性快照上的长时间查询时,可以正常地同时处理写入操作。且两者间没有任何锁定争用。
 
-为了实现快照隔离,数据库使用了我们看到的用于防止[图7-4](img/fig7-4.png)中的脏读的机制的一般化。数据库必须可能保留一个对象的几个不同的提交版本,因为各种正在进行的事务可能需要看到数据库在不同的时间点的状态。因为它同时维护着单个对象的多个版本,所以这种技术被称为**多版本并发控制(MVCC, multi-version concurrency control)**。
+为了实现快照隔离,数据库使用了我们看到的用于防止 [图 7-4](img/fig7-4.png) 中的脏读的机制的一般化。数据库必须可能保留一个对象的几个不同的提交版本,因为各种正在进行的事务可能需要看到数据库在不同的时间点的状态。因为它同时维护着单个对象的多个版本,所以这种技术被称为 **多版本并发控制(MVCC, multi-version concurrency control)**。
 
-如果一个数据库只需要提供**读已提交**的隔离级别,而不提供**快照隔离**,那么保留一个对象的两个版本就足够了:提交的版本和被覆盖但尚未提交的版本。支持快照隔离的存储引擎通常也使用MVCC来实现**读已提交**隔离级别。一种典型的方法是**读已提交**为每个查询使用单独的快照,而**快照隔离**对整个事务使用相同的快照。
+如果一个数据库只需要提供 **读已提交** 的隔离级别,而不提供 **快照隔离**,那么保留一个对象的两个版本就足够了:提交的版本和被覆盖但尚未提交的版本。支持快照隔离的存储引擎通常也使用 MVCC 来实现 **读已提交** 隔离级别。一种典型的方法是 **读已提交** 为每个查询使用单独的快照,而 **快照隔离** 对整个事务使用相同的快照。
 
-[图7-7](img/fig7-7.png)说明了如何在PostgreSQL中实现基于MVCC的快照隔离【31】(其他实现类似)。当一个事务开始时,它被赋予一个唯一的,永远增长[^vii]的事务ID(`txid`)。每当事务向数据库写入任何内容时,它所写入的数据都会被标记上写入者的事务ID。
+[图 7-7](img/fig7-7.png) 说明了如何在 PostgreSQL 中实现基于 MVCC 的快照隔离【31】(其他实现类似)。当一个事务开始时,它被赋予一个唯一的,永远增长 [^vii] 的事务 ID(`txid`)。每当事务向数据库写入任何内容时,它所写入的数据都会被标记上写入者的事务 ID。
 
-[^vii]: 事实上,事务ID是32位整数,所以大约会在40亿次事务之后溢出。 PostgreSQL的Vacuum过程会清理老旧的事务ID,确保事务ID溢出(回卷)不会影响到数据。
+[^vii]: 事实上,事务 ID 是 32 位整数,所以大约会在 40 亿次事务之后溢出。 PostgreSQL 的 Vacuum 过程会清理老旧的事务 ID,确保事务 ID 溢出(回卷)不会影响到数据。
 
 ![](img/fig7-7.png)
 
-**图7-7 使用多版本对象实现快照隔离**
+**图 7-7 使用多版本对象实现快照隔离**
 
-表中的每一行都有一个 `created_by` 字段,其中包含将该行插入到表中的的事务ID。此外,每行都有一个 `deleted_by` 字段,最初是空的。如果某个事务删除了一行,那么该行实际上并未从数据库中删除,而是通过将 `deleted_by` 字段设置为请求删除的事务的ID来标记为删除。在稍后的时间,当确定没有事务可以再访问已删除的数据时,数据库中的垃圾收集过程会将所有带有删除标记的行移除,并释放其空间。[^译注ii]
+表中的每一行都有一个 `created_by` 字段,其中包含将该行插入到表中的的事务 ID。此外,每行都有一个 `deleted_by` 字段,最初是空的。如果某个事务删除了一行,那么该行实际上并未从数据库中删除,而是通过将 `deleted_by` 字段设置为请求删除的事务的 ID 来标记为删除。在稍后的时间,当确定没有事务可以再访问已删除的数据时,数据库中的垃圾收集过程会将所有带有删除标记的行移除,并释放其空间。[^译注ii]
 
-[^译注ii]: 在PostgreSQL中,`created_by` 的实际名称为`xmin`,`deleted_by` 的实际名称为`xmax`
+[^译注ii]: 在 PostgreSQL 中,`created_by` 的实际名称为 `xmin`,`deleted_by` 的实际名称为 `xmax`
 
-`UPDATE` 操作在内部翻译为 `DELETE` 和 `INSERT` 。例如,在[图7-7](img/fig7-7.png)中,事务13 从账户2 中扣除100美元,将余额从500美元改为400美元。实际上包含两条账户2 的记录:余额为 \$500 的行被标记为**被事务13删除**,余额为 \$400 的行**由事务13创建**。
+`UPDATE` 操作在内部翻译为 `DELETE` 和 `INSERT` 。例如,在 [图 7-7](img/fig7-7.png) 中,事务 13 从账户 2 中扣除 100 美元,将余额从 500 美元改为 400 美元。实际上包含两条账户 2 的记录:余额为 \$500 的行被标记为 **被事务 13 删除**,余额为 \$400 的行 **由事务 13 创建**。
 
 #### 观察一致性快照的可见性规则
 
-当一个事务从数据库中读取时,事务ID用于决定它可以看见哪些对象,看不见哪些对象。通过仔细定义可见性规则,数据库可以向应用程序呈现一致的数据库快照。工作如下:
+当一个事务从数据库中读取时,事务 ID 用于决定它可以看见哪些对象,看不见哪些对象。通过仔细定义可见性规则,数据库可以向应用程序呈现一致的数据库快照。工作如下:
 
 1. 在每次事务开始时,数据库列出当时所有其他(尚未提交或尚未中止)的事务清单,即使之后提交了,这些事务已执行的任何写入也都会被忽略。
 2. 被中止事务所执行的任何写入都将被忽略。
-3. 由具有较晚事务ID(即,在当前事务开始之后开始的)的事务所做的任何写入都被忽略,而不管这些事务是否已经提交。
+3. 由具有较晚事务 ID(即,在当前事务开始之后开始的)的事务所做的任何写入都被忽略,而不管这些事务是否已经提交。
 4. 所有其他写入,对应用都是可见的。
 
-这些规则适用于创建和删除对象。在[图7-7](img/fig7-7.png)中,当事务12 从账户2 读取时,它会看到 \$500 的余额,因为 \$500 余额的删除是由事务13 完成的(根据规则3,事务12 看不到事务13 执行的删除),且400美元记录的创建也是不可见的(按照相同的规则)。
+这些规则适用于创建和删除对象。在 [图 7-7](img/fig7-7.png) 中,当事务 12 从账户 2 读取时,它会看到 \$500 的余额,因为 \$500 余额的删除是由事务 13 完成的(根据规则 3,事务 12 看不到事务 13 执行的删除),且 400 美元记录的创建也是不可见的(按照相同的规则)。
 
 换句话说,如果以下两个条件都成立,则可见一个对象:
 
@@ -359,59 +359,59 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
 
 索引如何在多版本数据库中工作?一种选择是使索引简单地指向对象的所有版本,并且需要索引查询来过滤掉当前事务不可见的任何对象版本。当垃圾收集删除任何事务不再可见的旧对象版本时,相应的索引条目也可以被删除。
 
-在实践中,许多实现细节决定了多版本并发控制的性能。例如,如果同一对象的不同版本可以放入同一个页面中,PostgreSQL的优化可以避免更新索引【31】。
+在实践中,许多实现细节决定了多版本并发控制的性能。例如,如果同一对象的不同版本可以放入同一个页面中,PostgreSQL 的优化可以避免更新索引【31】。
 
-在CouchDB,Datomic和LMDB中使用另一种方法。虽然它们也使用[B树](ch3.md#B树),但它们使用的是一种**仅追加/写时拷贝(append-only/copy-on-write)** 的变体,它们在更新时不覆盖树的页面,而为每个修改页面创建一份副本。从父页面直到树根都会级联更新,以指向它们子页面的新版本。任何不受写入影响的页面都不需要被复制,并且保持不变【33,34,35】。
+在 CouchDB、Datomic 和 LMDB 中使用另一种方法。虽然它们也使用 [B 树](ch3.md#B树),但它们使用的是一种 **仅追加 / 写时拷贝(append-only/copy-on-write)** 的变体,它们在更新时不覆盖树的页面,而为每个修改页面创建一份副本。从父页面直到树根都会级联更新,以指向它们子页面的新版本。任何不受写入影响的页面都不需要被复制,并且保持不变【33,34,35】。
 
-使用仅追加的B树,每个写入事务(或一批事务)都会创建一颗新的B树,当创建时,从该特定树根生长的树就是数据库的一个一致性快照。没必要根据事务ID过滤掉对象,因为后续写入不能修改现有的B树;它们只能创建新的树根。但这种方法也需要一个负责压缩和垃圾收集的后台进程。
+使用仅追加的 B 树,每个写入事务(或一批事务)都会创建一颗新的 B 树,当创建时,从该特定树根生长的树就是数据库的一个一致性快照。没必要根据事务 ID 过滤掉对象,因为后续写入不能修改现有的 B 树;它们只能创建新的树根。但这种方法也需要一个负责压缩和垃圾收集的后台进程。
 
 #### 可重复读与命名混淆
 
-快照隔离是一个有用的隔离级别,特别对于只读事务而言。但是,许多数据库实现了它,却用不同的名字来称呼。在Oracle中称为**可串行化(Serializable)** 的,在PostgreSQL和MySQL中称为**可重复读(repeatable read)**【23】。
+快照隔离是一个有用的隔离级别,特别对于只读事务而言。但是,许多数据库实现了它,却用不同的名字来称呼。在 Oracle 中称为 **可串行化(Serializable)** 的,在 PostgreSQL 和 MySQL 中称为 **可重复读(repeatable read)**【23】。
 
-这种命名混淆的原因是SQL标准没有**快照隔离**的概念,因为标准是基于System R 1975年定义的隔离级别【2】,那时候**快照隔离**尚未发明。相反,它定义了**可重复读**,表面上看起来与快照隔离很相似。 PostgreSQL和MySQL称其**快照隔离**级别为**可重复读(repeatable read)**,因为这样符合标准要求,所以它们可以声称自己“标准兼容”。
+这种命名混淆的原因是 SQL 标准没有 **快照隔离** 的概念,因为标准是基于 System R 1975 年定义的隔离级别【2】,那时候 **快照隔离** 尚未发明。相反,它定义了 **可重复读**,表面上看起来与快照隔离很相似。 PostgreSQL 和 MySQL 称其 **快照隔离** 级别为 **可重复读(repeatable read)**,因为这样符合标准要求,所以它们可以声称自己 “标准兼容”。
 
-不幸的是,SQL标准对隔离级别的定义是有缺陷的——模糊,不精确,并不像标准应有的样子独立于实现【28】。有几个数据库实现了可重复读,但它们实际提供的保证存在很大的差异,尽管表面上是标准化的【23】。在研究文献【29,30】中已经有了可重复读的正式定义,但大多数的实现并不能满足这个正式定义。最后,IBM DB2使用“可重复读”来引用可串行化【8】。
+不幸的是,SQL 标准对隔离级别的定义是有缺陷的 —— 模糊,不精确,并不像标准应有的样子独立于实现【28】。有几个数据库实现了可重复读,但它们实际提供的保证存在很大的差异,尽管表面上是标准化的【23】。在研究文献【29,30】中已经有了可重复读的正式定义,但大多数的实现并不能满足这个正式定义。最后,IBM DB2 使用 “可重复读” 来引用可串行化【8】。
 
-结果,没有人真正知道**可重复读**的意思。
+结果,没有人真正知道 **可重复读** 的意思。
 
 ### 防止丢失更新
 
-到目前为止已经讨论的**读已提交**和**快照隔离**级别,主要保证了**只读事务在并发写入时**可以看到什么。却忽略了两个事务并发写入的问题——我们只讨论了脏写(请参阅“[没有脏写](#没有脏写)”),一种特定类型的写-写冲突是可能出现的。
+到目前为止已经讨论的 **读已提交** 和 **快照隔离** 级别,主要保证了 **只读事务在并发写入时** 可以看到什么。却忽略了两个事务并发写入的问题 —— 我们只讨论了脏写(请参阅 “[没有脏写](#没有脏写)”),一种特定类型的写 - 写冲突是可能出现的。
 
-并发的写入事务之间还有其他几种有趣的冲突。其中最着名的是**丢失更新(lost update)** 问题,如[图7-1](img/fig7-1.png)所示,以两个并发计数器增量为例。
+并发的写入事务之间还有其他几种有趣的冲突。其中最着名的是 **丢失更新(lost update)** 问题,如 [图 7-1](img/fig7-1.png) 所示,以两个并发计数器增量为例。
 
-如果应用从数据库中读取一些值,修改它并写回修改的值(读取-修改-写入序列),则可能会发生丢失更新的问题。如果两个事务同时执行,则其中一个的修改可能会丢失,因为第二个写入的内容并没有包括第一个事务的修改(有时会说后面写入**狠揍(clobber)** 了前面的写入)这种模式发生在各种不同的情况下:
+如果应用从数据库中读取一些值,修改它并写回修改的值(读取 - 修改 - 写入序列),则可能会发生丢失更新的问题。如果两个事务同时执行,则其中一个的修改可能会丢失,因为第二个写入的内容并没有包括第一个事务的修改(有时会说后面写入 **狠揍(clobber)** 了前面的写入)这种模式发生在各种不同的情况下:
 
 - 增加计数器或更新账户余额(需要读取当前值,计算新值并写回更新后的值)
-- 在复杂值中进行本地修改:例如,将元素添加到JSON文档中的一个列表(需要解析文档,进行更改并写回修改的文档)
-- 两个用户同时编辑wiki页面,每个用户通过将整个页面内容发送到服务器来保存其更改,覆写数据库中当前的任何内容。
+- 在复杂值中进行本地修改:例如,将元素添加到 JSON 文档中的一个列表(需要解析文档,进行更改并写回修改的文档)
+- 两个用户同时编辑 wiki 页面,每个用户通过将整个页面内容发送到服务器来保存其更改,覆写数据库中当前的任何内容。
 
 这是一个普遍的问题,所以已经开发了各种解决方案。
 
 #### 原子写
 
-许多数据库提供了原子更新操作,从而消除了在应用程序代码中执行读取-修改-写入序列的需要。如果你的代码可以用这些操作来表达,那这通常是最好的解决方案。例如,下面的指令在大多数关系数据库中是并发安全的:
+许多数据库提供了原子更新操作,从而消除了在应用程序代码中执行读取 - 修改 - 写入序列的需要。如果你的代码可以用这些操作来表达,那这通常是最好的解决方案。例如,下面的指令在大多数关系数据库中是并发安全的:
 
 ```sql
 UPDATE counters SET value = value + 1 WHERE key = 'foo';
 ```
 
-类似地,像MongoDB这样的文档数据库提供了对JSON文档的一部分进行本地修改的原子操作,Redis提供了修改数据结构(如优先级队列)的原子操作。并不是所有的写操作都可以用原子操作的方式来表达,例如维基页面的更新涉及到任意文本编辑[^viii],但是在可以使用原子操作的情况下,它们通常是最好的选择。
+类似地,像 MongoDB 这样的文档数据库提供了对 JSON 文档的一部分进行本地修改的原子操作,Redis 提供了修改数据结构(如优先级队列)的原子操作。并不是所有的写操作都可以用原子操作的方式来表达,例如维基页面的更新涉及到任意文本编辑 [^viii],但是在可以使用原子操作的情况下,它们通常是最好的选择。
 
-[^viii]: 将文本文档的编辑表示为原子的变化流是可能的,尽管相当复杂。请参阅“[自动冲突解决](ch5.md#自动冲突解决)”。
+[^viii]: 将文本文档的编辑表示为原子的变化流是可能的,尽管相当复杂。请参阅 “[自动冲突解决](ch5.md#自动冲突解决)”。
 
-原子操作通常通过在读取对象时,获取其上的排它锁来实现。以便更新完成之前没有其他事务可以读取它。这种技术有时被称为**游标稳定性(cursor stability)**【36,37】。另一个选择是简单地强制所有的原子操作在单一线程上执行。
+原子操作通常通过在读取对象时,获取其上的排它锁来实现。以便更新完成之前没有其他事务可以读取它。这种技术有时被称为 **游标稳定性(cursor stability)**【36,37】。另一个选择是简单地强制所有的原子操作在单一线程上执行。
 
-不幸的是,ORM框架很容易意外地执行不安全的读取-修改-写入序列,而不是使用数据库提供的原子操作【38】。如果你知道自己在做什么那当然不是问题,但它经常产生那种很难测出来的微妙Bug。
+不幸的是,ORM 框架很容易意外地执行不安全的读取 - 修改 - 写入序列,而不是使用数据库提供的原子操作【38】。如果你知道自己在做什么那当然不是问题,但它经常产生那种很难测出来的微妙 Bug。
 
 #### 显式锁定
 
-如果数据库的内置原子操作没有提供必要的功能,防止丢失更新的另一个选择是让应用程序显式地锁定将要更新的对象。然后应用程序可以执行读取-修改-写入序列,如果任何其他事务尝试同时读取同一个对象,则强制等待,直到第一个**读取-修改-写入序列**完成。
+如果数据库的内置原子操作没有提供必要的功能,防止丢失更新的另一个选择是让应用程序显式地锁定将要更新的对象。然后应用程序可以执行读取 - 修改 - 写入序列,如果任何其他事务尝试同时读取同一个对象,则强制等待,直到第一个 **读取 - 修改 - 写入序列** 完成。
 
-例如,考虑一个多人游戏,其中几个玩家可以同时移动相同的棋子。在这种情况下,一个原子操作可能是不够的,因为应用程序还需要确保玩家的移动符合游戏规则,这可能涉及到一些不能合理地用数据库查询实现的逻辑。但你可以使用锁来防止两名玩家同时移动相同的棋子,如例7-1所示。
+例如,考虑一个多人游戏,其中几个玩家可以同时移动相同的棋子。在这种情况下,一个原子操作可能是不够的,因为应用程序还需要确保玩家的移动符合游戏规则,这可能涉及到一些不能合理地用数据库查询实现的逻辑。但你可以使用锁来防止两名玩家同时移动相同的棋子,如例 7-1 所示。
 
-**例7-1 显式锁定行以防止丢失更新**
+**例 7-1 显式锁定行以防止丢失更新**
 
 ```plsql
 BEGIN TRANSACTION;
@@ -419,28 +419,28 @@ SELECT * FROM figures
   WHERE name = 'robot' AND game_id = 222
 FOR UPDATE;
 
--- 检查玩家的操作是否有效,然后更新先前SELECT返回棋子的位置。
+-- 检查玩家的操作是否有效,然后更新先前 SELECT 返回棋子的位置。
 UPDATE figures SET position = 'c4' WHERE id = 1234;
 COMMIT;
 ```
 
-- `FOR UPDATE`子句告诉数据库应该对该查询返回的所有行加锁。
+- `FOR UPDATE` 子句告诉数据库应该对该查询返回的所有行加锁。
 
 这是有效的,但要做对,你需要仔细考虑应用逻辑。忘记在代码某处加锁很容易引入竞争条件。
 
 #### 自动检测丢失的更新
 
-原子操作和锁是通过强制**读取-修改-写入序列**按顺序发生,来防止丢失更新的方法。另一种方法是允许它们并行执行,如果事务管理器检测到丢失更新,则中止事务并强制它们重试其**读取-修改-写入序列**。
+原子操作和锁是通过强制 **读取 - 修改 - 写入序列** 按顺序发生,来防止丢失更新的方法。另一种方法是允许它们并行执行,如果事务管理器检测到丢失更新,则中止事务并强制它们重试其 **读取 - 修改 - 写入序列**。
 
-这种方法的一个优点是,数据库可以结合快照隔离高效地执行此检查。事实上,PostgreSQL的可重复读,Oracle的可串行化和SQL Server的快照隔离级别,都会自动检测到丢失更新,并中止惹麻烦的事务。但是,MySQL/InnoDB的可重复读并不会检测**丢失更新**【23】。一些作者【28,30】认为,数据库必须能防止丢失更新才称得上是提供了**快照隔离**,所以在这个定义下,MySQL下不提供快照隔离。
+这种方法的一个优点是,数据库可以结合快照隔离高效地执行此检查。事实上,PostgreSQL 的可重复读,Oracle 的可串行化和 SQL Server 的快照隔离级别,都会自动检测到丢失更新,并中止惹麻烦的事务。但是,MySQL/InnoDB 的可重复读并不会检测 **丢失更新**【23】。一些作者【28,30】认为,数据库必须能防止丢失更新才称得上是提供了 **快照隔离**,所以在这个定义下,MySQL 下不提供快照隔离。
 
 丢失更新检测是一个很好的功能,因为它不需要应用代码使用任何特殊的数据库功能,你可能会忘记使用锁或原子操作,从而引入错误;但丢失更新的检测是自动发生的,因此不太容易出错。
 
 #### 比较并设置(CAS)
 
-在不提供事务的数据库中,有时会发现一种原子操作:**比较并设置(CAS, 即Compare And Set,先前在“[单对象写入](#单对象写入)”中提到)。此操作的目的是为了避免丢失更新:只有当前值从上次读取时一直未改变,才允许更新发生。如果当前值与先前读取的值不匹配,则更新不起作用,且必须重试读取-修改-写入序列。
+在不提供事务的数据库中,有时会发现一种原子操作:** 比较并设置(CAS, 即 Compare And Set,先前在 “[单对象写入](#单对象写入)” 中提到)。此操作的目的是为了避免丢失更新:只有当前值从上次读取时一直未改变,才允许更新发生。如果当前值与先前读取的值不匹配,则更新不起作用,且必须重试读取 - 修改 - 写入序列。
 
-例如,为了防止两个用户同时更新同一个wiki页面,可以尝试类似这样的方式,只有当用户开始编辑页面内容时,才会发生更新:
+例如,为了防止两个用户同时更新同一个 wiki 页面,可以尝试类似这样的方式,只有当用户开始编辑页面内容时,才会发生更新:
 
 ```sql
 -- 根据数据库的实现情况,这可能安全也可能不安全
@@ -448,64 +448,64 @@ UPDATE wiki_pages SET content = '新内容'
   WHERE id = 1234 AND content = '旧内容';
 ```
 
-如果内容已经更改并且不再与“旧内容”相匹配,则此更新将不起作用,因此你需要检查更新是否生效,必要时重试。但是,如果数据库允许`WHERE`子句从旧快照中读取,则此语句可能无法防止丢失更新,因为即使发生了另一个并发写入,`WHERE`条件也可能为真。在依赖数据库的CAS操作前要检查其是否安全。
+如果内容已经更改并且不再与 “旧内容” 相匹配,则此更新将不起作用,因此你需要检查更新是否生效,必要时重试。但是,如果数据库允许 `WHERE` 子句从旧快照中读取,则此语句可能无法防止丢失更新,因为即使发生了另一个并发写入,`WHERE` 条件也可能为真。在依赖数据库的 CAS 操作前要检查其是否安全。
 
 #### 冲突解决和复制
 
-在复制数据库中(请参阅[第五章](ch5.md)),防止丢失的更新需要考虑另一个维度:由于在多个节点上存在数据副本,并且在不同节点上的数据可能被并发地修改,因此需要采取一些额外的步骤来防止丢失更新。
+在复制数据库中(请参阅 [第五章](ch5.md)),防止丢失的更新需要考虑另一个维度:由于在多个节点上存在数据副本,并且在不同节点上的数据可能被并发地修改,因此需要采取一些额外的步骤来防止丢失更新。
 
-锁和CAS操作假定只有一个最新的数据副本。但是多主或无主复制的数据库通常允许多个写入并发执行,并异步复制到副本上,因此无法保证只有一个最新数据的副本。所以基于锁或CAS操作的技术不适用于这种情况(我们将在“[线性一致性](ch9.md#线性一致性)”中更详细地讨论这个问题)。
+锁和 CAS 操作假定只有一个最新的数据副本。但是多主或无主复制的数据库通常允许多个写入并发执行,并异步复制到副本上,因此无法保证只有一个最新数据的副本。所以基于锁或 CAS 操作的技术不适用于这种情况(我们将在 “[线性一致性](ch9.md#线性一致性)” 中更详细地讨论这个问题)。
 
-相反,如“[检测并发写入](ch5.md#检测并发写入)”一节所述,这种复制数据库中的一种常见方法是允许并发写入创建多个冲突版本的值(也称为兄弟),并使用应用代码或特殊数据结构在事实发生之后解决和合并这些版本。
+相反,如 “[检测并发写入](ch5.md#检测并发写入)” 一节所述,这种复制数据库中的一种常见方法是允许并发写入创建多个冲突版本的值(也称为兄弟),并使用应用代码或特殊数据结构在事实发生之后解决和合并这些版本。
 
-原子操作可以在复制的上下文中很好地工作,尤其当它们具有可交换性时(即,可以在不同的副本上以不同的顺序应用它们,且仍然可以得到相同的结果)。例如,递增计数器或向集合添加元素是可交换的操作。这是Riak 2.0数据类型背后的思想,它可以防止复制副本丢失更新。当不同的客户端同时更新一个值时,Riak自动将更新合并在一起,以免丢失更新【39】。
+原子操作可以在复制的上下文中很好地工作,尤其当它们具有可交换性时(即,可以在不同的副本上以不同的顺序应用它们,且仍然可以得到相同的结果)。例如,递增计数器或向集合添加元素是可交换的操作。这是 Riak 2.0 数据类型背后的思想,它可以防止复制副本丢失更新。当不同的客户端同时更新一个值时,Riak 自动将更新合并在一起,以免丢失更新【39】。
 
-另一方面,最后写入胜利(LWW)的冲突解决方法很容易丢失更新,如“[最后写入胜利(丢弃并发写入)](ch5.md#最后写入胜利(丢弃并发写入))”中所述。不幸的是,LWW是许多复制数据库中的默认方案。
+另一方面,最后写入胜利(LWW)的冲突解决方法很容易丢失更新,如 “[最后写入胜利(丢弃并发写入)](ch5.md#最后写入胜利(丢弃并发写入))” 中所述。不幸的是,LWW 是许多复制数据库中的默认方案。
 
 ### 写入偏斜与幻读
 
-前面的章节中,我们看到了**脏写**和**丢失更新**,当不同的事务并发地尝试写入相同的对象时,会出现这两种竞争条件。为了避免数据损坏,这些竞争条件需要被阻止——既可以由数据库自动执行,也可以通过锁和原子写操作这类手动安全措施来防止。
+前面的章节中,我们看到了 **脏写** 和 **丢失更新**,当不同的事务并发地尝试写入相同的对象时,会出现这两种竞争条件。为了避免数据损坏,这些竞争条件需要被阻止 —— 既可以由数据库自动执行,也可以通过锁和原子写操作这类手动安全措施来防止。
 
 但是,并发写入间可能发生的竞争条件还没有完。在本节中,我们将看到一些更微妙的冲突例子。
 
 首先,想象一下这个例子:你正在为医院写一个医生轮班管理程序。医院通常会同时要求几位医生待命,但底线是至少有一位医生在待命。医生可以放弃他们的班次(例如,如果他们自己生病了),只要至少有一个同事在这一班中继续工作【40,41】。
 
-现在想象一下,Alice和Bob是两位值班医生。两人都感到不适,所以他们都决定请假。不幸的是,他们恰好在同一时间点击按钮下班。[图7-8](img/fig7-8.png)说明了接下来的事情。
+现在想象一下,Alice 和 Bob 是两位值班医生。两人都感到不适,所以他们都决定请假。不幸的是,他们恰好在同一时间点击按钮下班。[图 7-8](img/fig7-8.png) 说明了接下来的事情。
 
 ![](img/fig7-8.png)
 
-**图7-8 写入偏差导致应用程序错误的示例**
+**图 7-8 写入偏差导致应用程序错误的示例**
 
-在两个事务中,应用首先检查是否有两个或以上的医生正在值班;如果是的话,它就假定一名医生可以安全地休班。由于数据库使用快照隔离,两次检查都返回 2 ,所以两个事务都进入下一个阶段。Alice更新自己的记录休班了,而Bob也做了一样的事情。两个事务都成功提交了,现在没有医生值班了。违反了至少有一名医生在值班的要求。
+在两个事务中,应用首先检查是否有两个或以上的医生正在值班;如果是的话,它就假定一名医生可以安全地休班。由于数据库使用快照隔离,两次检查都返回 2 ,所以两个事务都进入下一个阶段。Alice 更新自己的记录休班了,而 Bob 也做了一样的事情。两个事务都成功提交了,现在没有医生值班了。违反了至少有一名医生在值班的要求。
 
 #### 写偏差的特征
 
-这种异常称为**写偏差**【28】。它既不是**脏写**,也不是**丢失更新**,因为这两个事务正在更新两个不同的对象(Alice和Bob各自的待命记录)。在这里发生的冲突并不是那么明显,但是这显然是一个竞争条件:如果两个事务一个接一个地运行,那么第二个医生就不能歇班了。异常行为只有在事务并发进行时才有可能。
+这种异常称为 **写偏差**【28】。它既不是 **脏写**,也不是 **丢失更新**,因为这两个事务正在更新两个不同的对象(Alice 和 Bob 各自的待命记录)。在这里发生的冲突并不是那么明显,但是这显然是一个竞争条件:如果两个事务一个接一个地运行,那么第二个医生就不能歇班了。异常行为只有在事务并发进行时才有可能。
 
 可以将写入偏差视为丢失更新问题的一般化。如果两个事务读取相同的对象,然后更新其中一些对象(不同的事务可能更新不同的对象),则可能发生写入偏差。在多个事务更新同一个对象的特殊情况下,就会发生脏写或丢失更新(取决于时序)。
 
 我们已经看到,有各种不同的方法来防止丢失的更新。但对于写偏差,我们的选择更受限制:
 
 * 由于涉及多个对象,单对象的原子操作不起作用。
-* 不幸的是,在一些快照隔离的实现中,自动检测丢失更新对此并没有帮助。在PostgreSQL的可重复读,MySQL/InnoDB的可重复读,Oracle可串行化或SQL Server的快照隔离级别中,都不会自动检测写入偏差【23】。自动防止写入偏差需要真正的可串行化隔离(请参阅“[可串行化](#可串行化)”)。
+* 不幸的是,在一些快照隔离的实现中,自动检测丢失更新对此并没有帮助。在 PostgreSQL 的可重复读,MySQL/InnoDB 的可重复读,Oracle 可串行化或 SQL Server 的快照隔离级别中,都不会自动检测写入偏差【23】。自动防止写入偏差需要真正的可串行化隔离(请参阅 “[可串行化](#可串行化)”)。
 * 某些数据库允许配置约束,然后由数据库强制执行(例如,唯一性,外键约束或特定值限制)。但是为了指定至少有一名医生必须在线,需要一个涉及多个对象的约束。大多数数据库没有内置对这种约束的支持,但是你可以使用触发器,或者物化视图来实现它们,这取决于不同的数据库【42】。
 * 如果无法使用可串行化的隔离级别,则此情况下的次优选项可能是显式锁定事务所依赖的行。在例子中,你可以写下如下的代码:
 
 ```sql
 BEGIN TRANSACTION;
 SELECT * FROM doctors
-  WHERE on_call = TRUE 
+  WHERE on_call = TRUE
   AND shift_id = 1234 FOR UPDATE;
 
 UPDATE doctors
   SET on_call = FALSE
-  WHERE name = 'Alice' 
+  WHERE name = 'Alice'
   AND shift_id = 1234;
   
 COMMIT;
 ```
 
-* 和以前一样,`FOR UPDATE`告诉数据库锁定返回的所有行以用于更新。
+* 和以前一样,`FOR UPDATE` 告诉数据库锁定返回的所有行以用于更新。
 
 #### 写偏差的更多例子
 
@@ -513,21 +513,21 @@ COMMIT;
 
 * 会议室预订系统
 
-  比如你想要规定不能在同一时间对同一个会议室进行多次的预订【43】。当有人想要预订时,首先检查是否存在相互冲突的预订(即预订时间范围重叠的同一房间),如果没有找到,则创建会议(请参阅示例7-2)[^ix]。
+  比如你想要规定不能在同一时间对同一个会议室进行多次的预订【43】。当有人想要预订时,首先检查是否存在相互冲突的预订(即预订时间范围重叠的同一房间),如果没有找到,则创建会议(请参阅示例 7-2)[^ix]。
 
-  [^ix]: 在PostgreSQL中,你可以使用范围类型优雅地执行此操作,但在其他数据库中并未得到广泛支持。
+  [^ix]: 在 PostgreSQL 中,你可以使用范围类型优雅地执行此操作,但在其他数据库中并未得到广泛支持。
 
-  **例7-2 会议室预订系统试图避免重复预订(在快照隔离下不安全)**
+  **例 7-2 会议室预订系统试图避免重复预订(在快照隔离下不安全)**
 
   ```sql
   BEGIN TRANSACTION;
 
-  -- 检查所有现存的与12:00~13:00重叠的预定
+  -- 检查所有现存的与 12:00~13:00 重叠的预定
   SELECT COUNT(*) FROM bookings
-  WHERE room_id = 123 AND 
+  WHERE room_id = 123 AND
     end_time > '2015-01-01 12:00' AND start_time < '2015-01-01 13:00';
 
-  -- 如果之前的查询返回0
+  -- 如果之前的查询返回 0
   INSERT INTO bookings(room_id, start_time, end_time, user_id)
     VALUES (123, '2015-01-01 12:00', '2015-01-01 13:00', 666);
 
@@ -538,7 +538,7 @@ COMMIT;
 
 * 多人游戏
 
-  在[例7-1]()中,我们使用一个锁来防止丢失更新(也就是确保两个玩家不能同时移动同一个棋子)。但是锁定并不妨碍玩家将两个不同的棋子移动到棋盘上的相同位置,或者采取其他违反游戏规则的行为。按照你正在执行的规则类型,也许可以使用唯一约束(unique constraint),否则你很容易发生写入偏差。
+  在 [例 7-1]() 中,我们使用一个锁来防止丢失更新(也就是确保两个玩家不能同时移动同一个棋子)。但是锁定并不妨碍玩家将两个不同的棋子移动到棋盘上的相同位置,或者采取其他违反游戏规则的行为。按照你正在执行的规则类型,也许可以使用唯一约束(unique constraint),否则你很容易发生写入偏差。
 
 * 抢注用户名
 
@@ -552,195 +552,195 @@ COMMIT;
 
 所有这些例子都遵循类似的模式:
 
-1. 一个`SELECT`查询找出符合条件的行,并检查是否符合一些要求。(例如:至少有两名医生在值班;不存在对该会议室同一时段的预定;棋盘上的位置没有被其他棋子占据;用户名还没有被抢注;账户里还有足够余额)
+1. 一个 `SELECT` 查询找出符合条件的行,并检查是否符合一些要求。(例如:至少有两名医生在值班;不存在对该会议室同一时段的预定;棋盘上的位置没有被其他棋子占据;用户名还没有被抢注;账户里还有足够余额)
 
 2. 按照第一个查询的结果,应用代码决定是否继续。(可能会继续操作,也可能中止并报错)
 
 3. 如果应用决定继续操作,就执行写入(插入、更新或删除),并提交事务。
 
-   这个写入的效果改变了步骤2 中的先决条件。换句话说,如果在提交写入后,重复执行一次步骤1 的SELECT查询,将会得到不同的结果。因为写入改变了符合搜索条件的行集(现在少了一个医生值班,那时候的会议室现在已经被预订了,棋盘上的这个位置已经被占据了,用户名已经被抢注,账户余额不够了)。
+   这个写入的效果改变了步骤 2 中的先决条件。换句话说,如果在提交写入后,重复执行一次步骤 1 的 SELECT 查询,将会得到不同的结果。因为写入改变了符合搜索条件的行集(现在少了一个医生值班,那时候的会议室现在已经被预订了,棋盘上的这个位置已经被占据了,用户名已经被抢注,账户余额不够了)。
 
-这些步骤可能以不同的顺序发生。例如可以首先进行写入,然后进行SELECT查询,最后根据查询结果决定是放弃还是提交。
+这些步骤可能以不同的顺序发生。例如可以首先进行写入,然后进行 SELECT 查询,最后根据查询结果决定是放弃还是提交。
 
-在医生值班的例子中,在步骤3中修改的行,是步骤1中返回的行之一,所以我们可以通过锁定步骤1 中的行(`SELECT FOR UPDATE`)来使事务安全并避免写入偏差。但是其他四个例子是不同的:它们检查是否**不存在**某些满足条件的行,写入会**添加**一个匹配相同条件的行。如果步骤1中的查询没有返回任何行,则`SELECT FOR UPDATE`锁不了任何东西。
+在医生值班的例子中,在步骤 3 中修改的行,是步骤 1 中返回的行之一,所以我们可以通过锁定步骤 1 中的行(`SELECT FOR UPDATE`)来使事务安全并避免写入偏差。但是其他四个例子是不同的:它们检查是否 **不存在** 某些满足条件的行,写入会 **添加** 一个匹配相同条件的行。如果步骤 1 中的查询没有返回任何行,则 `SELECT FOR UPDATE` 锁不了任何东西。
 
-这种效应:一个事务中的写入改变另一个事务的搜索查询的结果,被称为**幻读**【3】。快照隔离避免了只读查询中幻读,但是在像我们讨论的例子那样的读写事务中,幻读会导致特别棘手的写入偏差情况。
+这种效应:一个事务中的写入改变另一个事务的搜索查询的结果,被称为 **幻读**【3】。快照隔离避免了只读查询中幻读,但是在像我们讨论的例子那样的读写事务中,幻读会导致特别棘手的写入偏差情况。
 
 #### 物化冲突
 
 如果幻读的问题是没有对象可以加锁,也许可以人为地在数据库中引入一个锁对象?
 
-例如,在会议室预订的场景中,可以想象创建一个关于时间槽和房间的表。此表中的每一行对应于特定时间段(例如15分钟)的特定房间。可以提前插入房间和时间的所有可能组合行(例如接下来的六个月)。
+例如,在会议室预订的场景中,可以想象创建一个关于时间槽和房间的表。此表中的每一行对应于特定时间段(例如 15 分钟)的特定房间。可以提前插入房间和时间的所有可能组合行(例如接下来的六个月)。
 
-现在,要创建预订的事务可以锁定(`SELECT FOR UPDATE`)表中与所需房间和时间段对应的行。在获得锁定之后,它可以检查重叠的预订并像以前一样插入新的预订。请注意,这个表并不是用来存储预订相关的信息——它完全就是一组锁,用于防止同时修改同一房间和时间范围内的预订。
+现在,要创建预订的事务可以锁定(`SELECT FOR UPDATE`)表中与所需房间和时间段对应的行。在获得锁定之后,它可以检查重叠的预订并像以前一样插入新的预订。请注意,这个表并不是用来存储预订相关的信息 —— 它完全就是一组锁,用于防止同时修改同一房间和时间范围内的预订。
 
-这种方法被称为**物化冲突(materializing conflicts)**,因为它将幻读变为数据库中一组具体行上的锁冲突【11】。不幸的是,弄清楚如何物化冲突可能很难,也很容易出错,而让并发控制机制泄漏到应用数据模型是很丑陋的做法。出于这些原因,如果没有其他办法可以实现,物化冲突应被视为最后的手段。在大多数情况下。**可串行化(Serializable)** 的隔离级别是更可取的。
+这种方法被称为 **物化冲突(materializing conflicts)**,因为它将幻读变为数据库中一组具体行上的锁冲突【11】。不幸的是,弄清楚如何物化冲突可能很难,也很容易出错,而让并发控制机制泄漏到应用数据模型是很丑陋的做法。出于这些原因,如果没有其他办法可以实现,物化冲突应被视为最后的手段。在大多数情况下。**可串行化(Serializable)** 的隔离级别是更可取的。
 
 
 ## 可串行化
 
-在本章中,已经看到了几个易于出现竞争条件的事务例子。**读已提交**和**快照隔离**级别会阻止某些竞争条件,但不会阻止另一些。我们遇到了一些特别棘手的例子,**写入偏差**和**幻读**。这是一个可悲的情况:
+在本章中,已经看到了几个易于出现竞争条件的事务例子。**读已提交** 和 **快照隔离** 级别会阻止某些竞争条件,但不会阻止另一些。我们遇到了一些特别棘手的例子,**写入偏差** 和 **幻读**。这是一个可悲的情况:
 
-- 隔离级别难以理解,并且在不同的数据库中实现的不一致(例如,“可重复读”的含义天差地别)。
+- 隔离级别难以理解,并且在不同的数据库中实现的不一致(例如,“可重复读” 的含义天差地别)。
 - 光检查应用代码很难判断在特定的隔离级别运行是否安全。 特别是在大型应用程序中,你可能并不知道并发发生的所有事情。
 - 没有检测竞争条件的好工具。原则上来说,静态分析可能会有帮助【26】,但研究中的技术还没法实际应用。并发问题的测试是很难的,因为它们通常是非确定性的 —— 只有在倒霉的时序下才会出现问题。
 
-这不是一个新问题,从20世纪70年代以来就一直是这样了,当时首先引入了较弱的隔离级别【2】。一直以来,研究人员的答案都很简单:使用**可串行化(serializable)** 的隔离级别!
+这不是一个新问题,从 20 世纪 70 年代以来就一直是这样了,当时首先引入了较弱的隔离级别【2】。一直以来,研究人员的答案都很简单:使用 **可串行化(serializable)** 的隔离级别!
 
-**可串行化(Serializability)** 隔离通常被认为是最强的隔离级别。它保证即使事务可以并行执行,最终的结果也是一样的,就好像它们没有任何并发性,连续挨个执行一样。因此数据库保证,如果事务在单独运行时正常运行,则它们在并发运行时继续保持正确 —— 换句话说,数据库可以防止**所有**可能的竞争条件。
+**可串行化(Serializability)** 隔离通常被认为是最强的隔离级别。它保证即使事务可以并行执行,最终的结果也是一样的,就好像它们没有任何并发性,连续挨个执行一样。因此数据库保证,如果事务在单独运行时正常运行,则它们在并发运行时继续保持正确 —— 换句话说,数据库可以防止 **所有** 可能的竞争条件。
 
 但如果可串行化隔离级别比弱隔离级别的烂摊子要好得多,那为什么没有人见人爱?为了回答这个问题,我们需要看看实现可串行化的选项,以及它们如何执行。目前大多数提供可串行化的数据库都使用了三种技术之一,本章的剩余部分将会介绍这些技术:
 
-- 字面意义上地串行顺序执行事务(请参阅“[真的串行执行](#真的串行执行)”)
-- **两阶段锁定(2PL, two-phase locking)**,几十年来唯一可行的选择(请参阅“[两阶段锁定](#两阶段锁定)”)
-- 乐观并发控制技术,例如**可串行化快照隔离**(serializable snapshot isolation,请参阅“[可串行化快照隔离](#可串行化快照隔离)”)
+- 字面意义上地串行顺序执行事务(请参阅 “[真的串行执行](#真的串行执行)”)
+- **两阶段锁定(2PL, two-phase locking)**,几十年来唯一可行的选择(请参阅 “[两阶段锁定](#两阶段锁定)”)
+- 乐观并发控制技术,例如 **可串行化快照隔离**(serializable snapshot isolation,请参阅 “[可串行化快照隔离](#可串行化快照隔离)”)
 
-现在将主要在单节点数据库的背景下讨论这些技术;在[第九章](ch9.md)中,我们将研究如何将它们推广到涉及分布式系统中多个节点的事务。
+现在将主要在单节点数据库的背景下讨论这些技术;在 [第九章](ch9.md) 中,我们将研究如何将它们推广到涉及分布式系统中多个节点的事务。
 
 ### 真的串行执行
 
-避免并发问题的最简单方法就是完全不要并发:在单个线程上按顺序一次只执行一个事务。这样做就完全绕开了检测/防止事务间冲突的问题,由此产生的隔离,正是可串行化的定义。
+避免并发问题的最简单方法就是完全不要并发:在单个线程上按顺序一次只执行一个事务。这样做就完全绕开了检测 / 防止事务间冲突的问题,由此产生的隔离,正是可串行化的定义。
 
-尽管这似乎是一个明显的主意,但数据库设计人员只是在2007年左右才决定,单线程循环执行事务是可行的【45】。如果多线程并发在过去的30年中被认为是获得良好性能的关键所在,那么究竟是什么改变致使单线程执行变为可能呢?
+尽管这似乎是一个明显的主意,但数据库设计人员只是在 2007 年左右才决定,单线程循环执行事务是可行的【45】。如果多线程并发在过去的 30 年中被认为是获得良好性能的关键所在,那么究竟是什么改变致使单线程执行变为可能呢?
 
 两个进展引发了这个反思:
 
-- RAM足够便宜了,许多场景现在都可以将完整的活跃数据集保存在内存中。(请参阅“[在内存中存储一切](ch3.md#在内存中存储一切)”)。当事务需要访问的所有数据都在内存中时,事务处理的执行速度要比等待数据从磁盘加载时快得多。
-- 数据库设计人员意识到OLTP事务通常很短,而且只进行少量的读写操作(请参阅“[事务处理还是分析?](ch3.md#事务处理还是分析?)”)。相比之下,长时间运行的分析查询通常是只读的,因此它们可以在串行执行循环之外的一致快照(使用快照隔离)上运行。
+- RAM 足够便宜了,许多场景现在都可以将完整的活跃数据集保存在内存中。(请参阅 “[在内存中存储一切](ch3.md#在内存中存储一切)”)。当事务需要访问的所有数据都在内存中时,事务处理的执行速度要比等待数据从磁盘加载时快得多。
+- 数据库设计人员意识到 OLTP 事务通常很短,而且只进行少量的读写操作(请参阅 “[事务处理还是分析?](ch3.md#事务处理还是分析?)”)。相比之下,长时间运行的分析查询通常是只读的,因此它们可以在串行执行循环之外的一致快照(使用快照隔离)上运行。
 
-串行执行事务的方法在VoltDB/H-Store,Redis和Datomic中实现【46,47,48】。设计用于单线程执行的系统有时可以比支持并发的系统更好,因为它可以避免锁的协调开销。但是其吞吐量仅限于单个CPU核的吞吐量。为了充分利用单一线程,需要与传统形式的事务不同的结构。
+串行执行事务的方法在 VoltDB/H-Store,Redis 和 Datomic 中实现【46,47,48】。设计用于单线程执行的系统有时可以比支持并发的系统更好,因为它可以避免锁的协调开销。但是其吞吐量仅限于单个 CPU 核的吞吐量。为了充分利用单一线程,需要与传统形式的事务不同的结构。
 
 #### 在存储过程中封装事务
 
 在数据库的早期阶段,意图是数据库事务可以包含整个用户活动流程。例如,预订机票是一个多阶段的过程(搜索路线,票价和可用座位,决定行程,在每段行程的航班上订座,输入乘客信息,付款)。数据库设计者认为,如果整个过程是一个事务,那么它就可以被原子化地执行。
 
-不幸的是,人类做出决定和回应的速度非常缓慢。如果数据库事务需要等待来自用户的输入,则数据库需要支持潜在的大量并发事务,其中大部分是空闲的。大多数数据库不能高效完成这项工作,因此几乎所有的OLTP应用程序都避免在事务中等待交互式的用户输入,以此来保持事务的简短。在Web上,这意味着事务在同一个HTTP请求中被提交——一个事务不会跨越多个请求。一个新的HTTP请求开始一个新的事务。
+不幸的是,人类做出决定和回应的速度非常缓慢。如果数据库事务需要等待来自用户的输入,则数据库需要支持潜在的大量并发事务,其中大部分是空闲的。大多数数据库不能高效完成这项工作,因此几乎所有的 OLTP 应用程序都避免在事务中等待交互式的用户输入,以此来保持事务的简短。在 Web 上,这意味着事务在同一个 HTTP 请求中被提交 —— 一个事务不会跨越多个请求。一个新的 HTTP 请求开始一个新的事务。
 
-即使已经将人类从关键路径中排除,事务仍然以交互式的客户端/服务器风格执行,一次一个语句。应用程序进行查询,读取结果,可能根据第一个查询的结果进行另一个查询,依此类推。查询和结果在应用程序代码(在一台机器上运行)和数据库服务器(在另一台机器上)之间来回发送。
+即使已经将人类从关键路径中排除,事务仍然以交互式的客户端 / 服务器风格执行,一次一个语句。应用程序进行查询,读取结果,可能根据第一个查询的结果进行另一个查询,依此类推。查询和结果在应用程序代码(在一台机器上运行)和数据库服务器(在另一台机器上)之间来回发送。
 
 在这种交互式的事务方式中,应用程序和数据库之间的网络通信耗费了大量的时间。如果不允许在数据库中进行并发处理,且一次只处理一个事务,则吞吐量将会非常糟糕,因为数据库大部分的时间都花费在等待应用程序发出当前事务的下一个查询。在这种数据库中,为了获得合理的性能,需要同时处理多个事务。
 
-出于这个原因,具有单线程串行事务处理的系统不允许交互式的多语句事务。取而代之,应用程序必须提前将整个事务代码作为存储过程提交给数据库。这些方法之间的差异如[图7-9](img/fig7-9.png) 所示。如果事务所需的所有数据都在内存中,则存储过程可以非常快地执行,而不用等待任何网络或磁盘I/O。
+出于这个原因,具有单线程串行事务处理的系统不允许交互式的多语句事务。取而代之,应用程序必须提前将整个事务代码作为存储过程提交给数据库。这些方法之间的差异如 [图 7-9](img/fig7-9.png) 所示。如果事务所需的所有数据都在内存中,则存储过程可以非常快地执行,而不用等待任何网络或磁盘 I/O。
 
 ![](img/fig7-9.png)
 
-**图7-9 交互式事务和存储过程之间的区别(使用图7-8的示例事务)**
+**图 7-9 交互式事务和存储过程之间的区别(使用图 7-8 的示例事务)**
 
 #### 存储过程的优点和缺点
 
-存储过程在关系型数据库中已经存在了一段时间了,自1999年以来它们一直是SQL标准(SQL/PSM)的一部分。出于各种原因,它们的名声有点不太好:
+存储过程在关系型数据库中已经存在了一段时间了,自 1999 年以来它们一直是 SQL 标准(SQL/PSM)的一部分。出于各种原因,它们的名声有点不太好:
 
-- 每个数据库厂商都有自己的存储过程语言(Oracle有PL/SQL,SQL Server有T-SQL,PostgreSQL有PL/pgSQL等)。这些语言并没有跟上通用编程语言的发展,所以从今天的角度来看,它们看起来相当丑陋和陈旧,而且缺乏大多数编程语言中能找到的库的生态系统。
+- 每个数据库厂商都有自己的存储过程语言(Oracle 有 PL/SQL,SQL Server 有 T-SQL,PostgreSQL 有 PL/pgSQL 等)。这些语言并没有跟上通用编程语言的发展,所以从今天的角度来看,它们看起来相当丑陋和陈旧,而且缺乏大多数编程语言中能找到的库的生态系统。
 - 在数据库中运行的代码难以管理:与应用服务器相比,它更难调试,更难以保持版本控制和部署,更难测试,并且难以集成到指标收集系统来进行监控。
-- 数据库通常比应用服务器对性能敏感的多,因为单个数据库实例通常由许多应用服务器共享。数据库中一个写得不好的存储过程(例如,占用大量内存或CPU时间)会比在应用服务器中相同的代码造成更多的麻烦。
+- 数据库通常比应用服务器对性能敏感的多,因为单个数据库实例通常由许多应用服务器共享。数据库中一个写得不好的存储过程(例如,占用大量内存或 CPU 时间)会比在应用服务器中相同的代码造成更多的麻烦。
 
-但是这些问题都是可以克服的。现代的存储过程实现放弃了PL/SQL,而是使用现有的通用编程语言:VoltDB使用Java或Groovy,Datomic使用Java或Clojure,而Redis使用Lua。
+但是这些问题都是可以克服的。现代的存储过程实现放弃了 PL/SQL,而是使用现有的通用编程语言:VoltDB 使用 Java 或 Groovy,Datomic 使用 Java 或 Clojure,而 Redis 使用 Lua。
 
-**存储过程与内存存储**,使得在单个线程上执行所有事务变得可行。由于不需要等待I/O,且避免了并发控制机制的开销,它们可以在单个线程上实现相当好的吞吐量。
+**存储过程与内存存储**,使得在单个线程上执行所有事务变得可行。由于不需要等待 I/O,且避免了并发控制机制的开销,它们可以在单个线程上实现相当好的吞吐量。
 
-VoltDB还使用存储过程进行复制:但不是将事务的写入结果从一个节点复制到另一个节点,而是在每个节点上执行相同的存储过程。因此VoltDB要求存储过程是**确定性的**(在不同的节点上运行时,它们必须产生相同的结果)。举个例子,如果事务需要使用当前的日期和时间,则必须通过特殊的确定性API来实现。
+VoltDB 还使用存储过程进行复制:但不是将事务的写入结果从一个节点复制到另一个节点,而是在每个节点上执行相同的存储过程。因此 VoltDB 要求存储过程是 **确定性的**(在不同的节点上运行时,它们必须产生相同的结果)。举个例子,如果事务需要使用当前的日期和时间,则必须通过特殊的确定性 API 来实现。
 
 #### 分区
 
 顺序执行所有事务使并发控制简单多了,但数据库的事务吞吐量被限制为单机单核的速度。只读事务可以使用快照隔离在其它地方执行,但对于写入吞吐量较高的应用,单线程事务处理器可能成为一个严重的瓶颈。
 
-为了伸缩至多个CPU核心和多个节点,可以对数据进行分区(请参阅[第六章](ch6.md)),在VoltDB中支持这样做。如果你可以找到一种对数据集进行分区的方法,以便每个事务只需要在单个分区中读写数据,那么每个分区就可以拥有自己独立运行的事务处理线程。在这种情况下可以为每个分区指派一个独立的CPU核,事务吞吐量就可以与CPU核数保持线性伸缩【47】。
+为了伸缩至多个 CPU 核心和多个节点,可以对数据进行分区(请参阅 [第六章](ch6.md)),在 VoltDB 中支持这样做。如果你可以找到一种对数据集进行分区的方法,以便每个事务只需要在单个分区中读写数据,那么每个分区就可以拥有自己独立运行的事务处理线程。在这种情况下可以为每个分区指派一个独立的 CPU 核,事务吞吐量就可以与 CPU 核数保持线性伸缩【47】。
 
 但是,对于需要访问多个分区的任何事务,数据库必须在触及的所有分区之间协调事务。存储过程需要跨越所有分区锁定执行,以确保整个系统的可串行性。
 
-由于跨分区事务具有额外的协调开销,所以它们比单分区事务慢得多。 VoltDB报告的吞吐量大约是每秒1000个跨分区写入,比单分区吞吐量低几个数量级,并且不能通过增加更多的机器来增加【49】。
+由于跨分区事务具有额外的协调开销,所以它们比单分区事务慢得多。 VoltDB 报告的吞吐量大约是每秒 1000 个跨分区写入,比单分区吞吐量低几个数量级,并且不能通过增加更多的机器来增加【49】。
 
-事务是否可以是划分至单个分区很大程度上取决于应用数据的结构。简单的键值数据通常可以非常容易地进行分区,但是具有多个次级索引的数据可能需要大量的跨分区协调(请参阅“[分区与次级索引](ch6.md#分区与次级索引)”)。
+事务是否可以是划分至单个分区很大程度上取决于应用数据的结构。简单的键值数据通常可以非常容易地进行分区,但是具有多个次级索引的数据可能需要大量的跨分区协调(请参阅 “[分区与次级索引](ch6.md#分区与次级索引)”)。
 
 #### 串行执行小结
 
 在特定约束条件下,真的串行执行事务,已经成为一种实现可串行化隔离等级的可行办法。
 
 - 每个事务都必须小而快,只要有一个缓慢的事务,就会拖慢所有事务处理。
-- 仅限于活跃数据集可以放入内存的情况。很少访问的数据可能会被移动到磁盘,但如果需要在单线程执行的事务中访问,系统就会变得非常慢[^x]。
-- 写入吞吐量必须低到能在单个CPU核上处理,如若不然,事务需要能划分至单个分区,且不需要跨分区协调。
+- 仅限于活跃数据集可以放入内存的情况。很少访问的数据可能会被移动到磁盘,但如果需要在单线程执行的事务中访问,系统就会变得非常慢 [^x]。
+- 写入吞吐量必须低到能在单个 CPU 核上处理,如若不然,事务需要能划分至单个分区,且不需要跨分区协调。
 - 跨分区事务是可能的,但是它们能被使用的程度有很大的限制。
 
-[^x]: 如果事务需要访问不在内存中的数据,最好的解决方案可能是中止事务,异步地将数据提取到内存中,同时继续处理其他事务,然后在数据加载完毕时重新启动事务。这种方法被称为**反缓存(anti-caching)**,正如前面在“[在内存中存储一切](ch3.md#在内存中存储一切)”中所述。
+[^x]: 如果事务需要访问不在内存中的数据,最好的解决方案可能是中止事务,异步地将数据提取到内存中,同时继续处理其他事务,然后在数据加载完毕时重新启动事务。这种方法被称为 **反缓存(anti-caching)**,正如前面在 “[在内存中存储一切](ch3.md#在内存中存储一切)” 中所述。
 
 ### 两阶段锁定
 
-大约30年来,在数据库中只有一种广泛使用的串行化算法:**两阶段锁定(2PL,two-phase locking)** [^xi]
+大约 30 年来,在数据库中只有一种广泛使用的串行化算法:**两阶段锁定(2PL,two-phase locking)** [^xi]
 
-[^xi]: 有时也称为**严格两阶段锁定(SS2PL, strong strict two-phase locking)**,以便和其他2PL变体区分。
+[^xi]: 有时也称为 **严格两阶段锁定(SS2PL, strong strict two-phase locking)**,以便和其他 2PL 变体区分。
 
 > #### 2PL不是2PC
 >
-> 请注意,虽然两阶段锁定(2PL)听起来非常类似于两阶段提交(2PC),但它们是完全不同的东西。我们将在[第九章](ch9.md)讨论2PC。
+> 请注意,虽然两阶段锁定(2PL)听起来非常类似于两阶段提交(2PC),但它们是完全不同的东西。我们将在 [第九章](ch9.md) 讨论 2PC。
 
-之前我们看到锁通常用于防止脏写(请参阅“[没有脏写](#没有脏写)”一节):如果两个事务同时尝试写入同一个对象,则锁可确保第二个写入必须等到第一个写入完成事务(中止或提交),然后才能继续。
+之前我们看到锁通常用于防止脏写(请参阅 “[没有脏写](#没有脏写)” 一节):如果两个事务同时尝试写入同一个对象,则锁可确保第二个写入必须等到第一个写入完成事务(中止或提交),然后才能继续。
 
-两阶段锁定类似,但是锁的要求更强得多。只要没有写入,就允许多个事务同时读取同一个对象。但对象只要有写入(修改或删除),就需要**独占访问(exclusive access)** 权限:
+两阶段锁定类似,但是锁的要求更强得多。只要没有写入,就允许多个事务同时读取同一个对象。但对象只要有写入(修改或删除),就需要 **独占访问(exclusive access)** 权限:
 
-- 如果事务A读取了一个对象,并且事务B想要写入该对象,那么B必须等到A提交或中止才能继续(这确保B不能在A底下意外地改变对象)。
-- 如果事务A写入了一个对象,并且事务B想要读取该对象,则B必须等到A提交或中止才能继续(像[图7-1](img/fig7-1.png)那样读取旧版本的对象在2PL下是不可接受的)。
+- 如果事务 A 读取了一个对象,并且事务 B 想要写入该对象,那么 B 必须等到 A 提交或中止才能继续(这确保 B 不能在 A 底下意外地改变对象)。
+- 如果事务 A 写入了一个对象,并且事务 B 想要读取该对象,则 B 必须等到 A 提交或中止才能继续(像 [图 7-1](img/fig7-1.png) 那样读取旧版本的对象在 2PL 下是不可接受的)。
 
-在2PL中,写入不仅会阻塞其他写入,也会阻塞读,反之亦然。快照隔离使得**读不阻塞写,写也不阻塞读**(请参阅“[实现快照隔离](#实现快照隔离)”),这是2PL和快照隔离之间的关键区别。另一方面,因为2PL提供了可串行化的性质,它可以防止早先讨论的所有竞争条件,包括丢失更新和写入偏差。
+在 2PL 中,写入不仅会阻塞其他写入,也会阻塞读,反之亦然。快照隔离使得 **读不阻塞写,写也不阻塞读**(请参阅 “[实现快照隔离](#实现快照隔离)”),这是 2PL 和快照隔离之间的关键区别。另一方面,因为 2PL 提供了可串行化的性质,它可以防止早先讨论的所有竞争条件,包括丢失更新和写入偏差。
 
 #### 实现两阶段锁
 
-2PL用于MySQL(InnoDB)和SQL Server中的可串行化隔离级别,以及DB2中的可重复读隔离级别【23,36】。
+2PL 用于 MySQL(InnoDB)和 SQL Server 中的可串行化隔离级别,以及 DB2 中的可重复读隔离级别【23,36】。
 
-读与写的阻塞是通过为数据库中每个对象添加锁来实现的。锁可以处于**共享模式(shared mode)** 或**独占模式(exclusive mode)**。锁使用如下:
+读与写的阻塞是通过为数据库中每个对象添加锁来实现的。锁可以处于 **共享模式(shared mode)** 或 **独占模式(exclusive mode)**。锁使用如下:
 
 - 若事务要读取对象,则须先以共享模式获取锁。允许多个事务同时持有共享锁。但如果另一个事务已经在对象上持有排它锁,则这些事务必须等待。
 - 若事务要写入一个对象,它必须首先以独占模式获取该锁。没有其他事务可以同时持有锁(无论是共享模式还是独占模式),所以如果对象上存在任何锁,该事务必须等待。
 - 如果事务先读取再写入对象,则它可能会将其共享锁升级为独占锁。升级锁的工作与直接获得排他锁相同。
-- 事务获得锁之后,必须继续持有锁直到事务结束(提交或中止)。这就是“两阶段”这个名字的来源:第一阶段(当事务正在执行时)获取锁,第二阶段(在事务结束时)释放所有的锁。
+- 事务获得锁之后,必须继续持有锁直到事务结束(提交或中止)。这就是 “两阶段” 这个名字的来源:第一阶段(当事务正在执行时)获取锁,第二阶段(在事务结束时)释放所有的锁。
 
-由于使用了这么多的锁,因此很可能会发生:事务A等待事务B释放它的锁,反之亦然。这种情况叫做**死锁(Deadlock)**。数据库会自动检测事务之间的死锁,并中止其中一个,以便另一个继续执行。被中止的事务需要由应用程序重试。
+由于使用了这么多的锁,因此很可能会发生:事务 A 等待事务 B 释放它的锁,反之亦然。这种情况叫做 **死锁(Deadlock)**。数据库会自动检测事务之间的死锁,并中止其中一个,以便另一个继续执行。被中止的事务需要由应用程序重试。
 
 #### 两阶段锁定的性能
 
-两阶段锁定的巨大缺点,以及70年代以来没有被所有人使用的原因,是其性能问题。两阶段锁定下的事务吞吐量与查询响应时间要比弱隔离级别下要差得多。
+两阶段锁定的巨大缺点,以及 70 年代以来没有被所有人使用的原因,是其性能问题。两阶段锁定下的事务吞吐量与查询响应时间要比弱隔离级别下要差得多。
 
 这一部分是由于获取和释放所有这些锁的开销,但更重要的是由于并发性的降低。按照设计,如果两个并发事务试图做任何可能导致竞争条件的事情,那么必须等待另一个完成。
 
 传统的关系数据库不限制事务的持续时间,因为它们是为等待人类输入的交互式应用而设计的。因此,当一个事务需要等待另一个事务时,等待的时长并没有限制。即使你保证所有的事务都很短,如果有多个事务想要访问同一个对象,那么可能会形成一个队列,所以事务可能需要等待几个其他事务才能完成。
 
-因此,运行2PL的数据库可能具有相当不稳定的延迟,如果在工作负载中存在争用,那么可能高百分位点处的响应会非常的慢(请参阅“[描述性能](ch1.md#描述性能)”)。可能只需要一个缓慢的事务,或者一个访问大量数据并获取许多锁的事务,就能把系统的其他部分拖慢,甚至迫使系统停机。当需要稳健的操作时,这种不稳定性是有问题的。
+因此,运行 2PL 的数据库可能具有相当不稳定的延迟,如果在工作负载中存在争用,那么可能高百分位点处的响应会非常的慢(请参阅 “[描述性能](ch1.md#描述性能)”)。可能只需要一个缓慢的事务,或者一个访问大量数据并获取许多锁的事务,就能把系统的其他部分拖慢,甚至迫使系统停机。当需要稳健的操作时,这种不稳定性是有问题的。
 
-基于锁实现的读已提交隔离级别可能发生死锁,但在基于2PL实现的可串行化隔离级别中,它们会出现的频繁的多(取决于事务的访问模式)。这可能是一个额外的性能问题:当事务由于死锁而被中止并被重试时,它需要从头重做它的工作。如果死锁很频繁,这可能意味着巨大的浪费。
+基于锁实现的读已提交隔离级别可能发生死锁,但在基于 2PL 实现的可串行化隔离级别中,它们会出现的频繁的多(取决于事务的访问模式)。这可能是一个额外的性能问题:当事务由于死锁而被中止并被重试时,它需要从头重做它的工作。如果死锁很频繁,这可能意味着巨大的浪费。
 
 #### 谓词锁
 
-在前面关于锁的描述中,我们掩盖了一个微妙而重要的细节。在“[导致写入偏差的幻读](#导致写入偏差的幻读)”中,我们讨论了**幻读(phantoms)** 的问题。即一个事务改变另一个事务的搜索查询的结果。具有可串行化隔离级别的数据库必须防止**幻读**。
+在前面关于锁的描述中,我们掩盖了一个微妙而重要的细节。在 “[导致写入偏差的幻读](#导致写入偏差的幻读)” 中,我们讨论了 **幻读(phantoms)** 的问题。即一个事务改变另一个事务的搜索查询的结果。具有可串行化隔离级别的数据库必须防止 **幻读**。
 
-在会议室预订的例子中,这意味着如果一个事务在某个时间窗口内搜索了一个房间的现有预订(见[例7-2]()),则另一个事务不能同时插入或更新同一时间窗口与同一房间的另一个预订 (可以同时插入其他房间的预订,或在不影响另一个预定的条件下预定同一房间的其他时间段)。
+在会议室预订的例子中,这意味着如果一个事务在某个时间窗口内搜索了一个房间的现有预订(见 [例 7-2]()),则另一个事务不能同时插入或更新同一时间窗口与同一房间的另一个预订 (可以同时插入其他房间的预订,或在不影响另一个预定的条件下预定同一房间的其他时间段)。
 
-如何实现这一点?从概念上讲,我们需要一个**谓词锁(predicate lock)**【3】。它类似于前面描述的共享/排它锁,但不属于特定的对象(例如,表中的一行),它属于所有符合某些搜索条件的对象,如:
+如何实现这一点?从概念上讲,我们需要一个 **谓词锁(predicate lock)**【3】。它类似于前面描述的共享 / 排它锁,但不属于特定的对象(例如,表中的一行),它属于所有符合某些搜索条件的对象,如:
 
 ```sql
 SELECT * FROM bookings
 WHERE room_id = 123 AND
-      end_time > '2018-01-01 12:00' AND 
+      end_time > '2018-01-01 12:00' AND
       start_time < '2018-01-01 13:00';
 ```
 
 谓词锁限制访问,如下所示:
 
-- 如果事务A想要读取匹配某些条件的对象,就像在这个 `SELECT` 查询中那样,它必须获取查询条件上的**共享谓词锁(shared-mode predicate lock)**。如果另一个事务B持有任何满足这一查询条件对象的排它锁,那么A必须等到B释放它的锁之后才允许进行查询。
-- 如果事务A想要插入,更新或删除任何对象,则必须首先检查旧值或新值是否与任何现有的谓词锁匹配。如果事务B持有匹配的谓词锁,那么A必须等到B已经提交或中止后才能继续。
+- 如果事务 A 想要读取匹配某些条件的对象,就像在这个 `SELECT` 查询中那样,它必须获取查询条件上的 **共享谓词锁(shared-mode predicate lock)**。如果另一个事务 B 持有任何满足这一查询条件对象的排它锁,那么 A 必须等到 B 释放它的锁之后才允许进行查询。
+- 如果事务 A 想要插入,更新或删除任何对象,则必须首先检查旧值或新值是否与任何现有的谓词锁匹配。如果事务 B 持有匹配的谓词锁,那么 A 必须等到 B 已经提交或中止后才能继续。
 
 这里的关键思想是,谓词锁甚至适用于数据库中尚不存在,但将来可能会添加的对象(幻象)。如果两阶段锁定包含谓词锁,则数据库将阻止所有形式的写入偏差和其他竞争条件,因此其隔离实现了可串行化。
 
 #### 索引范围锁
 
-不幸的是谓词锁性能不佳:**如果活跃事务持有很多锁,检查匹配的锁会非常耗时。**因此,大多数使用2PL的数据库实际上实现了索引范围锁(index-range locking,也称为**next-key locking**),这是一个简化的近似版谓词锁【41,50】。
+不幸的是谓词锁性能不佳:**如果活跃事务持有很多锁,检查匹配的锁会非常耗时。** 因此,大多数使用 2PL 的数据库实际上实现了索引范围锁(index-range locking,也称为 **next-key locking**),这是一个简化的近似版谓词锁【41,50】。
 
-通过使谓词匹配到一个更大的集合来简化谓词锁是安全的。例如,如果你有在中午和下午1点之间预订123号房间的谓词锁,则锁定123号房间的所有时间段,或者锁定12:00~13:00时间段的所有房间(不只是123号房间)是一个安全的近似,因为任何满足原始谓词的写入也一定会满足这种更松散的近似。
+通过使谓词匹配到一个更大的集合来简化谓词锁是安全的。例如,如果你有在中午和下午 1 点之间预订 123 号房间的谓词锁,则锁定 123 号房间的所有时间段,或者锁定 12:00~13:00 时间段的所有房间(不只是 123 号房间)是一个安全的近似,因为任何满足原始谓词的写入也一定会满足这种更松散的近似。
 
-在房间预订数据库中,你可能会在`room_id`列上有一个索引,并且/或者在`start_time` 和 `end_time`上有索引(否则前面的查询在大型数据库上的速度会非常慢):
+在房间预订数据库中,你可能会在 `room_id` 列上有一个索引,并且 / 或者在 `start_time` 和 `end_time` 上有索引(否则前面的查询在大型数据库上的速度会非常慢):
 
-- 假设你的索引位于`room_id`上,并且数据库使用此索引查找123号房间的现有预订。现在数据库可以简单地将共享锁附加到这个索引项上,指示事务已搜索123号房间用于预订。
-- 或者,如果数据库使用基于时间的索引来查找现有预订,那么它可以将共享锁附加到该索引中的一系列值,指示事务已经将12:00~13:00时间段标记为用于预定。
+- 假设你的索引位于 `room_id` 上,并且数据库使用此索引查找 123 号房间的现有预订。现在数据库可以简单地将共享锁附加到这个索引项上,指示事务已搜索 123 号房间用于预订。
+- 或者,如果数据库使用基于时间的索引来查找现有预订,那么它可以将共享锁附加到该索引中的一系列值,指示事务已经将 12:00~13:00 时间段标记为用于预定。
 
-无论哪种方式,搜索条件的近似值都附加到其中一个索引上。现在,如果另一个事务想要插入,更新或删除同一个房间和/或重叠时间段的预订,则它将不得不更新索引的相同部分。在这样做的过程中,它会遇到共享锁,它将被迫等到锁被释放。
+无论哪种方式,搜索条件的近似值都附加到其中一个索引上。现在,如果另一个事务想要插入,更新或删除同一个房间和 / 或重叠时间段的预订,则它将不得不更新索引的相同部分。在这样做的过程中,它会遇到共享锁,它将被迫等到锁被释放。
 
 这种方法能够有效防止幻读和写入偏差。索引范围锁并不像谓词锁那样精确(它们可能会锁定更大范围的对象,而不是维持可串行化所必需的范围),但是由于它们的开销较低,所以是一个很好的折衷。
 
@@ -751,76 +751,76 @@ WHERE room_id = 123 AND
 
 本章描绘了数据库中并发控制的黯淡画面。一方面,我们实现了性能不好(2PL)或者伸缩性不好(串行执行)的可串行化隔离级别。另一方面,我们有性能良好的弱隔离级别,但容易出现各种竞争条件(丢失更新,写入偏差,幻读等)。串行化的隔离级别和高性能是从根本上相互矛盾的吗?
 
-也许不是:一个称为**可串行化快照隔离(SSI, serializable snapshot isolation)** 的算法是非常有前途的。它提供了完整的可串行化隔离级别,但与快照隔离相比只有很小的性能损失。 SSI是相当新的:它在2008年首次被描述【40】,并且是Michael Cahill的博士论文【51】的主题。
+也许不是:一个称为 **可串行化快照隔离(SSI, serializable snapshot isolation)** 的算法是非常有前途的。它提供了完整的可串行化隔离级别,但与快照隔离相比只有很小的性能损失。 SSI 是相当新的:它在 2008 年首次被描述【40】,并且是 Michael Cahill 的博士论文【51】的主题。
 
-今天,SSI既用于单节点数据库(PostgreSQL9.1 以后的可串行化隔离级别)和分布式数据库(FoundationDB使用类似的算法)。由于SSI与其他并发控制机制相比还很年轻,还处于在实践中证明自己表现的阶段。但它有可能因为足够快而在未来成为新的默认选项。
+今天,SSI 既用于单节点数据库(PostgreSQL9.1 以后的可串行化隔离级别)和分布式数据库(FoundationDB 使用类似的算法)。由于 SSI 与其他并发控制机制相比还很年轻,还处于在实践中证明自己表现的阶段。但它有可能因为足够快而在未来成为新的默认选项。
 
 #### 悲观与乐观的并发控制
 
-两阶段锁是一种所谓的**悲观并发控制机制(pessimistic)** :它是基于这样的原则:如果有事情可能出错(如另一个事务所持有的锁所表示的),最好等到情况安全后再做任何事情。这就像互斥,用于保护多线程编程中的数据结构。
+两阶段锁是一种所谓的 **悲观并发控制机制(pessimistic)** :它是基于这样的原则:如果有事情可能出错(如另一个事务所持有的锁所表示的),最好等到情况安全后再做任何事情。这就像互斥,用于保护多线程编程中的数据结构。
 
-从某种意义上说,串行执行可以称为悲观到了极致:在事务持续期间,每个事务对整个数据库(或数据库的一个分区)具有排它锁,作为对悲观的补偿,我们让每笔事务执行得非常快,所以只需要短时间持有“锁”。
+从某种意义上说,串行执行可以称为悲观到了极致:在事务持续期间,每个事务对整个数据库(或数据库的一个分区)具有排它锁,作为对悲观的补偿,我们让每笔事务执行得非常快,所以只需要短时间持有 “锁”。
 
-相比之下,**串行化快照隔离**是一种**乐观(optimistic)** 的并发控制技术。在这种情况下,乐观意味着,如果存在潜在的危险也不阻止事务,而是继续执行事务,希望一切都会好起来。当一个事务想要提交时,数据库检查是否有什么不好的事情发生(即隔离是否被违反);如果是的话,事务将被中止,并且必须重试。只有可串行化的事务才被允许提交。
+相比之下,**串行化快照隔离** 是一种 **乐观(optimistic)** 的并发控制技术。在这种情况下,乐观意味着,如果存在潜在的危险也不阻止事务,而是继续执行事务,希望一切都会好起来。当一个事务想要提交时,数据库检查是否有什么不好的事情发生(即隔离是否被违反);如果是的话,事务将被中止,并且必须重试。只有可串行化的事务才被允许提交。
 
-乐观并发控制是一个古老的想法【52】,其优点和缺点已经争论了很长时间【53】。如果存在很多**争用**(contention,即很多事务试图访问相同的对象),则表现不佳,因为这会导致很大一部分事务需要中止。如果系统已经接近最大吞吐量,来自重试事务的额外负载可能会使性能变差。
+乐观并发控制是一个古老的想法【52】,其优点和缺点已经争论了很长时间【53】。如果存在很多 **争用**(contention,即很多事务试图访问相同的对象),则表现不佳,因为这会导致很大一部分事务需要中止。如果系统已经接近最大吞吐量,来自重试事务的额外负载可能会使性能变差。
 
 但是,如果有足够的备用容量,并且事务之间的争用不是太高,乐观的并发控制技术往往比悲观的要好。可交换的原子操作可以减少争用:例如,如果多个事务同时要增加一个计数器,那么应用增量的顺序(只要计数器不在同一个事务中读取)就无关紧要了,所以并发增量可以全部应用且无需冲突。
 
-顾名思义,SSI基于快照隔离——也就是说,事务中的所有读取都是来自数据库的一致性快照(请参阅“[快照隔离和可重复读取](#快照隔离和可重复读)”)。与早期的乐观并发控制技术相比这是主要的区别。在快照隔离的基础上,SSI添加了一种算法来检测写入之间的串行化冲突,并确定要中止哪些事务。
+顾名思义,SSI 基于快照隔离 —— 也就是说,事务中的所有读取都是来自数据库的一致性快照(请参阅 “[快照隔离和可重复读取](#快照隔离和可重复读)”)。与早期的乐观并发控制技术相比这是主要的区别。在快照隔离的基础上,SSI 添加了一种算法来检测写入之间的串行化冲突,并确定要中止哪些事务。
 
 #### 基于过时前提的决策
 
-先前讨论了快照隔离中的写入偏差(请参阅“[写入偏斜与幻读](#写入偏斜与幻读)”)时,我们观察到一个循环模式:事务从数据库读取一些数据,检查查询的结果,并根据它看到的结果决定采取一些操作(写入数据库)。但是,在快照隔离的情况下,原始查询的结果在事务提交时可能不再是最新的,因为数据可能在同一时间被修改。
+先前讨论了快照隔离中的写入偏差(请参阅 “[写入偏斜与幻读](#写入偏斜与幻读)”)时,我们观察到一个循环模式:事务从数据库读取一些数据,检查查询的结果,并根据它看到的结果决定采取一些操作(写入数据库)。但是,在快照隔离的情况下,原始查询的结果在事务提交时可能不再是最新的,因为数据可能在同一时间被修改。
 
-换句话说,事务基于一个**前提(premise)** 采取行动(事务开始时候的事实,例如:“目前有两名医生正在值班”)。之后当事务要提交时,原始数据可能已经改变——前提可能不再成立。
+换句话说,事务基于一个 **前提(premise)** 采取行动(事务开始时候的事实,例如:“目前有两名医生正在值班”)。之后当事务要提交时,原始数据可能已经改变 —— 前提可能不再成立。
 
 当应用程序进行查询时(例如,“当前有多少医生正在值班?”),数据库不知道应用逻辑如何使用该查询结果。在这种情况下为了安全,数据库需要假设任何对该结果集的变更都可能会使该事务中的写入变得无效。 换而言之,事务中的查询与写入可能存在因果依赖。为了提供可串行化的隔离级别,如果事务在过时的前提下执行操作,数据库必须能检测到这种情况,并中止事务。
 
 数据库如何知道查询结果是否可能已经改变?有两种情况需要考虑:
 
-- 检测对旧MVCC对象版本的读取(读之前存在未提交的写入)
+- 检测对旧 MVCC 对象版本的读取(读之前存在未提交的写入)
 - 检测影响先前读取的写入(读之后发生写入)
 
 #### 检测旧MVCC读取
 
-回想一下,快照隔离通常是通过多版本并发控制(MVCC;见[图7-10](img/fig7-10.png))来实现的。当一个事务从MVCC数据库中的一致快照读时,它将忽略取快照时尚未提交的任何其他事务所做的写入。在[图7-10](img/fig7-10.png)中,事务43 认为Alice的 `on_call = true` ,因为事务42(修改Alice的待命状态)未被提交。然而,在事务43想要提交时,事务42 已经提交。这意味着在读一致性快照时被忽略的写入已经生效,事务43 的前提不再为真。
+回想一下,快照隔离通常是通过多版本并发控制(MVCC;见 [图 7-10](img/fig7-10.png))来实现的。当一个事务从 MVCC 数据库中的一致快照读时,它将忽略取快照时尚未提交的任何其他事务所做的写入。在 [图 7-10](img/fig7-10.png) 中,事务 43 认为 Alice 的 `on_call = true` ,因为事务 42(修改 Alice 的待命状态)未被提交。然而,在事务 43 想要提交时,事务 42 已经提交。这意味着在读一致性快照时被忽略的写入已经生效,事务 43 的前提不再为真。
 
 ![](img/fig7-10.png)
 
-**图7-10 检测事务何时从MVCC快照读取过时的值**
+**图 7-10 检测事务何时从 MVCC 快照读取过时的值**
 
-为了防止这种异常,数据库需要跟踪一个事务由于MVCC可见性规则而忽略另一个事务的写入。当事务想要提交时,数据库检查是否有任何被忽略的写入现在已经被提交。如果是这样,事务必须中止。
+为了防止这种异常,数据库需要跟踪一个事务由于 MVCC 可见性规则而忽略另一个事务的写入。当事务想要提交时,数据库检查是否有任何被忽略的写入现在已经被提交。如果是这样,事务必须中止。
 
-为什么要等到提交?当检测到陈旧的读取时,为什么不立即中止事务43 ?因为如果事务43 是只读事务,则不需要中止,因为没有写入偏差的风险。当事务43 进行读取时,数据库还不知道事务是否要稍后执行写操作。此外,事务42 可能在事务43 被提交的时候中止或者可能仍然未被提交,因此读取可能终究不是陈旧的。通过避免不必要的中止,SSI 保留了快照隔离从一致快照中长时间读取的能力。
+为什么要等到提交?当检测到陈旧的读取时,为什么不立即中止事务 43 ?因为如果事务 43 是只读事务,则不需要中止,因为没有写入偏差的风险。当事务 43 进行读取时,数据库还不知道事务是否要稍后执行写操作。此外,事务 42 可能在事务 43 被提交的时候中止或者可能仍然未被提交,因此读取可能终究不是陈旧的。通过避免不必要的中止,SSI 保留了快照隔离从一致快照中长时间读取的能力。
 
 #### 检测影响之前读取的写入
 
-第二种情况要考虑的是另一个事务在读取数据之后修改数据。这种情况如[图7-11](img/fig7-11.png)所示。
+第二种情况要考虑的是另一个事务在读取数据之后修改数据。这种情况如 [图 7-11](img/fig7-11.png) 所示。
 
 ![](img/fig7-11.png)
 
-**图7-11 在可串行化快照隔离中,检测一个事务何时修改另一个事务的读取。**
+**图 7-11 在可串行化快照隔离中,检测一个事务何时修改另一个事务的读取。**
 
-在两阶段锁定的上下文中,我们讨论了索引范围锁(请参阅“[索引范围锁](#索引范围锁)”),它允许数据库锁定与某个搜索查询匹配的所有行的访问权,例如 `WHERE shift_id = 1234`。可以在这里使用类似的技术,除了SSI锁不会阻塞其他事务。
+在两阶段锁定的上下文中,我们讨论了索引范围锁(请参阅 “[索引范围锁](#索引范围锁)”),它允许数据库锁定与某个搜索查询匹配的所有行的访问权,例如 `WHERE shift_id = 1234`。可以在这里使用类似的技术,除了 SSI 锁不会阻塞其他事务。
 
-在[图7-11](img/fig7-11.png)中,事务42 和43 都在班次1234 查找值班医生。如果在`shift_id`上有索引,则数据库可以使用索引项1234 来记录事务42 和43 读取这个数据的事实。 (如果没有索引,这个信息可以在表级别进行跟踪)。这个信息只需要保留一段时间:在一个事务完成(提交或中止),并且所有的并发事务完成之后,数据库就可以忘记它读取的数据了。
+在 [图 7-11](img/fig7-11.png) 中,事务 42 和 43 都在班次 1234 查找值班医生。如果在 `shift_id` 上有索引,则数据库可以使用索引项 1234 来记录事务 42 和 43 读取这个数据的事实。 (如果没有索引,这个信息可以在表级别进行跟踪)。这个信息只需要保留一段时间:在一个事务完成(提交或中止),并且所有的并发事务完成之后,数据库就可以忘记它读取的数据了。
 
 当事务写入数据库时,它必须在索引中查找最近曾读取受影响数据的其他事务。这个过程类似于在受影响的键范围上获取写锁,但锁并不会阻塞事务指导其他读事务完成,而是像警戒线一样只是简单通知其他事务:你们读过的数据可能不是最新的啦。
 
-在[图7-11](img/fig7-11.png)中,事务43 通知事务42 其先前读已过时,反之亦然。事务42首先提交并成功,尽管事务43 的写影响了42 ,但因为事务43 尚未提交,所以写入尚未生效。然而当事务43 想要提交时,来自事务42 的冲突写入已经被提交,所以事务43 必须中止。
+在 [图 7-11](img/fig7-11.png) 中,事务 43 通知事务 42 其先前读已过时,反之亦然。事务 42 首先提交并成功,尽管事务 43 的写影响了 42 ,但因为事务 43 尚未提交,所以写入尚未生效。然而当事务 43 想要提交时,来自事务 42 的冲突写入已经被提交,所以事务 43 必须中止。
 
 #### 可串行化快照隔离的性能
 
-与往常一样,许多工程细节会影响算法的实际表现。例如一个权衡是跟踪事务的读取和写入的**粒度(granularity)**。如果数据库详细地跟踪每个事务的活动(细粒度),那么可以准确地确定哪些事务需要中止,但是簿记开销可能变得很显著。简略的跟踪速度更快(粗粒度),但可能会导致更多不必要的事务中止。
+与往常一样,许多工程细节会影响算法的实际表现。例如一个权衡是跟踪事务的读取和写入的 **粒度(granularity)**。如果数据库详细地跟踪每个事务的活动(细粒度),那么可以准确地确定哪些事务需要中止,但是簿记开销可能变得很显著。简略的跟踪速度更快(粗粒度),但可能会导致更多不必要的事务中止。
 
-在某些情况下,事务可以读取被另一个事务覆盖的信息:这取决于发生了什么,有时可以证明执行结果无论如何都是可串行化的。 PostgreSQL使用这个理论来减少不必要的中止次数【11,41】。
+在某些情况下,事务可以读取被另一个事务覆盖的信息:这取决于发生了什么,有时可以证明执行结果无论如何都是可串行化的。 PostgreSQL 使用这个理论来减少不必要的中止次数【11,41】。
 
 与两阶段锁定相比,可串行化快照隔离的最大优点是一个事务不需要阻塞等待另一个事务所持有的锁。就像在快照隔离下一样,写不会阻塞读,反之亦然。这种设计原则使得查询延迟更可预测,变量更少。特别是,只读查询可以运行在一致快照上,而不需要任何锁定,这对于读取繁重的工作负载非常有吸引力。
 
-与串行执行相比,可串行化快照隔离并不局限于单个CPU核的吞吐量:FoundationDB将检测到的串行化冲突分布在多台机器上,允许扩展到很高的吞吐量。即使数据可能跨多台机器进行分区,事务也可以在保证可串行化隔离等级的同时读写多个分区中的数据【54】。
+与串行执行相比,可串行化快照隔离并不局限于单个 CPU 核的吞吐量:FoundationDB 将检测到的串行化冲突分布在多台机器上,允许扩展到很高的吞吐量。即使数据可能跨多台机器进行分区,事务也可以在保证可串行化隔离等级的同时读写多个分区中的数据【54】。
 
-中止率显著影响SSI的整体表现。例如,长时间读取和写入数据的事务很可能会发生冲突并中止,因此SSI要求同时读写的事务尽量短(只读的长事务可能没问题)。对于慢事务,SSI可能比两阶段锁定或串行执行更不敏感。
+中止率显著影响 SSI 的整体表现。例如,长时间读取和写入数据的事务很可能会发生冲突并中止,因此 SSI 要求同时读写的事务尽量短(只读的长事务可能没问题)。对于慢事务,SSI 可能比两阶段锁定或串行执行更不敏感。
 
 
 ## 本章小结
@@ -831,11 +831,11 @@ WHERE room_id = 123 AND
 
 如果没有事务处理,各种错误情况(进程崩溃,网络中断,停电,磁盘已满,意外并发等)意味着数据可能以各种方式变得不一致。例如,非规范化的数据可能很容易与源数据不同步。如果没有事务处理,就很难推断复杂的交互访问可能对数据库造成的影响。
 
-本章深入讨论了**并发控制**的话题。我们讨论了几个广泛使用的隔离级别,特别是**读已提交**,**快照隔离**(有时称为可重复读)和**可串行化**。并通过研究竞争条件的各种例子,来描述这些隔离等级:
+本章深入讨论了 **并发控制** 的话题。我们讨论了几个广泛使用的隔离级别,特别是 **读已提交**、**快照隔离**(有时称为可重复读)和 **可串行化**。并通过研究竞争条件的各种例子,来描述这些隔离等级:
 
 * 脏读
 
-  一个客户端读取到另一个客户端尚未提交的写入。**读已提交**或更强的隔离级别可以防止脏读。
+  一个客户端读取到另一个客户端尚未提交的写入。**读已提交** 或更强的隔离级别可以防止脏读。
 
 * 脏写
 
@@ -843,11 +843,11 @@ WHERE room_id = 123 AND
 
 * 读取偏差(不可重复读)
 
-  在同一个事务中,客户端在不同的时间点会看见数据库的不同状态。**快照隔离**经常用于解决这个问题,它允许事务从一个特定时间点的一致性快照中读取数据。快照隔离通常使用**多版本并发控制(MVCC)** 来实现。
+  在同一个事务中,客户端在不同的时间点会看见数据库的不同状态。**快照隔离** 经常用于解决这个问题,它允许事务从一个特定时间点的一致性快照中读取数据。快照隔离通常使用 **多版本并发控制(MVCC)** 来实现。
 
 * 更新丢失
 
-  两个客户端同时执行**读取-修改-写入序列**。其中一个写操作,在没有合并另一个写入变更情况下,直接覆盖了另一个写操作的结果。所以导致数据丢失。快照隔离的一些实现可以自动防止这种异常,而另一些实现则需要手动锁定(`SELECT FOR UPDATE`)。
+  两个客户端同时执行 **读取 - 修改 - 写入序列**。其中一个写操作,在没有合并另一个写入变更情况下,直接覆盖了另一个写操作的结果。所以导致数据丢失。快照隔离的一些实现可以自动防止这种异常,而另一些实现则需要手动锁定(`SELECT FOR UPDATE`)。
 
 * 写偏差
 
@@ -861,7 +861,7 @@ WHERE room_id = 123 AND
 
 * 字面意义上的串行执行
 
-  如果每个事务的执行速度非常快,并且事务吞吐量足够低,足以在单个CPU核上处理,这是一个简单而有效的选择。
+  如果每个事务的执行速度非常快,并且事务吞吐量足够低,足以在单个 CPU 核上处理,这是一个简单而有效的选择。
 
 * 两阶段锁定
 
@@ -871,7 +871,7 @@ WHERE room_id = 123 AND
 
   一个相当新的算法,避免了先前方法的大部分缺点。它使用乐观的方法,允许事务执行而无需阻塞。当一个事务想要提交时,它会进行检查,如果执行不可串行化,事务就会被中止。
 
-本章中的示例主要是在关系数据模型的上下文中。但是,正如在讨论中,无论使用哪种数据模型,如“**[多对象事务的需求](#多对象事务的需求)**”中所讨论的,事务都是有价值的数据库功能。
+本章中的示例主要是在关系数据模型的上下文中。但是,正如在讨论中,无论使用哪种数据模型,如 “**[多对象事务的需求](#多对象事务的需求)**” 中所讨论的,事务都是有价值的数据库功能。
 
 本章主要是在单机数据库的上下文中,探讨了各种想法和算法。分布式数据库中的事务,则引入了一系列新的困难挑战,我们将在接下来的两章中讨论。
 
@@ -912,7 +912,7 @@ WHERE room_id = 123 AND
 1. Annamalai Gurusami: “[Repeatable Read Isolation Level in InnoDB – How Consistent Read View Works](https://blogs.oracle.com/mysqlinnodb/entry/repeatable_read_isolation_level_in),” *blogs.oracle.com*, January 15, 2013.
 1. Nikita Prokopov: “[Unofficial Guide to Datomic Internals](http://tonsky.me/blog/unofficial-guide-to-datomic-internals/),” *tonsky.me*, May 6, 2014.
 1. Baron Schwartz: “[Immutability, MVCC, and Garbage Collection](http://www.xaprb.com/blog/2013/12/28/immutability-mvcc-and-garbage-collection/),” *xaprb.com*, December 28, 2013.
-1. J. Chris Anderson, Jan Lehnardt, and Noah Slater: *CouchDB: The Definitive Guide*. O'Reilly Media, 2010. ISBN: 978-0-596-15589-6 
+1. J. Chris Anderson, Jan Lehnardt, and Noah Slater: *CouchDB: The Definitive Guide*. O'Reilly Media, 2010. ISBN: 978-0-596-15589-6
 1. Rikdeb Mukherjee: “[Isolation in DB2 (Repeatable Read, Read Stability, Cursor Stability, Uncommitted Read) with Examples](http://mframes.blogspot.co.uk/2013/07/isolation-in-cursor.html),” *mframes.blogspot.co.uk*, July 4, 2013.
 1. Steve Hilker: “[Cursor Stability (CS) – IBM DB2 Community](http://www.toadworld.com/platforms/ibmdb2/w/wiki/6661.cursor-stability-cs.aspx),” *toadworld.com*, March 14, 2013.
 1. Nate Wiger: “[An Atomic Rant](http://www.nateware.com/an-atomic-rant.html),” *nateware.com*, February 18, 2010.
diff --git a/zh-tw/ch4.md b/zh-tw/ch4.md
index 105411a..f6cfe2b 100644
--- a/zh-tw/ch4.md
+++ b/zh-tw/ch4.md
@@ -4,25 +4,25 @@
 
 > 唯變所適
 >
-> —— 以弗所的赫拉克利特,為柏拉圖所引(公元前360年)
+> —— 以弗所的赫拉克利特,為柏拉圖所引(公元前 360 年)
 >
 
 -------------------
 
 [TOC]
 
-應用程式不可避免地隨時間而變化。新產品的推出,對需求的深入理解,或者商業環境的變化,總會伴隨著**功能(feature)** 的增增改改。[第一章](ch1.md)介紹了**可演化性(evolvability)** 的概念:應該盡力構建能靈活適應變化的系統(請參閱“[可演化性:擁抱變化](ch1.md#可演化性:擁抱變化)”)。
+應用程式不可避免地隨時間而變化。新產品的推出,對需求的深入理解,或者商業環境的變化,總會伴隨著 **功能(feature)** 的增增改改。[第一章](ch1.md) 介紹了 **可演化性(evolvability)** 的概念:應該盡力構建能靈活適應變化的系統(請參閱 “[可演化性:擁抱變化](ch1.md#可演化性:擁抱變化)”)。
 
 在大多數情況下,修改應用程式的功能也意味著需要更改其儲存的資料:可能需要使用新的欄位或記錄型別,或者以新方式展示現有資料。
 
-我們在[第二章](ch2.md)討論的資料模型有不同的方法來應對這種變化。關係資料庫通常假定資料庫中的所有資料都遵循一個模式:儘管可以更改該模式(透過模式遷移,即`ALTER`語句),但是在任何時間點都有且僅有一個正確的模式。相比之下,**讀時模式**(schema-on-read,或**無模式**,即schemaless)資料庫不會強制一個模式,因此資料庫可以包含在不同時間寫入的新老資料格式的混合(請參閱 “[文件模型中的模式靈活性](ch2.md#文件模型中的模式靈活性)” )。
+我們在 [第二章](ch2.md) 討論的資料模型有不同的方法來應對這種變化。關係資料庫通常假定資料庫中的所有資料都遵循一個模式:儘管可以更改該模式(透過模式遷移,即 `ALTER` 語句),但是在任何時間點都有且僅有一個正確的模式。相比之下,**讀時模式**(schema-on-read,或 **無模式**,即 schemaless)資料庫不會強制一個模式,因此資料庫可以包含在不同時間寫入的新老資料格式的混合(請參閱 “[文件模型中的模式靈活性](ch2.md#文件模型中的模式靈活性)” )。
 
-當資料**格式(format)** 或**模式(schema)** 發生變化時,通常需要對應用程式程式碼進行相應的更改(例如,為記錄新增新欄位,然後修改程式開始讀寫該欄位)。但在大型應用程式中,程式碼變更通常不會立即完成:
+當資料 **格式(format)** 或 **模式(schema)** 發生變化時,通常需要對應用程式程式碼進行相應的更改(例如,為記錄新增新欄位,然後修改程式開始讀寫該欄位)。但在大型應用程式中,程式碼變更通常不會立即完成:
 
 * 對於 **服務端(server-side)** 應用程式,可能需要執行 **滾動升級 (rolling upgrade)** (也稱為 **階段釋出(staged rollout)** ),一次將新版本部署到少數幾個節點,檢查新版本是否執行正常,然後逐漸部完所有的節點。這樣無需中斷服務即可部署新版本,為頻繁釋出提供了可行性,從而帶來更好的可演化性。
 * 對於 **客戶端(client-side)** 應用程式,升不升級就要看使用者的心情了。使用者可能相當長一段時間裡都不會去升級軟體。
 
-這意味著,新舊版本的程式碼,以及新舊資料格式可能會在系統中同時共處。系統想要繼續順利執行,就需要保持**雙向相容性**:
+這意味著,新舊版本的程式碼,以及新舊資料格式可能會在系統中同時共處。系統想要繼續順利執行,就需要保持 **雙向相容性**:
 
 * 向後相容 (backward compatibility)
 
@@ -36,63 +36,63 @@
 
 向前相容性可能會更棘手,因為舊版的程式需要忽略新版資料格式中新增的部分。
 
-本章中將介紹幾種編碼資料的格式,包括 JSON,XML,Protocol Buffers,Thrift和Avro。尤其將關注這些格式如何應對模式變化,以及它們如何對新舊程式碼資料需要共存的系統提供支援。然後將討論如何使用這些格式進行資料儲存和通訊:在Web服務中,**表述性狀態傳遞(REST)** 和**遠端過程呼叫(RPC)**,以及**訊息傳遞系統**(如Actor和訊息佇列)。
+本章中將介紹幾種編碼資料的格式,包括 JSON、XML、Protocol Buffers、Thrift 和 Avro。尤其將關注這些格式如何應對模式變化,以及它們如何對新舊程式碼資料需要共存的系統提供支援。然後將討論如何使用這些格式進行資料儲存和通訊:在 Web 服務中,**表述性狀態傳遞(REST)** 和 **遠端過程呼叫(RPC)**,以及 **訊息傳遞系統**(如 Actor 和訊息佇列)。
 
 ## 編碼資料的格式
 
 程式通常(至少)使用兩種形式的資料:
 
-1. 在記憶體中,資料儲存在物件,結構體,列表,陣列,散列表,樹等中。 這些資料結構針對CPU的高效訪問和操作進行了最佳化(通常使用指標)。
-2. 如果要將資料寫入檔案,或透過網路傳送,則必須將其 **編碼(encode)** 為某種自包含的位元組序列(例如,JSON文件)。 由於每個程序都有自己獨立的地址空間,一個程序中的指標對任何其他程序都沒有意義,所以這個位元組序列表示會與通常在記憶體中使用的資料結構完全不同[^i]。
+1. 在記憶體中,資料儲存在物件、結構體、列表、陣列、散列表、樹等中。 這些資料結構針對 CPU 的高效訪問和操作進行了最佳化(通常使用指標)。
+2. 如果要將資料寫入檔案,或透過網路傳送,則必須將其 **編碼(encode)** 為某種自包含的位元組序列(例如,JSON 文件)。 由於每個程序都有自己獨立的地址空間,一個程序中的指標對任何其他程序都沒有意義,所以這個位元組序列表示會與通常在記憶體中使用的資料結構完全不同 [^i]。
 
-[^i]: 除一些特殊情況外,例如某些記憶體對映檔案或直接在壓縮資料上操作(如“[列壓縮](ch4.md#列壓縮)”中所述)。
+[^i]: 除一些特殊情況外,例如某些記憶體對映檔案或直接在壓縮資料上操作(如 “[列壓縮](ch4.md#列壓縮)” 中所述)。
 
-所以,需要在兩種表示之間進行某種型別的翻譯。 從記憶體中表示到位元組序列的轉換稱為 **編碼(Encoding)** (也稱為**序列化(serialization)** 或**編組(marshalling)**),反過來稱為**解碼(Decoding)**[^ii](**解析(Parsing)**,**反序列化(deserialization)**,**反編組( unmarshalling)**)[^譯i]。
+所以,需要在兩種表示之間進行某種型別的翻譯。 從記憶體中表示到位元組序列的轉換稱為 **編碼(Encoding)** (也稱為 **序列化(serialization)** 或 **編組(marshalling)**),反過來稱為 **解碼(Decoding)**[^ii](**解析(Parsing)**,**反序列化(deserialization)**,**反編組 (unmarshalling)**)[^譯i]。
 
 [^ii]: 請注意,**編碼(encode)**  與 **加密(encryption)** 無關。 本書不討論加密。
-[^譯i]: Marshal與Serialization的區別:Marshal不僅傳輸物件的狀態,而且會一起傳輸物件的方法(相關程式碼)。
+[^譯i]: Marshal 與 Serialization 的區別:Marshal 不僅傳輸物件的狀態,而且會一起傳輸物件的方法(相關程式碼)。
 
 > #### 術語衝突
-> 不幸的是,在[第七章](ch7.md): **事務(Transaction)** 的上下文裡,**序列化(Serialization)** 這個術語也出現了,而且具有完全不同的含義。儘管序列化可能是更常見的術語,為了避免術語過載,本書中堅持使用 **編碼(Encoding)** 表達此含義。
+> 不幸的是,在 [第七章](ch7.md): **事務(Transaction)** 的上下文裡,**序列化(Serialization)** 這個術語也出現了,而且具有完全不同的含義。儘管序列化可能是更常見的術語,為了避免術語過載,本書中堅持使用 **編碼(Encoding)** 表達此含義。
 
 這是一個常見的問題,因而有許多庫和編碼格式可供選擇。 首先讓我們概覽一下。
 
 ### 語言特定的格式
 
-許多程式語言都內建了將記憶體物件編碼為位元組序列的支援。例如,Java有`java.io.Serializable` 【1】,Ruby有`Marshal`【2】,Python有`pickle`【3】等等。許多第三方庫也存在,例如`Kryo for Java` 【4】。
+許多程式語言都內建了將記憶體物件編碼為位元組序列的支援。例如,Java 有 `java.io.Serializable` 【1】,Ruby 有 `Marshal`【2】,Python 有 `pickle`【3】等等。許多第三方庫也存在,例如 `Kryo for Java` 【4】。
 
 這些編碼庫非常方便,可以用很少的額外程式碼實現記憶體物件的儲存與恢復。但是它們也有一些深層次的問題:
 
 * 這類編碼通常與特定的程式語言深度繫結,其他語言很難讀取這種資料。如果以這類編碼儲存或傳輸資料,那你就和這門語言綁死在一起了。並且很難將系統與其他組織的系統(可能用的是不同的語言)進行整合。
-* 為了恢復相同物件型別的資料,解碼過程需要**例項化任意類**的能力,這通常是安全問題的一個來源【5】:如果攻擊者可以讓應用程式解碼任意的位元組序列,他們就能例項化任意的類,這會允許他們做可怕的事情,如遠端執行任意程式碼【6,7】。
+* 為了恢復相同物件型別的資料,解碼過程需要 **例項化任意類** 的能力,這通常是安全問題的一個來源【5】:如果攻擊者可以讓應用程式解碼任意的位元組序列,他們就能例項化任意的類,這會允許他們做可怕的事情,如遠端執行任意程式碼【6,7】。
 * 在這些庫中,資料版本控制通常是事後才考慮的。因為它們旨在快速簡便地對資料進行編碼,所以往往忽略了前向後向相容性帶來的麻煩問題。
-* 效率(編碼或解碼所花費的CPU時間,以及編碼結構的大小)往往也是事後才考慮的。 例如,Java的內建序列化由於其糟糕的效能和臃腫的編碼而臭名昭著【8】。
+* 效率(編碼或解碼所花費的 CPU 時間,以及編碼結構的大小)往往也是事後才考慮的。 例如,Java 的內建序列化由於其糟糕的效能和臃腫的編碼而臭名昭著【8】。
 
 因此,除非臨時使用,採用語言內建編碼通常是一個壞主意。
 
 ### JSON、XML和二進位制變體
 
-當我們談到可以被多種程式語言讀寫的標準編碼時,JSON和XML是最顯眼的角逐者。它們廣為人知,廣受支援,也“廣受憎惡”。 XML經常收到批評:過於冗長與且過份複雜【9】。 JSON的流行則主要源於(透過成為JavaScript的一個子集)Web瀏覽器的內建支援,以及相對於XML的簡單性。 CSV是另一種流行的與語言無關的格式,儘管其功能相對較弱。
+當我們談到可以被多種程式語言讀寫的標準編碼時,JSON 和 XML 是最顯眼的角逐者。它們廣為人知,廣受支援,也 “廣受憎惡”。 XML 經常收到批評:過於冗長與且過份複雜【9】。 JSON 的流行則主要源於(透過成為 JavaScript 的一個子集)Web 瀏覽器的內建支援,以及相對於 XML 的簡單性。 CSV 是另一種流行的與語言無關的格式,儘管其功能相對較弱。
 
-JSON,XML和CSV屬於文字格式,因此具有人類可讀性(儘管它們的語法是一個熱門爭議話題)。除了表面的語法問題之外,它們也存在一些微妙的問題:
+JSON,XML 和 CSV 屬於文字格式,因此具有人類可讀性(儘管它們的語法是一個熱門爭議話題)。除了表面的語法問題之外,它們也存在一些微妙的問題:
 
-* **數值(numbers)** 的編碼多有歧義之處。XML和CSV不能區分數字和字串(除非引用一個外部模式)。 JSON雖然區分字串與數值,但不區分整數和浮點數,而且不能指定精度。
-* 當處理更大的數值時,這個問題顯得尤為嚴重。例如大於$2^{53}$的整數無法使用IEEE 754雙精度浮點數精確表示,因此在使用浮點數(例如JavaScript)的語言進行分析時,這些數字會變得不準確。 Twitter有一個關於大於$2^{53}$的數字的例子,它使用64位整數來標識每條推文。 Twitter API返回的JSON包含了兩種推特ID,一種是JSON數值,另一種是十進位制字串,以避免JavaScript程式無法正確解析數字的問題【10】。
-* JSON和XML對Unicode字串(即人類可讀的文字)有很好的支援,但是它們不支援二進位制資料(即不帶 **字元編碼(character encoding)** 的位元組序列)。二進位制串是很有用的功能,人們透過使用Base64將二進位制資料編碼為文字來繞過此限制。其特有的模式標識著這個值應當被解釋為Base64編碼的二進位制資料。這種方案雖然管用,但比較Hacky,並且會增加三分之一的資料大小。
-*  XML 【11】和 JSON 【12】都有可選的模式支援。這些模式語言相當強大,所以學習和實現起來都相當複雜。 XML模式的使用相當普遍,但許多基於JSON的工具才不會去折騰模式。對資料的正確解讀(例如區分數值與二進位制串)取決於模式中的資訊,因此不使用XML/JSON模式的應用程式可能需要對相應的編碼/解碼邏輯進行硬編碼。
-* CSV沒有任何模式,因此每行和每列的含義完全由應用程式自行定義。如果應用程式變更添加了新的行或列,那麼這種變更必須透過手工處理。 CSV也是一個相當模糊的格式(如果一個值包含逗號或換行符,會發生什麼?)。儘管其轉義規則已經被正式指定【13】,但並不是所有的解析器都正確的實現了標準。
+* **數值(numbers)** 的編碼多有歧義之處。XML 和 CSV 不能區分數字和字串(除非引用一個外部模式)。 JSON 雖然區分字串與數值,但不區分整數和浮點數,而且不能指定精度。
+* 當處理更大的數值時,這個問題顯得尤為嚴重。例如大於 $2^{53}$ 的整數無法使用 IEEE 754 雙精度浮點數精確表示,因此在使用浮點數(例如 JavaScript)的語言進行分析時,這些數字會變得不準確。 Twitter 有一個關於大於 $2^{53}$ 的數字的例子,它使用 64 位整數來標識每條推文。 Twitter API 返回的 JSON 包含了兩種推特 ID,一種是 JSON 數值,另一種是十進位制字串,以避免 JavaScript 程式無法正確解析數字的問題【10】。
+* JSON 和 XML 對 Unicode 字串(即人類可讀的文字)有很好的支援,但是它們不支援二進位制資料(即不帶 **字元編碼 (character encoding)** 的位元組序列)。二進位制串是很有用的功能,人們透過使用 Base64 將二進位制資料編碼為文字來繞過此限制。其特有的模式標識著這個值應當被解釋為 Base64 編碼的二進位制資料。這種方案雖然管用,但比較 Hacky,並且會增加三分之一的資料大小。
+*  XML 【11】和 JSON 【12】都有可選的模式支援。這些模式語言相當強大,所以學習和實現起來都相當複雜。 XML 模式的使用相當普遍,但許多基於 JSON 的工具才不會去折騰模式。對資料的正確解讀(例如區分數值與二進位制串)取決於模式中的資訊,因此不使用 XML/JSON 模式的應用程式可能需要對相應的編碼 / 解碼邏輯進行硬編碼。
+* CSV 沒有任何模式,因此每行和每列的含義完全由應用程式自行定義。如果應用程式變更添加了新的行或列,那麼這種變更必須透過手工處理。 CSV 也是一個相當模糊的格式(如果一個值包含逗號或換行符,會發生什麼?)。儘管其轉義規則已經被正式指定【13】,但並不是所有的解析器都正確的實現了標準。
 
-儘管存在這些缺陷,但JSON,XML和CSV對很多需求來說已經足夠好了。它們很可能會繼續流行下去,特別是作為資料交換格式來說(即將資料從一個組織傳送到另一個組織)。在這種情況下,只要人們對格式是什麼意見一致,格式有多美觀或者效率有多高效就無所謂了。讓不同的組織就這些東西達成一致的難度超過了絕大多數問題。
+儘管存在這些缺陷,但 JSON、XML 和 CSV 對很多需求來說已經足夠好了。它們很可能會繼續流行下去,特別是作為資料交換格式來說(即將資料從一個組織傳送到另一個組織)。在這種情況下,只要人們對格式是什麼意見一致,格式有多美觀或者效率有多高效就無所謂了。讓不同的組織就這些東西達成一致的難度超過了絕大多數問題。
 
 #### 二進位制編碼
 
-對於僅在組織內部使用的資料,使用最小公約數式的編碼格式壓力較小。例如,可以選擇更緊湊或更快的解析格式。雖然對小資料集來說,收益可以忽略不計;但一旦達到TB級別,資料格式的選型就會產生巨大的影響。
+對於僅在組織內部使用的資料,使用最小公約數式的編碼格式壓力較小。例如,可以選擇更緊湊或更快的解析格式。雖然對小資料集來說,收益可以忽略不計;但一旦達到 TB 級別,資料格式的選型就會產生巨大的影響。
 
-JSON比XML簡潔,但與二進位制格式相比還是太佔空間。這一事實導致大量二進位制編碼版本JSON(MessagePack,BSON,BJSON,UBJSON,BISON和Smile等) 和 XML(例如WBXML和Fast Infoset)的出現。這些格式已經在各種各樣的領域中採用,但是沒有一個能像文字版JSON和XML那樣被廣泛採用。
+JSON 比 XML 簡潔,但與二進位制格式相比還是太佔空間。這一事實導致大量二進位制編碼版本 JSON(MessagePack、BSON、BJSON、UBJSON、BISON 和 Smile 等) 和 XML(例如 WBXML 和 Fast Infoset)的出現。這些格式已經在各種各樣的領域中採用,但是沒有一個能像文字版 JSON 和 XML 那樣被廣泛採用。
 
-這些格式中的一些擴充套件了一組資料型別(例如,區分整數和浮點數,或者增加對二進位制字串的支援),另一方面,它們沒有改變JSON / XML的資料模型。特別是由於它們沒有規定模式,所以它們需要在編碼資料中包含所有的物件欄位名稱。也就是說,在[例4-1]()中的JSON文件的二進位制編碼中,需要在某處包含字串`userName`,`favoriteNumber`和`interests`。
+這些格式中的一些擴充套件了一組資料型別(例如,區分整數和浮點數,或者增加對二進位制字串的支援),另一方面,它們沒有改變 JSON / XML 的資料模型。特別是由於它們沒有規定模式,所以它們需要在編碼資料中包含所有的物件欄位名稱。也就是說,在 [例 4-1]() 中的 JSON 文件的二進位制編碼中,需要在某處包含字串 `userName`,`favoriteNumber` 和 `interests`。
 
-**例4-1 本章中用於展示二進位制編碼的示例記錄**
+**例 4-1 本章中用於展示二進位制編碼的示例記錄**
 
 ```json
 {
@@ -102,26 +102,26 @@ JSON比XML簡潔,但與二進位制格式相比還是太佔空間。這一事
 }
 ```
 
-我們來看一個MessagePack的例子,它是一個JSON的二進位制編碼。圖4-1顯示瞭如果使用MessagePack 【14】對[例4-1]()中的JSON文件進行編碼,則得到的位元組序列。前幾個位元組如下:
+我們來看一個 MessagePack 的例子,它是一個 JSON 的二進位制編碼。圖 4-1 顯示瞭如果使用 MessagePack 【14】對 [例 4-1]() 中的 JSON 文件進行編碼,則得到的位元組序列。前幾個位元組如下:
 
-1. 第一個位元組`0x83`表示接下來是**3**個欄位(低四位= `0x03`)的**物件 object**(高四位= `0x80`)。 (如果想知道如果一個物件有15個以上的欄位會發生什麼情況,欄位的數量塞不進4個bit裡,那麼它會用另一個不同的型別識別符號,欄位的數量被編碼兩個或四個位元組)。
-2. 第二個位元組`0xa8`表示接下來是**8**位元組長(低四位=0x08)的字串(高四位= 0x0a)。
-3. 接下來八個位元組是ASCII字串形式的欄位名稱`userName`。由於之前已經指明長度,不需要任何標記來標識字串的結束位置(或者任何轉義)。
-4. 接下來的七個位元組對字首為`0xa6`的六個字母的字串值`Martin`進行編碼,依此類推。
+1. 第一個位元組 `0x83` 表示接下來是 **3** 個欄位(低四位 = `0x03`)的 **物件 object**(高四位 = `0x80`)。 (如果想知道如果一個物件有 15 個以上的欄位會發生什麼情況,欄位的數量塞不進 4 個 bit 裡,那麼它會用另一個不同的型別識別符號,欄位的數量被編碼兩個或四個位元組)。
+2. 第二個位元組 `0xa8` 表示接下來是 **8** 位元組長(低四位 = `0x08`)的字串(高四位 = `0x0a`)。
+3. 接下來八個位元組是 ASCII 字串形式的欄位名稱 `userName`。由於之前已經指明長度,不需要任何標記來標識字串的結束位置(或者任何轉義)。
+4. 接下來的七個位元組對字首為 `0xa6` 的六個字母的字串值 `Martin` 進行編碼,依此類推。
 
-二進位制編碼長度為66個位元組,僅略小於文字JSON編碼所取的81個位元組(刪除了空白)。所有的JSON的二進位制編碼在這方面是相似的。空間節省了一丁點(以及解析加速)是否能彌補可讀性的損失,誰也說不準。
+二進位制編碼長度為 66 個位元組,僅略小於文字 JSON 編碼所取的 81 個位元組(刪除了空白)。所有的 JSON 的二進位制編碼在這方面是相似的。空間節省了一丁點(以及解析加速)是否能彌補可讀性的損失,誰也說不準。
 
-在下面的章節中,能達到比這好得多的結果,只用32個位元組對相同的記錄進行編碼。
+在下面的章節中,能達到比這好得多的結果,只用 32 個位元組對相同的記錄進行編碼。
 
 
 ![](../img/fig4-1.png)
 
-**圖4-1 使用MessagePack編碼的記錄(例4-1)**
+**圖 4-1 使用 MessagePack 編碼的記錄(例 4-1)**
 
 ### Thrift與Protocol Buffers
 
-Apache Thrift 【15】和Protocol Buffers(protobuf)【16】是基於相同原理的二進位制編碼庫。 Protocol Buffers最初是在Google開發的,Thrift最初是在Facebook開發的,並且在2007~2008年都是開源的【17】。
-Thrift和Protocol Buffers都需要一個模式來編碼任何資料。要在Thrift的[例4-1]()中對資料進行編碼,可以使用Thrift **介面定義語言(IDL)** 來描述模式,如下所示:
+Apache Thrift 【15】和 Protocol Buffers(protobuf)【16】是基於相同原理的二進位制編碼庫。 Protocol Buffers 最初是在 Google 開發的,Thrift 最初是在 Facebook 開發的,並且在 2007~2008 年都是開源的【17】。
+Thrift 和 Protocol Buffers 都需要一個模式來編碼任何資料。要在 Thrift 的 [例 4-1]() 中對資料進行編碼,可以使用 Thrift **介面定義語言(IDL)** 來描述模式,如下所示:
 
 ```c
 struct Person {
@@ -131,7 +131,7 @@ struct Person {
 }
 ```
 
-Protocol Buffers的等效模式定義看起來非常相似:
+Protocol Buffers 的等效模式定義看起來非常相似:
 
 ```protobuf
 message Person {
@@ -141,38 +141,38 @@ message Person {
 }
 ```
 
-Thrift和Protocol Buffers每一個都帶有一個程式碼生成工具,它採用了類似於這裡所示的模式定義,並且生成了以各種程式語言實現模式的類【18】。你的應用程式程式碼可以呼叫此生成的程式碼來對模式的記錄進行編碼或解碼。
-用這個模式編碼的資料是什麼樣的?令人困惑的是,Thrift有兩種不同的二進位制編碼格式[^iii],分別稱為BinaryProtocol和CompactProtocol。先來看看BinaryProtocol。使用這種格式的編碼來編碼[例4-1]()中的訊息只需要59個位元組,如[圖4-2](../img/fig4-2.png)所示【19】。
+Thrift 和 Protocol Buffers 每一個都帶有一個程式碼生成工具,它採用了類似於這裡所示的模式定義,並且生成了以各種程式語言實現模式的類【18】。你的應用程式程式碼可以呼叫此生成的程式碼來對模式的記錄進行編碼或解碼。
+用這個模式編碼的資料是什麼樣的?令人困惑的是,Thrift 有兩種不同的二進位制編碼格式 [^iii],分別稱為 BinaryProtocol 和 CompactProtocol。先來看看 BinaryProtocol。使用這種格式的編碼來編碼 [例 4-1]() 中的訊息只需要 59 個位元組,如 [圖 4-2](../img/fig4-2.png) 所示【19】。
 
 ![](../img/fig4-2.png)
 
-**圖4-2 使用Thrift二進位制協議編碼的記錄**
+**圖 4-2 使用 Thrift 二進位制協議編碼的記錄**
 
-[^iii]: 實際上,Thrift有三種二進位制協議:BinaryProtocol、CompactProtocol和DenseProtocol,儘管DenseProtocol只支援C ++實現,所以不算作跨語言【18】。 除此之外,它還有兩種不同的基於JSON的編碼格式【19】。 真逗!
+[^iii]: 實際上,Thrift 有三種二進位制協議:BinaryProtocol、CompactProtocol 和 DenseProtocol,儘管 DenseProtocol 只支援 C ++ 實現,所以不算作跨語言【18】。 除此之外,它還有兩種不同的基於 JSON 的編碼格式【19】。 真逗!
 
-與[圖4-1](Img/fig4-1.png)類似,每個欄位都有一個型別註釋(用於指示它是一個字串,整數,列表等),還可以根據需要指定長度(字串的長度,列表中的專案數) 。出現在資料中的字串`(“Martin”, “daydreaming”, “hacking”)`也被編碼為ASCII(或者說,UTF-8),與之前類似。
+與 [圖 4-1](Img/fig4-1.png) 類似,每個欄位都有一個型別註釋(用於指示它是一個字串,整數,列表等),還可以根據需要指定長度(字串的長度,列表中的專案數) 。出現在資料中的字串 `(“Martin”, “daydreaming”, “hacking”)` 也被編碼為 ASCII(或者說,UTF-8),與之前類似。
 
-與[圖4-1](../img/fig4-1.png)相比,最大的區別是沒有欄位名`(userName, favoriteNumber, interests)`。相反,編碼資料包含欄位標籤,它們是數字`(1, 2和3)`。這些是模式定義中出現的數字。欄位標記就像欄位的別名 - 它們是說我們正在談論的欄位的一種緊湊的方式,而不必拼出欄位名稱。
+與 [圖 4-1](../img/fig4-1.png) 相比,最大的區別是沒有欄位名 `(userName, favoriteNumber, interests)`。相反,編碼資料包含欄位標籤,它們是數字 `(1, 2 和 3)`。這些是模式定義中出現的數字。欄位標記就像欄位的別名 - 它們是說我們正在談論的欄位的一種緊湊的方式,而不必拼出欄位名稱。
 
-Thrift CompactProtocol編碼在語義上等同於BinaryProtocol,但是如[圖4-3](../img/fig4-3.png)所示,它只將相同的資訊打包成只有34個位元組。它透過將欄位型別和標籤號打包到單個位元組中,並使用可變長度整數來實現。數字1337不是使用全部八個位元組,而是用兩個位元組編碼,每個位元組的最高位用來指示是否還有更多的位元組。這意味著-64到63之間的數字被編碼為一個位元組,-8192和8191之間的數字以兩個位元組編碼,等等。較大的數字使用更多的位元組。
+Thrift CompactProtocol 編碼在語義上等同於 BinaryProtocol,但是如 [圖 4-3](../img/fig4-3.png) 所示,它只將相同的資訊打包成只有 34 個位元組。它透過將欄位型別和標籤號打包到單個位元組中,並使用可變長度整數來實現。數字 1337 不是使用全部八個位元組,而是用兩個位元組編碼,每個位元組的最高位用來指示是否還有更多的位元組。這意味著 - 64 到 63 之間的數字被編碼為一個位元組,-8192 和 8191 之間的數字以兩個位元組編碼,等等。較大的數字使用更多的位元組。
 
 ![](../img/fig4-3.png)
 
-**圖4-3 使用Thrift壓縮協議編碼的記錄**
+**圖 4-3 使用 Thrift 壓縮協議編碼的記錄**
 
-最後,Protocol Buffers(只有一種二進位制編碼格式)對相同的資料進行編碼,如[圖4-4](../img/fig4-4.png)所示。 它的打包方式稍有不同,但與Thrift的CompactProtocol非常相似。 Protobuf將同樣的記錄塞進了33個位元組中。
+最後,Protocol Buffers(只有一種二進位制編碼格式)對相同的資料進行編碼,如 [圖 4-4](../img/fig4-4.png) 所示。 它的打包方式稍有不同,但與 Thrift 的 CompactProtocol 非常相似。 Protobuf 將同樣的記錄塞進了 33 個位元組中。
 
 ![](../img/fig4-4.png)
 
-**圖4-4 使用Protobuf編碼的記錄**
+**圖 4-4 使用 Protobuf 編碼的記錄**
 
 需要注意的一個細節:在前面所示的模式中,每個欄位被標記為必需或可選,但是這對欄位如何編碼沒有任何影響(二進位制資料中沒有任何欄位指示某欄位是否必須)。區別在於,如果欄位設定為 `required`,但未設定該欄位,則所需的執行時檢查將失敗,這對於捕獲錯誤非常有用。
 
 #### 欄位標籤和模式演變
 
-我們之前說過,模式不可避免地需要隨著時間而改變。我們稱之為模式演變。 Thrift和Protocol Buffers如何處理模式更改,同時保持向後相容性?
+我們之前說過,模式不可避免地需要隨著時間而改變。我們稱之為模式演變。 Thrift 和 Protocol Buffers 如何處理模式更改,同時保持向後相容性?
 
-從示例中可以看出,編碼的記錄就是其編碼欄位的拼接。每個欄位由其標籤號碼(樣本模式中的數字1,2,3)標識,並用資料型別(例如字串或整數)註釋。如果沒有設定欄位值,則簡單地從編碼記錄中省略。從中可以看到,欄位標記對編碼資料的含義至關重要。你可以更改架構中欄位的名稱,因為編碼的資料永遠不會引用欄位名稱,但不能更改欄位的標記,因為這會使所有現有的編碼資料無效。
+從示例中可以看出,編碼的記錄就是其編碼欄位的拼接。每個欄位由其標籤號碼(樣本模式中的數字 1,2,3)標識,並用資料型別(例如字串或整數)註釋。如果沒有設定欄位值,則簡單地從編碼記錄中省略。從中可以看到,欄位標記對編碼資料的含義至關重要。你可以更改架構中欄位的名稱,因為編碼的資料永遠不會引用欄位名稱,但不能更改欄位的標記,因為這會使所有現有的編碼資料無效。
 
 你可以新增新的欄位到架構,只要你給每個欄位一個新的標籤號碼。如果舊的程式碼(不知道你新增的新的標籤號碼)試圖讀取新程式碼寫入的資料,包括一個新的欄位,其標籤號碼不能識別,它可以簡單地忽略該欄位。資料型別註釋允許解析器確定需要跳過的位元組數。這保持了向前相容性:舊程式碼可以讀取由新程式碼編寫的記錄。
 
@@ -182,19 +182,19 @@ Thrift CompactProtocol編碼在語義上等同於BinaryProtocol,但是如[圖4
 
 #### 資料型別和模式演變
 
-如何改變欄位的資料型別?這也許是可能的——詳細資訊請查閱相關的文件——但是有一個風險,值將失去精度或被截斷。例如,假設你將一個32位的整數變成一個64位的整數。新程式碼可以輕鬆讀取舊程式碼寫入的資料,因為解析器可以用零填充任何缺失的位。但是,如果舊程式碼讀取由新程式碼寫入的資料,則舊程式碼仍使用32位變數來儲存該值。如果解碼的64位值不適合32位,則它將被截斷。
+如何改變欄位的資料型別?這也許是可能的 —— 詳細資訊請查閱相關的文件 —— 但是有一個風險,值將失去精度或被截斷。例如,假設你將一個 32 位的整數變成一個 64 位的整數。新程式碼可以輕鬆讀取舊程式碼寫入的資料,因為解析器可以用零填充任何缺失的位。但是,如果舊程式碼讀取由新程式碼寫入的資料,則舊程式碼仍使用 32 位變數來儲存該值。如果解碼的 64 位值不適合 32 位,則它將被截斷。
 
-Protobuf的一個奇怪的細節是,它沒有列表或陣列資料型別,而是有一個欄位的重複標記(`repeated`,這是除必需和可選之外的第三個選項)。如[圖4-4](../img/fig4-4.png)所示,重複欄位的編碼正如它所說的那樣:同一個欄位標記只是簡單地出現在記錄中。這具有很好的效果,可以將可選(單值)欄位更改為重複(多值)欄位。讀取舊資料的新程式碼會看到一個包含零個或一個元素的列表(取決於該欄位是否存在)。讀取新資料的舊程式碼只能看到列表的最後一個元素。
+Protobuf 的一個奇怪的細節是,它沒有列表或陣列資料型別,而是有一個欄位的重複標記(`repeated`,這是除必需和可選之外的第三個選項)。如 [圖 4-4](../img/fig4-4.png) 所示,重複欄位的編碼正如它所說的那樣:同一個欄位標記只是簡單地出現在記錄中。這具有很好的效果,可以將可選(單值)欄位更改為重複(多值)欄位。讀取舊資料的新程式碼會看到一個包含零個或一個元素的列表(取決於該欄位是否存在)。讀取新資料的舊程式碼只能看到列表的最後一個元素。
 
-Thrift有一個專用的列表資料型別,它使用列表元素的資料型別進行引數化。這不允許Protocol Buffers所做的從單值到多值的演變,但是它具有支援巢狀列表的優點。
+Thrift 有一個專用的列表資料型別,它使用列表元素的資料型別進行引數化。這不允許 Protocol Buffers 所做的從單值到多值的演變,但是它具有支援巢狀列表的優點。
 
 ### Avro
 
-Apache Avro 【20】是另一種二進位制編碼格式,與Protocol Buffers和Thrift有著有趣的不同。 它是作為Hadoop的一個子專案在2009年開始的,因為Thrift不適合Hadoop的用例【21】。
+Apache Avro 【20】是另一種二進位制編碼格式,與 Protocol Buffers 和 Thrift 有著有趣的不同。 它是作為 Hadoop 的一個子專案在 2009 年開始的,因為 Thrift 不適合 Hadoop 的用例【21】。
 
-Avro也使用模式來指定正在編碼的資料的結構。 它有兩種模式語言:一種(Avro IDL)用於人工編輯,一種(基於JSON)更易於機器讀取。
+Avro 也使用模式來指定正在編碼的資料的結構。 它有兩種模式語言:一種(Avro IDL)用於人工編輯,一種(基於 JSON)更易於機器讀取。
 
-我們用Avro IDL編寫的示例模式可能如下所示:
+我們用 Avro IDL 編寫的示例模式可能如下所示:
 
 ```c
 record Person {
@@ -204,7 +204,7 @@ record Person {
 }
 ```
 
-等價的JSON表示:
+等價的 JSON 表示:
 
 ```json
 {
@@ -218,102 +218,102 @@ record Person {
 }
 ```
 
-首先,請注意模式中沒有標籤號碼。 如果我們使用這個模式編碼我們的例子記錄([例4-1]()),Avro二進位制編碼只有32個位元組長,這是我們所見過的所有編碼中最緊湊的。 編碼位元組序列的分解如[圖4-5](../img/fig4-5.png)所示。
+首先,請注意模式中沒有標籤號碼。 如果我們使用這個模式編碼我們的例子記錄([例 4-1]()),Avro 二進位制編碼只有 32 個位元組長,這是我們所見過的所有編碼中最緊湊的。 編碼位元組序列的分解如 [圖 4-5](../img/fig4-5.png) 所示。
 
-如果你檢查位元組序列,你可以看到沒有什麼可以識別字段或其資料型別。 編碼只是由連在一起的值組成。 一個字串只是一個長度字首,後跟UTF-8位元組,但是在被包含的資料中沒有任何內容告訴你它是一個字串。 它可以是一個整數,也可以是其他的整數。 整數使用可變長度編碼(與Thrift的CompactProtocol相同)進行編碼。
+如果你檢查位元組序列,你可以看到沒有什麼可以識別字段或其資料型別。 編碼只是由連在一起的值組成。 一個字串只是一個長度字首,後跟 UTF-8 位元組,但是在被包含的資料中沒有任何內容告訴你它是一個字串。 它可以是一個整數,也可以是其他的整數。 整數使用可變長度編碼(與 Thrift 的 CompactProtocol 相同)進行編碼。
 
 ![](../img/fig4-5.png)
 
-**圖4-5 使用Avro編碼的記錄**
+**圖 4-5 使用 Avro 編碼的記錄**
 
-為了解析二進位制資料,你按照它們出現在模式中的順序遍歷這些欄位,並使用模式來告訴你每個欄位的資料型別。這意味著如果讀取資料的程式碼使用與寫入資料的程式碼完全相同的模式,才能正確解碼二進位制資料。Reader和Writer之間的模式不匹配意味著錯誤地解碼資料。
+為了解析二進位制資料,你按照它們出現在模式中的順序遍歷這些欄位,並使用模式來告訴你每個欄位的資料型別。這意味著如果讀取資料的程式碼使用與寫入資料的程式碼完全相同的模式,才能正確解碼二進位制資料。Reader 和 Writer 之間的模式不匹配意味著錯誤地解碼資料。
 
-那麼,Avro如何支援模式演變呢?
+那麼,Avro 如何支援模式演變呢?
 
 #### Writer模式與Reader模式
 
-有了Avro,當應用程式想要編碼一些資料(將其寫入檔案或資料庫,透過網路傳送等)時,它使用它知道的任何版本的模式編碼資料,例如,模式可能被編譯到應用程式中。這被稱為Writer模式。
+有了 Avro,當應用程式想要編碼一些資料(將其寫入檔案或資料庫,透過網路傳送等)時,它使用它知道的任何版本的模式編碼資料,例如,模式可能被編譯到應用程式中。這被稱為 Writer 模式。
 
-當一個應用程式想要解碼一些資料(從一個檔案或資料庫讀取資料,從網路接收資料等)時,它希望資料在某個模式中,這就是Reader模式。這是應用程式程式碼所依賴的模式,在應用程式的構建過程中,程式碼可能已經從該模式生成。
+當一個應用程式想要解碼一些資料(從一個檔案或資料庫讀取資料,從網路接收資料等)時,它希望資料在某個模式中,這就是 Reader 模式。這是應用程式程式碼所依賴的模式,在應用程式的構建過程中,程式碼可能已經從該模式生成。
 
-Avro的關鍵思想是Writer模式和Reader模式不必是相同的 - 他們只需要相容。當資料解碼(讀取)時,Avro庫透過並排檢視Writer模式和Reader模式並將資料從Writer模式轉換到Reader模式來解決差異。 Avro規範【20】確切地定義了這種解析的工作原理,如[圖4-6](../img/fig4-6.png)所示。
+Avro 的關鍵思想是 Writer 模式和 Reader 模式不必是相同的 - 他們只需要相容。當資料解碼(讀取)時,Avro 庫透過並排檢視 Writer 模式和 Reader 模式並將資料從 Writer 模式轉換到 Reader 模式來解決差異。 Avro 規範【20】確切地定義了這種解析的工作原理,如 [圖 4-6](../img/fig4-6.png) 所示。
 
-例如,如果Writer模式和Reader模式的欄位順序不同,這是沒有問題的,因為模式解析透過欄位名匹配欄位。如果讀取資料的程式碼遇到出現在Writer模式中但不在Reader模式中的欄位,則忽略它。如果讀取資料的程式碼需要某個欄位,但是Writer模式不包含該名稱的欄位,則使用在Reader模式中宣告的預設值填充。
+例如,如果 Writer 模式和 Reader 模式的欄位順序不同,這是沒有問題的,因為模式解析透過欄位名匹配欄位。如果讀取資料的程式碼遇到出現在 Writer 模式中但不在 Reader 模式中的欄位,則忽略它。如果讀取資料的程式碼需要某個欄位,但是 Writer 模式不包含該名稱的欄位,則使用在 Reader 模式中宣告的預設值填充。
 
 ![](../img/fig4-6.png)
 
-**圖4-6 一個Avro Reader解決讀寫模式的差異**
+**圖 4-6 一個 Avro Reader 解決讀寫模式的差異**
 
 #### 模式演變規則
 
-使用Avro,向前相容性意味著你可以將新版本的模式作為Writer,並將舊版本的模式作為Reader。相反,向後相容意味著你可以有一個作為Reader的新版本模式和作為Writer的舊版本模式。
+使用 Avro,向前相容性意味著你可以將新版本的模式作為 Writer,並將舊版本的模式作為 Reader。相反,向後相容意味著你可以有一個作為 Reader 的新版本模式和作為 Writer 的舊版本模式。
 
-為了保持相容性,你只能新增或刪除具有預設值的欄位(我們的Avro模式中的欄位`favoriteNumber`的預設值為`null`)。例如,假設你添加了一個有預設值的欄位,這個新的欄位將存在於新模式而不是舊模式中。當使用新模式的Reader讀取使用舊模式寫入的記錄時,將為缺少的欄位填充預設值。
+為了保持相容性,你只能新增或刪除具有預設值的欄位(我們的 Avro 模式中的欄位 `favoriteNumber` 的預設值為 `null`)。例如,假設你添加了一個有預設值的欄位,這個新的欄位將存在於新模式而不是舊模式中。當使用新模式的 Reader 讀取使用舊模式寫入的記錄時,將為缺少的欄位填充預設值。
 
-如果你要新增一個沒有預設值的欄位,新的Reader將無法讀取舊Writer寫的資料,所以你會破壞向後相容性。如果你要刪除沒有預設值的欄位,舊的Reader將無法讀取新Writer寫入的資料,因此你會打破向前相容性。在一些程式語言中,null是任何變數可以接受的預設值,但在Avro中並不是這樣:如果要允許一個欄位為`null`,則必須使用聯合型別。例如,`union {null, long, string} field;`表示field可以是數字或字串,也可以是`null`。如果要將null作為預設值,則它必須是union的分支之一[^iv]。這樣的寫法比預設情況下就允許任何變數是`null`顯得更加冗長,但是透過明確什麼可以和什麼不可以是`null`,有助於防止出錯【22】。
+如果你要新增一個沒有預設值的欄位,新的 Reader 將無法讀取舊 Writer 寫的資料,所以你會破壞向後相容性。如果你要刪除沒有預設值的欄位,舊的 Reader 將無法讀取新 Writer 寫入的資料,因此你會打破向前相容性。在一些程式語言中,null 是任何變數可以接受的預設值,但在 Avro 中並不是這樣:如果要允許一個欄位為 `null`,則必須使用聯合型別。例如,`union {null, long, string} field;` 表示 field 可以是數字或字串,也可以是 `null`。如果要將 null 作為預設值,則它必須是 union 的分支之一 [^iv]。這樣的寫法比預設情況下就允許任何變數是 `null` 顯得更加冗長,但是透過明確什麼可以和什麼不可以是 `null`,有助於防止出錯【22】。
 
-[^iv]: 確切地說,預設值必須是聯合的第一個分支的型別,儘管這是Avro的特定限制,而不是聯合型別的一般特徵。
+[^iv]: 確切地說,預設值必須是聯合的第一個分支的型別,儘管這是 Avro 的特定限制,而不是聯合型別的一般特徵。
 
-因此,Avro沒有像Protocol Buffers和Thrift那樣的`optional`和`required`標記(但它有聯合型別和預設值)。
+因此,Avro 沒有像 Protocol Buffers 和 Thrift 那樣的 `optional` 和 `required` 標記(但它有聯合型別和預設值)。
 
-只要Avro可以支援相應的型別轉換,就可以改變欄位的資料型別。更改欄位的名稱也是可能的,但有點棘手:Reader模式可以包含欄位名稱的別名,所以它可以匹配舊Writer的模式欄位名稱與別名。這意味著更改欄位名稱是向後相容的,但不能向前相容。同樣,向聯合型別新增分支也是向後相容的,但不能向前相容。
+只要 Avro 可以支援相應的型別轉換,就可以改變欄位的資料型別。更改欄位的名稱也是可能的,但有點棘手:Reader 模式可以包含欄位名稱的別名,所以它可以匹配舊 Writer 的模式欄位名稱與別名。這意味著更改欄位名稱是向後相容的,但不能向前相容。同樣,向聯合型別新增分支也是向後相容的,但不能向前相容。
 
 #### 但Writer模式到底是什麼?
 
-到目前為止,我們一直跳過了一個重要的問題:對於一段特定的編碼資料,Reader如何知道其Writer模式?我們不能只將整個模式包括在每個記錄中,因為模式可能比編碼的資料大得多,從而使二進位制編碼節省的所有空間都是徒勞的。
+到目前為止,我們一直跳過了一個重要的問題:對於一段特定的編碼資料,Reader 如何知道其 Writer 模式?我們不能只將整個模式包括在每個記錄中,因為模式可能比編碼的資料大得多,從而使二進位制編碼節省的所有空間都是徒勞的。
 
-答案取決於Avro使用的上下文。舉幾個例子:
+答案取決於 Avro 使用的上下文。舉幾個例子:
 
 * 有很多記錄的大檔案
 
-  Avro的一個常見用途 - 尤其是在Hadoop環境中 - 用於儲存包含數百萬條記錄的大檔案,所有記錄都使用相同的模式進行編碼(我們將在[第十章](ch10.md)討論這種情況)。在這種情況下,該檔案的作者可以在檔案的開頭只包含一次Writer模式。 Avro指定了一個檔案格式(物件容器檔案)來做到這一點。
+  Avro 的一個常見用途 - 尤其是在 Hadoop 環境中 - 用於儲存包含數百萬條記錄的大檔案,所有記錄都使用相同的模式進行編碼(我們將在 [第十章](ch10.md) 討論這種情況)。在這種情況下,該檔案的作者可以在檔案的開頭只包含一次 Writer 模式。 Avro 指定了一個檔案格式(物件容器檔案)來做到這一點。
 
 * 支援獨立寫入的記錄的資料庫
 
-  在一個數據庫中,不同的記錄可能會在不同的時間點使用不同的Writer模式來寫入 - 你不能假定所有的記錄都有相同的模式。最簡單的解決方案是在每個編碼記錄的開始處包含一個版本號,並在資料庫中保留一個模式版本列表。Reader可以獲取記錄,提取版本號,然後從資料庫中獲取該版本號的Writer模式。使用該Writer模式,它可以解碼記錄的其餘部分(例如Espresso 【23】就是這樣工作的)。
+  在一個數據庫中,不同的記錄可能會在不同的時間點使用不同的 Writer 模式來寫入 - 你不能假定所有的記錄都有相同的模式。最簡單的解決方案是在每個編碼記錄的開始處包含一個版本號,並在資料庫中保留一個模式版本列表。Reader 可以獲取記錄,提取版本號,然後從資料庫中獲取該版本號的 Writer 模式。使用該 Writer 模式,它可以解碼記錄的其餘部分(例如 Espresso 【23】就是這樣工作的)。
 
 * 透過網路連線傳送記錄
 
-  當兩個程序透過雙向網路連線進行通訊時,他們可以在連線設定上協商模式版本,然後在連線的生命週期中使用該模式。 Avro RPC協議(請參閱“[服務中的資料流:REST與RPC](#服務中的資料流:REST與RPC)”)就是這樣工作的。
+  當兩個程序透過雙向網路連線進行通訊時,他們可以在連線設定上協商模式版本,然後在連線的生命週期中使用該模式。 Avro RPC 協議(請參閱 “[服務中的資料流:REST 與 RPC](#服務中的資料流:REST與RPC)”)就是這樣工作的。
 
 具有模式版本的資料庫在任何情況下都是非常有用的,因為它充當文件併為你提供了檢查模式相容性的機會【24】。作為版本號,你可以使用一個簡單的遞增整數,或者你可以使用模式的雜湊。
 
 #### 動態生成的模式
 
-與Protocol Buffers和Thrift相比,Avro方法的一個優點是架構不包含任何標籤號碼。但為什麼這很重要?在模式中保留一些數字有什麼問題?
+與 Protocol Buffers 和 Thrift 相比,Avro 方法的一個優點是架構不包含任何標籤號碼。但為什麼這很重要?在模式中保留一些數字有什麼問題?
 
-不同之處在於Avro對動態生成的模式更友善。例如,假如你有一個關係資料庫,你想要把它的內容轉儲到一個檔案中,並且你想使用二進位制格式來避免前面提到的文字格式(JSON,CSV,SQL)的問題。如果你使用Avro,你可以很容易地從關係模式生成一個Avro模式(在我們之前看到的JSON表示中),並使用該模式對資料庫內容進行編碼,並將其全部轉儲到Avro物件容器檔案【25】中。你為每個資料庫表生成一個記錄模式,每個列成為該記錄中的一個欄位。資料庫中的列名稱對映到Avro中的欄位名稱。
+不同之處在於 Avro 對動態生成的模式更友善。例如,假如你有一個關係資料庫,你想要把它的內容轉儲到一個檔案中,並且你想使用二進位制格式來避免前面提到的文字格式(JSON,CSV,SQL)的問題。如果你使用 Avro,你可以很容易地從關係模式生成一個 Avro 模式(在我們之前看到的 JSON 表示中),並使用該模式對資料庫內容進行編碼,並將其全部轉儲到 Avro 物件容器檔案【25】中。你為每個資料庫表生成一個記錄模式,每個列成為該記錄中的一個欄位。資料庫中的列名稱對映到 Avro 中的欄位名稱。
 
-現在,如果資料庫模式發生變化(例如,一個表中添加了一列,刪除了一列),則可以從更新的資料庫模式生成新的Avro模式,並在新的Avro模式中匯出資料。資料匯出過程不需要注意模式的改變 - 每次執行時都可以簡單地進行模式轉換。任何讀取新資料檔案的人都會看到記錄的欄位已經改變,但是由於欄位是透過名字來標識的,所以更新的Writer模式仍然可以與舊的Reader模式匹配。
+現在,如果資料庫模式發生變化(例如,一個表中添加了一列,刪除了一列),則可以從更新的資料庫模式生成新的 Avro 模式,並在新的 Avro 模式中匯出資料。資料匯出過程不需要注意模式的改變 - 每次執行時都可以簡單地進行模式轉換。任何讀取新資料檔案的人都會看到記錄的欄位已經改變,但是由於欄位是透過名字來標識的,所以更新的 Writer 模式仍然可以與舊的 Reader 模式匹配。
 
-相比之下,如果你為此使用Thrift或Protocol Buffers,則欄位標籤可能必須手動分配:每次資料庫模式更改時,管理員都必須手動更新從資料庫列名到欄位標籤的對映(這可能會自動化,但模式生成器必須非常小心,不要分配以前使用的欄位標籤)。這種動態生成的模式根本不是Thrift或Protocol Buffers的設計目標,而是Avro的。
+相比之下,如果你為此使用 Thrift 或 Protocol Buffers,則欄位標籤可能必須手動分配:每次資料庫模式更改時,管理員都必須手動更新從資料庫列名到欄位標籤的對映(這可能會自動化,但模式生成器必須非常小心,不要分配以前使用的欄位標籤)。這種動態生成的模式根本不是 Thrift 或 Protocol Buffers 的設計目標,而是 Avro 的。
 
 #### 程式碼生成和動態型別的語言
 
-Thrift和Protobuf依賴於程式碼生成:在定義了模式之後,可以使用你選擇的程式語言生成實現此模式的程式碼。這在Java、C++或C#等靜態型別語言中很有用,因為它允許將高效的記憶體中結構用於解碼的資料,並且在編寫訪問資料結構的程式時允許在IDE中進行型別檢查和自動完成。
+Thrift 和 Protobuf 依賴於程式碼生成:在定義了模式之後,可以使用你選擇的程式語言生成實現此模式的程式碼。這在 Java、C++ 或 C# 等靜態型別語言中很有用,因為它允許將高效的記憶體中結構用於解碼的資料,並且在編寫訪問資料結構的程式時允許在 IDE 中進行型別檢查和自動完成。
 
-在動態型別程式語言(如JavaScript、Ruby或Python)中,生成程式碼沒有太多意義,因為沒有編譯時型別檢查器來滿足。程式碼生成在這些語言中經常被忽視,因為它們避免了顯式的編譯步驟。而且,對於動態生成的模式(例如從資料庫表生成的Avro模式),程式碼生成對獲取資料是一個不必要的障礙。
+在動態型別程式語言(如 JavaScript、Ruby 或 Python)中,生成程式碼沒有太多意義,因為沒有編譯時型別檢查器來滿足。程式碼生成在這些語言中經常被忽視,因為它們避免了顯式的編譯步驟。而且,對於動態生成的模式(例如從資料庫表生成的 Avro 模式),程式碼生成對獲取資料是一個不必要的障礙。
 
-Avro為靜態型別程式語言提供了可選的程式碼生成功能,但是它也可以在不生成任何程式碼的情況下使用。如果你有一個物件容器檔案(它嵌入了Writer模式),你可以簡單地使用Avro庫開啟它,並以與檢視JSON檔案相同的方式檢視資料。該檔案是自描述的,因為它包含所有必要的元資料。
+Avro 為靜態型別程式語言提供了可選的程式碼生成功能,但是它也可以在不生成任何程式碼的情況下使用。如果你有一個物件容器檔案(它嵌入了 Writer 模式),你可以簡單地使用 Avro 庫開啟它,並以與檢視 JSON 檔案相同的方式檢視資料。該檔案是自描述的,因為它包含所有必要的元資料。
 
-這個屬性特別適用於動態型別的資料處理語言如Apache Pig 【26】。在Pig中,你可以開啟一些Avro檔案,開始分析它們,並編寫派生資料集以Avro格式輸出檔案,而無需考慮模式。
+這個屬性特別適用於動態型別的資料處理語言如 Apache Pig 【26】。在 Pig 中,你可以開啟一些 Avro 檔案,開始分析它們,並編寫派生資料集以 Avro 格式輸出檔案,而無需考慮模式。
 
 ### 模式的優點
 
-正如我們所看到的,Protocol Buffers,Thrift和Avro都使用模式來描述二進位制編碼格式。他們的模式語言比XML模式或者JSON模式簡單得多,而後者支援更詳細的驗證規則(例如,“該欄位的字串值必須與該正則表示式匹配”或“該欄位的整數值必須在0和100之間“)。由於Protocol Buffers,Thrift和Avro實現起來更簡單,使用起來也更簡單,所以它們已經發展到支援相當廣泛的程式語言。
+正如我們所看到的,Protocol Buffers,Thrift 和 Avro 都使用模式來描述二進位制編碼格式。他們的模式語言比 XML 模式或者 JSON 模式簡單得多,而後者支援更詳細的驗證規則(例如,“該欄位的字串值必須與該正則表示式匹配” 或 “該欄位的整數值必須在 0 和 100 之間 “)。由於 Protocol Buffers,Thrift 和 Avro 實現起來更簡單,使用起來也更簡單,所以它們已經發展到支援相當廣泛的程式語言。
 
-這些編碼所基於的想法絕不是新的。例如,它們與ASN.1有很多相似之處,它是1984年首次被標準化的模式定義語言【27】。它被用來定義各種網路協議,例如其二進位制編碼(DER)仍然被用於編碼SSL證書(X.509)【28】。 ASN.1支援使用標籤號碼的模式演進,類似於Protocol Buffers和Thrift 【29】。然而,它也非常複雜,而且沒有好的配套文件,所以ASN.1可能不是新應用程式的好選擇。
+這些編碼所基於的想法絕不是新的。例如,它們與 ASN.1 有很多相似之處,它是 1984 年首次被標準化的模式定義語言【27】。它被用來定義各種網路協議,例如其二進位制編碼(DER)仍然被用於編碼 SSL 證書(X.509)【28】。 ASN.1 支援使用標籤號碼的模式演進,類似於 Protocol Buffers 和 Thrift 【29】。然而,它也非常複雜,而且沒有好的配套文件,所以 ASN.1 可能不是新應用程式的好選擇。
 
-許多資料系統也為其資料實現了某種專有的二進位制編碼。例如,大多數關係資料庫都有一個網路協議,你可以透過該協議向資料庫傳送查詢並獲取響應。這些協議通常特定於特定的資料庫,並且資料庫供應商提供將來自資料庫的網路協議的響應解碼為記憶體資料結構的驅動程式(例如使用ODBC或JDBC API)。
+許多資料系統也為其資料實現了某種專有的二進位制編碼。例如,大多數關係資料庫都有一個網路協議,你可以透過該協議向資料庫傳送查詢並獲取響應。這些協議通常特定於特定的資料庫,並且資料庫供應商提供將來自資料庫的網路協議的響應解碼為記憶體資料結構的驅動程式(例如使用 ODBC 或 JDBC API)。
 
-所以,我們可以看到,儘管JSON,XML和CSV等文字資料格式非常普遍,但基於模式的二進位制編碼也是一個可行的選擇。他們有一些很好的屬性:
+所以,我們可以看到,儘管 JSON,XML 和 CSV 等文字資料格式非常普遍,但基於模式的二進位制編碼也是一個可行的選擇。他們有一些很好的屬性:
 
-* 它們可以比各種“二進位制JSON”變體更緊湊,因為它們可以省略編碼資料中的欄位名稱。
+* 它們可以比各種 “二進位制 JSON” 變體更緊湊,因為它們可以省略編碼資料中的欄位名稱。
 * 模式是一種有價值的文件形式,因為模式是解碼所必需的,所以可以確定它是最新的(而手動維護的文件可能很容易偏離現實)。
 * 維護一個模式的資料庫允許你在部署任何內容之前檢查模式更改的向前和向後相容性。
 * 對於靜態型別程式語言的使用者來說,從模式生成程式碼的能力是有用的,因為它可以在編譯時進行型別檢查。
 
-總而言之,模式進化允許與JSON資料庫提供的無模式/讀時模式相同的靈活性(請參閱“[文件模型中的模式靈活性](ch2.md#文件模型中的模式靈活性)”),同時還可以更好地保證資料和更好的工具。
+總而言之,模式進化允許與 JSON 資料庫提供的無模式 / 讀時模式相同的靈活性(請參閱 “[文件模型中的模式靈活性](ch2.md#文件模型中的模式靈活性)”),同時還可以更好地保證資料和更好的工具。
 
 
 ## 資料流的型別
@@ -324,9 +324,9 @@ Avro為靜態型別程式語言提供了可選的程式碼生成功能,但是
 
 這是一個相當抽象的概念 - 資料可以透過多種方式從一個流程流向另一個流程。誰編碼資料,誰解碼?在本章的其餘部分中,我們將探討資料如何在流程之間流動的一些最常見的方式:
 
-* 透過資料庫(請參閱“[資料庫中的資料流](#資料庫中的資料流)”)
-* 透過服務呼叫(請參閱“[服務中的資料流:REST與RPC](#服務中的資料流:REST與RPC)”)
-* 透過非同步訊息傳遞(請參閱“[訊息傳遞中的資料流](#訊息傳遞中的資料流)”)
+* 透過資料庫(請參閱 “[資料庫中的資料流](#資料庫中的資料流)”)
+* 透過服務呼叫(請參閱 “[服務中的資料流:REST 與 RPC](#服務中的資料流:REST與RPC)”)
+* 透過非同步訊息傳遞(請參閱 “[訊息傳遞中的資料流](#訊息傳遞中的資料流)”)
 
 
 ### 資料庫中的資料流
@@ -341,11 +341,11 @@ Avro為靜態型別程式語言提供了可選的程式碼生成功能,但是
 
 但是,還有一個額外的障礙。假設你將一個欄位新增到記錄模式,並且較新的程式碼將該新欄位的值寫入資料庫。隨後,舊版本的程式碼(尚不知道新欄位)將讀取記錄,更新記錄並將其寫回。在這種情況下,理想的行為通常是舊程式碼保持新的欄位不變,即使它不能被解釋。
 
-前面討論的編碼格式支援未知欄位的儲存,但是有時候需要在應用程式層面保持謹慎,如圖4-7所示。例如,如果將資料庫值解碼為應用程式中的模型物件,稍後重新編碼這些模型物件,那麼未知欄位可能會在該翻譯過程中丟失。解決這個問題不是一個難題,你只需要意識到它。
+前面討論的編碼格式支援未知欄位的儲存,但是有時候需要在應用程式層面保持謹慎,如圖 4-7 所示。例如,如果將資料庫值解碼為應用程式中的模型物件,稍後重新編碼這些模型物件,那麼未知欄位可能會在該翻譯過程中丟失。解決這個問題不是一個難題,你只需要意識到它。
 
 ![](../img/fig4-7.png)
 
-**圖4-7 當較舊版本的應用程式更新以前由較新版本的應用程式編寫的資料時,如果不小心,資料可能會丟失。**
+**圖 4-7 當較舊版本的應用程式更新以前由較新版本的應用程式編寫的資料時,如果不小心,資料可能會丟失。**
 
 #### 在不同的時間寫入不同的值
 
@@ -353,147 +353,147 @@ Avro為靜態型別程式語言提供了可選的程式碼生成功能,但是
 
 在部署應用程式的新版本時,也許用不了幾分鐘就可以將所有的舊版本替換為新版本(至少伺服器端應用程式是這樣的)。但資料庫內容並非如此:對於五年前的資料來說,除非對其進行顯式重寫,否則它仍然會以原始編碼形式存在。這種現象有時被概括為:資料的生命週期超出程式碼的生命週期。
 
-將資料重寫(遷移)到一個新的模式當然是可能的,但是在一個大資料集上執行是一個昂貴的事情,所以大多數資料庫如果可能的話就避免它。大多數關係資料庫都允許簡單的模式更改,例如新增一個預設值為空的新列,而不重寫現有資料[^v]。讀取舊行時,對於磁碟上的編碼資料缺少的任何列,資料庫將填充空值。 LinkedIn的文件資料庫Espresso使用Avro儲存,允許它使用Avro的模式演變規則【23】。
+將資料重寫(遷移)到一個新的模式當然是可能的,但是在一個大資料集上執行是一個昂貴的事情,所以大多數資料庫如果可能的話就避免它。大多數關係資料庫都允許簡單的模式更改,例如新增一個預設值為空的新列,而不重寫現有資料 [^v]。讀取舊行時,對於磁碟上的編碼資料缺少的任何列,資料庫將填充空值。 LinkedIn 的文件資料庫 Espresso 使用 Avro 儲存,允許它使用 Avro 的模式演變規則【23】。
 
 因此,模式演變允許整個資料庫看起來好像是用單個模式編碼的,即使底層儲存可能包含用各種歷史版本的模式編碼的記錄。
 
-[^v]: 除了MySQL,即使並非真的必要,它也經常會重寫整個表,正如“[文件模型中的模式靈活性](ch3.md#文件模型中的模式靈活性)”中所提到的。
+[^v]: 除了 MySQL,即使並非真的必要,它也經常會重寫整個表,正如 “[文件模型中的模式靈活性](ch3.md#文件模型中的模式靈活性)” 中所提到的。
 
 
 #### 歸檔儲存
 
-也許你不時為資料庫建立一個快照,例如備份或載入到資料倉庫(請參閱“[資料倉庫](ch3.md#資料倉庫)”)。在這種情況下,即使源資料庫中的原始編碼包含來自不同時代的模式版本的混合,資料轉儲通常也將使用最新模式進行編碼。既然你不管怎樣都要複製資料,那麼你可以對這個資料複製進行一致的編碼。
+也許你不時為資料庫建立一個快照,例如備份或載入到資料倉庫(請參閱 “[資料倉庫](ch3.md#資料倉庫)”)。在這種情況下,即使源資料庫中的原始編碼包含來自不同時代的模式版本的混合,資料轉儲通常也將使用最新模式進行編碼。既然你不管怎樣都要複製資料,那麼你可以對這個資料複製進行一致的編碼。
 
-由於資料轉儲是一次寫入的,而且以後是不可變的,所以Avro物件容器檔案等格式非常適合。這也是一個很好的機會,可以將資料編碼為面向分析的列式格式,例如Parquet(請參閱“[列壓縮](ch3.md#列壓縮)”)。
+由於資料轉儲是一次寫入的,而且以後是不可變的,所以 Avro 物件容器檔案等格式非常適合。這也是一個很好的機會,可以將資料編碼為面向分析的列式格式,例如 Parquet(請參閱 “[列壓縮](ch3.md#列壓縮)”)。
 
-在[第十章](ch10.md)中,我們將詳細討論使用檔案儲存中的資料。
+在 [第十章](ch10.md) 中,我們將詳細討論使用檔案儲存中的資料。
 
 
 ### 服務中的資料流:REST與RPC
 
-當你需要透過網路進行通訊的程序時,安排該通訊的方式有幾種。最常見的安排是有兩個角色:客戶端和伺服器。伺服器透過網路公開API,並且客戶端可以連線到伺服器以向該API發出請求。伺服器公開的API被稱為服務。
+當你需要透過網路進行通訊的程序時,安排該通訊的方式有幾種。最常見的安排是有兩個角色:客戶端和伺服器。伺服器透過網路公開 API,並且客戶端可以連線到伺服器以向該 API 發出請求。伺服器公開的 API 被稱為服務。
 
-Web以這種方式工作:客戶(Web瀏覽器)向Web伺服器發出請求,透過GET請求下載HTML,CSS,JavaScript,影象等,並透過POST請求提交資料到伺服器。 API包含一組標準的協議和資料格式(HTTP,URL,SSL/TLS,HTML等)。由於網路瀏覽器,網路伺服器和網站作者大多同意這些標準,你可以使用任何網路瀏覽器訪問任何網站(至少在理論上!)。
+Web 以這種方式工作:客戶(Web 瀏覽器)向 Web 伺服器發出請求,透過 GET 請求下載 HTML、CSS、JavaScript、影象等,並透過 POST 請求提交資料到伺服器。 API 包含一組標準的協議和資料格式(HTTP、URL、SSL/TLS、HTML 等)。由於網路瀏覽器、網路伺服器和網站作者大多同意這些標準,你可以使用任何網路瀏覽器訪問任何網站(至少在理論上!)。
 
-Web瀏覽器不是唯一的客戶端型別。例如,在移動裝置或桌面計算機上執行的本地應用程式也可以向伺服器發出網路請求,並且在Web瀏覽器內執行的客戶端JavaScript應用程式可以使用XMLHttpRequest成為HTTP客戶端(該技術被稱為Ajax 【30】)。在這種情況下,伺服器的響應通常不是用於顯示給人的HTML,而是便於客戶端應用程式程式碼進一步處理的編碼資料(如JSON)。儘管HTTP可能被用作傳輸協議,但頂層實現的API是特定於應用程式的,客戶端和伺服器需要就該API的細節達成一致。
+Web 瀏覽器不是唯一的客戶端型別。例如,在移動裝置或桌面計算機上執行的本地應用程式也可以向伺服器發出網路請求,並且在 Web 瀏覽器內執行的客戶端 JavaScript 應用程式可以使用 XMLHttpRequest 成為 HTTP 客戶端(該技術被稱為 Ajax 【30】)。在這種情況下,伺服器的響應通常不是用於顯示給人的 HTML,而是便於客戶端應用程式程式碼進一步處理的編碼資料(如 JSON)。儘管 HTTP 可能被用作傳輸協議,但頂層實現的 API 是特定於應用程式的,客戶端和伺服器需要就該 API 的細節達成一致。
 
-此外,伺服器本身可以是另一個服務的客戶端(例如,典型的Web應用伺服器充當資料庫的客戶端)。這種方法通常用於將大型應用程式按照功能區域分解為較小的服務,這樣當一個服務需要來自另一個服務的某些功能或資料時,就會向另一個服務發出請求。這種構建應用程式的方式傳統上被稱為**面向服務的體系結構(service-oriented architecture,SOA)**,最近被改進和更名為**微服務架構**【31,32】。
+此外,伺服器本身可以是另一個服務的客戶端(例如,典型的 Web 應用伺服器充當資料庫的客戶端)。這種方法通常用於將大型應用程式按照功能區域分解為較小的服務,這樣當一個服務需要來自另一個服務的某些功能或資料時,就會向另一個服務發出請求。這種構建應用程式的方式傳統上被稱為 **面向服務的體系結構(service-oriented architecture,SOA)**,最近被改進和更名為 **微服務架構**【31,32】。
 
-在某些方面,服務類似於資料庫:它們通常允許客戶端提交和查詢資料。但是,雖然資料庫允許使用我們在[第二章](ch2.md)中討論的查詢語言進行任意查詢,但是服務公開了一個特定於應用程式的API,它只允許由服務的業務邏輯(應用程式程式碼)預定的輸入和輸出【33】。這種限制提供了一定程度的封裝:服務能夠對客戶可以做什麼和不可以做什麼施加細粒度的限制。
+在某些方面,服務類似於資料庫:它們通常允許客戶端提交和查詢資料。但是,雖然資料庫允許使用我們在 [第二章](ch2.md) 中討論的查詢語言進行任意查詢,但是服務公開了一個特定於應用程式的 API,它只允許由服務的業務邏輯(應用程式程式碼)預定的輸入和輸出【33】。這種限制提供了一定程度的封裝:服務能夠對客戶可以做什麼和不可以做什麼施加細粒度的限制。
 
-面向服務/微服務架構的一個關鍵設計目標是透過使服務獨立部署和演化來使應用程式更易於更改和維護。例如,每個服務應該由一個團隊擁有,並且該團隊應該能夠經常釋出新版本的服務,而不必與其他團隊協調。換句話說,我們應該期望伺服器和客戶端的舊版本和新版本同時執行,因此伺服器和客戶端使用的資料編碼必須在不同版本的服務API之間相容——這正是我們在本章所一直在談論的。
+面向服務 / 微服務架構的一個關鍵設計目標是透過使服務獨立部署和演化來使應用程式更易於更改和維護。例如,每個服務應該由一個團隊擁有,並且該團隊應該能夠經常釋出新版本的服務,而不必與其他團隊協調。換句話說,我們應該期望伺服器和客戶端的舊版本和新版本同時執行,因此伺服器和客戶端使用的資料編碼必須在不同版本的服務 API 之間相容 —— 這正是我們在本章所一直在談論的。
 
 #### Web服務
 
-**當服務使用HTTP作為底層通訊協議時,可稱之為Web服務**。這可能是一個小錯誤,因為Web服務不僅在Web上使用,而且在幾個不同的環境中使用。例如:
+**當服務使用 HTTP 作為底層通訊協議時,可稱之為 Web 服務**。這可能是一個小錯誤,因為 Web 服務不僅在 Web 上使用,而且在幾個不同的環境中使用。例如:
 
-1. 執行在使用者裝置上的客戶端應用程式(例如,移動裝置上的本地應用程式,或使用Ajax的JavaScript web應用程式)透過HTTP向服務發出請求。這些請求通常透過公共網際網路進行。
-2. 一種服務向同一組織擁有的另一項服務提出請求,這些服務通常位於同一資料中心內,作為面向服務/微型架構的一部分。 (支援這種用例的軟體有時被稱為 **中介軟體(middleware)** )
-3. 一種服務透過網際網路向不同組織所擁有的服務提出請求。這用於不同組織後端系統之間的資料交換。此類別包括由線上服務(如信用卡處理系統)提供的公共API,或用於共享訪問使用者資料的OAuth。
+1. 執行在使用者裝置上的客戶端應用程式(例如,移動裝置上的本地應用程式,或使用 Ajax 的 JavaScript web 應用程式)透過 HTTP 向服務發出請求。這些請求通常透過公共網際網路進行。
+2. 一種服務向同一組織擁有的另一項服務提出請求,這些服務通常位於同一資料中心內,作為面向服務 / 微型架構的一部分。 (支援這種用例的軟體有時被稱為 **中介軟體(middleware)** )
+3. 一種服務透過網際網路向不同組織所擁有的服務提出請求。這用於不同組織後端系統之間的資料交換。此類別包括由線上服務(如信用卡處理系統)提供的公共 API,或用於共享訪問使用者資料的 OAuth。
 
-有兩種流行的Web服務方法:REST和SOAP。他們在哲學方面幾乎是截然相反的,往往也是各自支持者之間的激烈辯論的主題[^vi]。
+有兩種流行的 Web 服務方法:REST 和 SOAP。他們在哲學方面幾乎是截然相反的,往往也是各自支持者之間的激烈辯論的主題 [^vi]。
 
 [^vi]: 即使在每個陣營內也有很多爭論。 例如,**HATEOAS(超媒體作為應用程式狀態的引擎)** 就經常引發討論【35】。
 
-REST不是一個協議,而是一個基於HTTP原則的設計哲學【34,35】。它強調簡單的資料格式,使用URL來標識資源,並使用HTTP功能進行快取控制,身份驗證和內容型別協商。與SOAP相比,REST已經越來越受歡迎,至少在跨組織服務整合的背景下【36】,並經常與微服務相關【31】。根據REST原則設計的API稱為RESTful。
+REST 不是一個協議,而是一個基於 HTTP 原則的設計哲學【34,35】。它強調簡單的資料格式,使用 URL 來標識資源,並使用 HTTP 功能進行快取控制,身份驗證和內容型別協商。與 SOAP 相比,REST 已經越來越受歡迎,至少在跨組織服務整合的背景下【36】,並經常與微服務相關【31】。根據 REST 原則設計的 API 稱為 RESTful。
 
-相比之下,SOAP是用於製作網路API請求的基於XML的協議[^vii]。雖然它最常用於HTTP,但其目的是獨立於HTTP,並避免使用大多數HTTP功能。相反,它帶有龐大而複雜的多種相關標準(Web服務框架,稱為`WS-*`),它們增加了各種功能【37】。
+相比之下,SOAP 是用於製作網路 API 請求的基於 XML 的協議 [^vii]。雖然它最常用於 HTTP,但其目的是獨立於 HTTP,並避免使用大多數 HTTP 功能。相反,它帶有龐大而複雜的多種相關標準(Web 服務框架,稱為 `WS-*`),它們增加了各種功能【37】。
 
-[^vii]: 儘管首字母縮寫詞相似,SOAP並不是SOA的要求。 SOAP是一種特殊的技術,而SOA是構建系統的一般方法。
+[^vii]: 儘管首字母縮寫詞相似,SOAP 並不是 SOA 的要求。 SOAP 是一種特殊的技術,而 SOA 是構建系統的一般方法。
 
-SOAP Web服務的API使用稱為Web服務描述語言(WSDL)的基於XML的語言來描述。 WSDL支援程式碼生成,客戶端可以使用本地類和方法呼叫(編碼為XML訊息並由框架再次解碼)訪問遠端服務。這在靜態型別程式語言中非常有用,但在動態型別程式語言中很少(請參閱“[程式碼生成和動態型別的語言](#程式碼生成和動態型別的語言)”)。
+SOAP Web 服務的 API 使用稱為 Web 服務描述語言(WSDL)的基於 XML 的語言來描述。 WSDL 支援程式碼生成,客戶端可以使用本地類和方法呼叫(編碼為 XML 訊息並由框架再次解碼)訪問遠端服務。這在靜態型別程式語言中非常有用,但在動態型別程式語言中很少(請參閱 “[程式碼生成和動態型別的語言](#程式碼生成和動態型別的語言)”)。
 
-由於WSDL的設計不是人類可讀的,而且由於SOAP訊息通常因為過於複雜而無法手動構建,所以SOAP的使用者在很大程度上依賴於工具支援,程式碼生成和IDE【38】。對於SOAP供應商不支援的程式語言的使用者來說,與SOAP服務的整合是困難的。
+由於 WSDL 的設計不是人類可讀的,而且由於 SOAP 訊息通常因為過於複雜而無法手動構建,所以 SOAP 的使用者在很大程度上依賴於工具支援,程式碼生成和 IDE【38】。對於 SOAP 供應商不支援的程式語言的使用者來說,與 SOAP 服務的整合是困難的。
 
-儘管SOAP及其各種擴充套件表面上是標準化的,但是不同廠商的實現之間的互操作性往往會造成問題【39】。由於所有這些原因,儘管許多大型企業仍然使用SOAP,但在大多數小公司中已經不再受到青睞。
+儘管 SOAP 及其各種擴充套件表面上是標準化的,但是不同廠商的實現之間的互操作性往往會造成問題【39】。由於所有這些原因,儘管許多大型企業仍然使用 SOAP,但在大多數小公司中已經不再受到青睞。
 
-REST風格的API傾向於更簡單的方法,通常涉及較少的程式碼生成和自動化工具。定義格式(如OpenAPI,也稱為Swagger 【40】)可用於描述RESTful API並生成文件。
+REST 風格的 API 傾向於更簡單的方法,通常涉及較少的程式碼生成和自動化工具。定義格式(如 OpenAPI,也稱為 Swagger 【40】)可用於描述 RESTful API 並生成文件。
 
 #### 遠端過程呼叫(RPC)的問題
 
-Web服務僅僅是透過網路進行API請求的一系列技術的最新版本,其中許多技術受到了大量的炒作,但是存在嚴重的問題。 Enterprise JavaBeans(EJB)和Java的**遠端方法呼叫(RMI)** 僅限於Java。**分散式元件物件模型(DCOM)** 僅限於Microsoft平臺。**公共物件請求代理體系結構(CORBA)** 過於複雜,不提供前向或後向相容性【41】。
+Web 服務僅僅是透過網路進行 API 請求的一系列技術的最新版本,其中許多技術受到了大量的炒作,但是存在嚴重的問題。 Enterprise JavaBeans(EJB)和 Java 的 **遠端方法呼叫(RMI)** 僅限於 Java。**分散式元件物件模型(DCOM)** 僅限於 Microsoft 平臺。**公共物件請求代理體系結構(CORBA)** 過於複雜,不提供前向或後向相容性【41】。
 
-所有這些都是基於 **遠端過程呼叫(RPC)** 的思想,該過程呼叫自20世紀70年代以來一直存在【42】。 RPC模型試圖向遠端網路服務發出請求,看起來與在同一程序中呼叫程式語言中的函式或方法相同(這種抽象稱為位置透明)。儘管RPC起初看起來很方便,但這種方法根本上是有缺陷的【43,44】。網路請求與本地函式呼叫非常不同:
+所有這些都是基於 **遠端過程呼叫(RPC)** 的思想,該過程呼叫自 20 世紀 70 年代以來一直存在【42】。 RPC 模型試圖向遠端網路服務發出請求,看起來與在同一程序中呼叫程式語言中的函式或方法相同(這種抽象稱為位置透明)。儘管 RPC 起初看起來很方便,但這種方法根本上是有缺陷的【43,44】。網路請求與本地函式呼叫非常不同:
 
 * 本地函式呼叫是可預測的,並且成功或失敗僅取決於受你控制的引數。網路請求是不可預知的:由於網路問題,請求或響應可能會丟失,或者遠端計算機可能很慢或不可用,這些問題完全不在你的控制範圍之內。網路問題是常見的,所以你必須預測他們,例如透過重試失敗的請求。
-* 本地函式呼叫要麼返回結果,要麼丟擲異常,或者永遠不返回(因為進入無限迴圈或程序崩潰)。網路請求有另一個可能的結果:由於超時,它可能會返回沒有結果。在這種情況下,你根本不知道發生了什麼:如果你沒有得到來自遠端服務的響應,你無法知道請求是否透過(我們將在[第八章](ch8.md)更詳細地討論這個問題)。
-* 如果你重試失敗的網路請求,可能會發生請求實際上正在透過,只有響應丟失。在這種情況下,重試將導致該操作被執行多次,除非你在協議中引入去重機制(**冪等**,即idempotence)。本地函式呼叫沒有這個問題。 (在[第十一章](ch11.md)更詳細地討論冪等性)
+* 本地函式呼叫要麼返回結果,要麼丟擲異常,或者永遠不返回(因為進入無限迴圈或程序崩潰)。網路請求有另一個可能的結果:由於超時,它可能會返回沒有結果。在這種情況下,你根本不知道發生了什麼:如果你沒有得到來自遠端服務的響應,你無法知道請求是否透過(我們將在 [第八章](ch8.md) 更詳細地討論這個問題)。
+* 如果你重試失敗的網路請求,可能會發生請求實際上正在透過,只有響應丟失。在這種情況下,重試將導致該操作被執行多次,除非你在協議中引入去重機制(**冪等**,即 idempotence)。本地函式呼叫沒有這個問題。 (在 [第十一章](ch11.md) 更詳細地討論冪等性)
 * 每次呼叫本地功能時,通常需要大致相同的時間來執行。網路請求比函式呼叫要慢得多,而且其延遲也是非常可變的:好的時候它可能會在不到一毫秒的時間內完成,但是當網路擁塞或者遠端服務超載時,可能需要幾秒鐘的時間完成一樣的東西。
 * 呼叫本地函式時,可以高效地將引用(指標)傳遞給本地記憶體中的物件。當你發出一個網路請求時,所有這些引數都需要被編碼成可以透過網路傳送的一系列位元組。如果引數是像數字或字串這樣的基本型別倒是沒關係,但是對於較大的物件很快就會變成問題。
 
-- 客戶端和服務可以用不同的程式語言實現,所以RPC框架必須將資料型別從一種語言翻譯成另一種語言。這可能會捅出大簍子,因為不是所有的語言都具有相同的型別 —— 例如回想一下JavaScript的數字大於$2^{53}$的問題(請參閱“[JSON、XML和二進位制變體](#JSON、XML和二進位制變體)”)。用單一語言編寫的單個程序中不存在此問題。
+- 客戶端和服務可以用不同的程式語言實現,所以 RPC 框架必須將資料型別從一種語言翻譯成另一種語言。這可能會捅出大簍子,因為不是所有的語言都具有相同的型別 —— 例如回想一下 JavaScript 的數字大於 $2^{53}$ 的問題(請參閱 “[JSON、XML 和二進位制變體](#JSON、XML和二進位制變體)”)。用單一語言編寫的單個程序中不存在此問題。
 
-所有這些因素意味著嘗試使遠端服務看起來像程式語言中的本地物件一樣毫無意義,因為這是一個根本不同的事情。 REST的部分吸引力在於,它並不試圖隱藏它是一個網路協議的事實(儘管這似乎並沒有阻止人們在REST之上構建RPC庫)。
+所有這些因素意味著嘗試使遠端服務看起來像程式語言中的本地物件一樣毫無意義,因為這是一個根本不同的事情。 REST 的部分吸引力在於,它並不試圖隱藏它是一個網路協議的事實(儘管這似乎並沒有阻止人們在 REST 之上構建 RPC 庫)。
 
 #### RPC的當前方向
 
-儘管有這樣那樣的問題,RPC不會消失。在本章提到的所有編碼的基礎上構建了各種RPC框架:例如,Thrift和Avro帶有RPC支援,gRPC是使用Protocol Buffers的RPC實現,Finagle也使用Thrift,Rest.li使用JSON over HTTP。
+儘管有這樣那樣的問題,RPC 不會消失。在本章提到的所有編碼的基礎上構建了各種 RPC 框架:例如,Thrift 和 Avro 帶有 RPC 支援,gRPC 是使用 Protocol Buffers 的 RPC 實現,Finagle 也使用 Thrift,Rest.li 使用 JSON over HTTP。
 
-這種新一代的RPC框架更加明確的是,遠端請求與本地函式呼叫不同。例如,Finagle和Rest.li 使用futures(promises)來封裝可能失敗的非同步操作。`Futures`還可以簡化需要並行發出多項服務並將其結果合併的情況【45】。 gRPC支援流,其中一個呼叫不僅包括一個請求和一個響應,還可以是隨時間的一系列請求和響應【46】。
+這種新一代的 RPC 框架更加明確的是,遠端請求與本地函式呼叫不同。例如,Finagle 和 Rest.li 使用 futures(promises)來封裝可能失敗的非同步操作。`Futures` 還可以簡化需要並行發出多項服務並將其結果合併的情況【45】。 gRPC 支援流,其中一個呼叫不僅包括一個請求和一個響應,還可以是隨時間的一系列請求和響應【46】。
 
-其中一些框架還提供服務發現,即允許客戶端找出在哪個IP地址和埠號上可以找到特定的服務。我們將在“[請求路由](ch6.md#請求路由)”中回到這個主題。
+其中一些框架還提供服務發現,即允許客戶端找出在哪個 IP 地址和埠號上可以找到特定的服務。我們將在 “[請求路由](ch6.md#請求路由)” 中回到這個主題。
 
-使用二進位制編碼格式的自定義RPC協議可以實現比通用的JSON over REST更好的效能。但是,RESTful API還有其他一些顯著的優點:方便實驗和除錯(只需使用Web瀏覽器或命令列工具curl,無需任何程式碼生成或軟體安裝即可向其請求),能被所有主流的程式語言和平臺所支援,還有大量可用的工具(伺服器,快取,負載平衡器,代理,防火牆,監控,除錯工具,測試工具等)的生態系統。
+使用二進位制編碼格式的自定義 RPC 協議可以實現比通用的 JSON over REST 更好的效能。但是,RESTful API 還有其他一些顯著的優點:方便實驗和除錯(只需使用 Web 瀏覽器或命令列工具 curl,無需任何程式碼生成或軟體安裝即可向其請求),能被所有主流的程式語言和平臺所支援,還有大量可用的工具(伺服器,快取,負載平衡器,代理,防火牆,監控,除錯工具,測試工具等)的生態系統。
 
-由於這些原因,REST似乎是公共API的主要風格。 RPC框架的主要重點在於同一組織擁有的服務之間的請求,通常在同一資料中心內。
+由於這些原因,REST 似乎是公共 API 的主要風格。 RPC 框架的主要重點在於同一組織擁有的服務之間的請求,通常在同一資料中心內。
 
 #### 資料編碼與RPC的演化
 
-對於可演化性,重要的是可以獨立更改和部署RPC客戶端和伺服器。與透過資料庫流動的資料相比(如上一節所述),我們可以在透過服務進行資料流的情況下做一個簡化的假設:假定所有的伺服器都會先更新,其次是所有的客戶端。因此,你只需要在請求上具有向後相容性,並且對響應具有前向相容性。
+對於可演化性,重要的是可以獨立更改和部署 RPC 客戶端和伺服器。與透過資料庫流動的資料相比(如上一節所述),我們可以在透過服務進行資料流的情況下做一個簡化的假設:假定所有的伺服器都會先更新,其次是所有的客戶端。因此,你只需要在請求上具有向後相容性,並且對響應具有前向相容性。
 
-RPC方案的前後向相容性屬性從它使用的編碼方式中繼承:
+RPC 方案的前後向相容性屬性從它使用的編碼方式中繼承:
 
-* Thrift,gRPC(Protobuf)和Avro RPC可以根據相應編碼格式的相容性規則進行演變。
-* 在SOAP中,請求和響應是使用XML模式指定的。這些可以演變,但有一些微妙的陷阱【47】。
-* RESTful API通常使用JSON(沒有正式指定的模式)用於響應,以及用於請求的JSON或URI編碼/表單編碼的請求引數。新增可選的請求引數並向響應物件新增新的欄位通常被認為是保持相容性的改變。
+* Thrift、gRPC(Protobuf)和 Avro RPC 可以根據相應編碼格式的相容性規則進行演變。
+* 在 SOAP 中,請求和響應是使用 XML 模式指定的。這些可以演變,但有一些微妙的陷阱【47】。
+* RESTful API 通常使用 JSON(沒有正式指定的模式)用於響應,以及用於請求的 JSON 或 URI 編碼 / 表單編碼的請求引數。新增可選的請求引數並向響應物件新增新的欄位通常被認為是保持相容性的改變。
 
-由於RPC經常被用於跨越組織邊界的通訊,所以服務的相容性變得更加困難,因此服務的提供者經常無法控制其客戶,也不能強迫他們升級。因此,需要長期保持相容性,也許是無限期的。如果需要進行相容性更改,則服務提供商通常會並排維護多個版本的服務API。
+由於 RPC 經常被用於跨越組織邊界的通訊,所以服務的相容性變得更加困難,因此服務的提供者經常無法控制其客戶,也不能強迫他們升級。因此,需要長期保持相容性,也許是無限期的。如果需要進行相容性更改,則服務提供商通常會並排維護多個版本的服務 API。
 
-關於API版本化應該如何工作(即,客戶端如何指示它想要使用哪個版本的API)沒有一致意見【48】)。對於RESTful API,常用的方法是在URL或HTTP Accept頭中使用版本號。對於使用API金鑰來標識特定客戶端的服務,另一種選擇是將客戶端請求的API版本儲存在伺服器上,並允許透過單獨的管理介面更新該版本選項【49】。
+關於 API 版本化應該如何工作(即,客戶端如何指示它想要使用哪個版本的 API)沒有一致意見【48】)。對於 RESTful API,常用的方法是在 URL 或 HTTP Accept 頭中使用版本號。對於使用 API 金鑰來標識特定客戶端的服務,另一種選擇是將客戶端請求的 API 版本儲存在伺服器上,並允許透過單獨的管理介面更新該版本選項【49】。
 
 ### 訊息傳遞中的資料流
 
-我們一直在研究從一個過程到另一個過程的編碼資料流的不同方式。到目前為止,我們已經討論了REST和RPC(其中一個程序透過網路向另一個程序傳送請求並期望儘可能快的響應)以及資料庫(一個程序寫入編碼資料,另一個程序在將來再次讀取)。
+我們一直在研究從一個過程到另一個過程的編碼資料流的不同方式。到目前為止,我們已經討論了 REST 和 RPC(其中一個程序透過網路向另一個程序傳送請求並期望儘可能快的響應)以及資料庫(一個程序寫入編碼資料,另一個程序在將來再次讀取)。
 
-在最後一節中,我們將簡要介紹一下RPC和資料庫之間的非同步訊息傳遞系統。它們與RPC類似,因為客戶端的請求(通常稱為訊息)以低延遲傳送到另一個程序。它們與資料庫類似,不是透過直接的網路連線傳送訊息,而是透過稱為訊息代理(也稱為訊息佇列或面向訊息的中介軟體)的中介來臨時儲存訊息。
+在最後一節中,我們將簡要介紹一下 RPC 和資料庫之間的非同步訊息傳遞系統。它們與 RPC 類似,因為客戶端的請求(通常稱為訊息)以低延遲傳送到另一個程序。它們與資料庫類似,不是透過直接的網路連線傳送訊息,而是透過稱為訊息代理(也稱為訊息佇列或面向訊息的中介軟體)的中介來臨時儲存訊息。
 
-與直接RPC相比,使用訊息代理有幾個優點:
+與直接 RPC 相比,使用訊息代理有幾個優點:
 
 * 如果收件人不可用或過載,可以充當緩衝區,從而提高系統的可靠性。
 * 它可以自動將訊息重新發送到已經崩潰的程序,從而防止訊息丟失。
-* 避免發件人需要知道收件人的IP地址和埠號(這在虛擬機器經常出入的雲部署中特別有用)。
+* 避免發件人需要知道收件人的 IP 地址和埠號(這在虛擬機器經常出入的雲部署中特別有用)。
 * 它允許將一條訊息傳送給多個收件人。
 * 將發件人與收件人邏輯分離(發件人只是釋出郵件,不關心使用者)。
 
-然而,與RPC相比,差異在於訊息傳遞通訊通常是單向的:傳送者通常不期望收到其訊息的回覆。一個程序可能傳送一個響應,但這通常是在一個單獨的通道上完成的。這種通訊模式是非同步的:傳送者不會等待訊息被傳遞,而只是傳送它,然後忘記它。
+然而,與 RPC 相比,差異在於訊息傳遞通訊通常是單向的:傳送者通常不期望收到其訊息的回覆。一個程序可能傳送一個響應,但這通常是在一個單獨的通道上完成的。這種通訊模式是非同步的:傳送者不會等待訊息被傳遞,而只是傳送它,然後忘記它。
 
 #### 訊息代理
 
-過去,**訊息代理(Message Broker)** 主要是TIBCO、IBM WebSphere和webMethods等公司的商業軟體的秀場。最近像RabbitMQ、ActiveMQ、HornetQ、NATS和Apache Kafka這樣的開源實現已經流行起來。我們將在[第十一章](ch11.md)中對它們進行更詳細的比較。
+過去,**訊息代理(Message Broker)** 主要是 TIBCO、IBM WebSphere 和 webMethods 等公司的商業軟體的秀場。最近像 RabbitMQ、ActiveMQ、HornetQ、NATS 和 Apache Kafka 這樣的開源實現已經流行起來。我們將在 [第十一章](ch11.md) 中對它們進行更詳細的比較。
 
 詳細的交付語義因實現和配置而異,但通常情況下,訊息代理的使用方式如下:一個程序將訊息傳送到指定的佇列或主題,代理確保將訊息傳遞給那個佇列或主題的一個或多個消費者或訂閱者。在同一主題上可以有許多生產者和許多消費者。
 
-一個主題只提供單向資料流。但是,消費者本身可能會將訊息釋出到另一個主題上(因此,可以將它們連結在一起,就像我們將在[第十一章](ch11.md)中看到的那樣),或者傳送給原始訊息的傳送者使用的回覆佇列(允許請求/響應資料流,類似於RPC)。
+一個主題只提供單向資料流。但是,消費者本身可能會將訊息釋出到另一個主題上(因此,可以將它們連結在一起,就像我們將在 [第十一章](ch11.md) 中看到的那樣),或者傳送給原始訊息的傳送者使用的回覆佇列(允許請求 / 響應資料流,類似於 RPC)。
 
 訊息代理通常不會執行任何特定的資料模型 —— 訊息只是包含一些元資料的位元組序列,因此你可以使用任何編碼格式。如果編碼是向後和向前相容的,你可以靈活地對釋出者和消費者的編碼進行獨立的修改,並以任意順序進行部署。
 
-如果消費者重新發布訊息到另一個主題,則可能需要小心保留未知欄位,以防止前面在資料庫環境中描述的問題([圖4-7](../img/fig4-7.png))。
+如果消費者重新發布訊息到另一個主題,則可能需要小心保留未知欄位,以防止前面在資料庫環境中描述的問題([圖 4-7](../img/fig4-7.png))。
 
 #### 分散式的Actor框架
 
-Actor模型是單個程序中併發的程式設計模型。邏輯被封裝在actor中,而不是直接處理執行緒(以及競爭條件,鎖定和死鎖的相關問題)。每個actor通常代表一個客戶或實體,它可能有一些本地狀態(不與其他任何角色共享),它透過傳送和接收非同步訊息與其他角色通訊。不保證訊息傳送:在某些錯誤情況下,訊息將丟失。由於每個角色一次只能處理一條訊息,因此不需要擔心執行緒,每個角色可以由框架獨立排程。
+Actor 模型是單個程序中併發的程式設計模型。邏輯被封裝在 actor 中,而不是直接處理執行緒(以及競爭條件、鎖定和死鎖的相關問題)。每個 actor 通常代表一個客戶或實體,它可能有一些本地狀態(不與其他任何角色共享),它透過傳送和接收非同步訊息與其他角色通訊。不保證訊息傳送:在某些錯誤情況下,訊息將丟失。由於每個角色一次只能處理一條訊息,因此不需要擔心執行緒,每個角色可以由框架獨立排程。
 
-在分散式Actor框架中,此程式設計模型用於跨多個節點伸縮應用程式。不管傳送方和接收方是在同一個節點上還是在不同的節點上,都使用相同的訊息傳遞機制。如果它們在不同的節點上,則該訊息被透明地編碼成位元組序列,透過網路傳送,並在另一側解碼。
+在分散式 Actor 框架中,此程式設計模型用於跨多個節點伸縮應用程式。不管傳送方和接收方是在同一個節點上還是在不同的節點上,都使用相同的訊息傳遞機制。如果它們在不同的節點上,則該訊息被透明地編碼成位元組序列,透過網路傳送,並在另一側解碼。
 
-位置透明在actor模型中比在RPC中效果更好,因為actor模型已經假定訊息可能會丟失,即使在單個程序中也是如此。儘管網路上的延遲可能比同一個程序中的延遲更高,但是在使用actor模型時,本地和遠端通訊之間的基本不匹配是較少的。
+位置透明在 actor 模型中比在 RPC 中效果更好,因為 actor 模型已經假定訊息可能會丟失,即使在單個程序中也是如此。儘管網路上的延遲可能比同一個程序中的延遲更高,但是在使用 actor 模型時,本地和遠端通訊之間的基本不匹配是較少的。
 
-分散式的Actor框架實質上是將訊息代理和actor程式設計模型整合到一個框架中。但是,如果要執行基於actor的應用程式的滾動升級,則仍然需要擔心向前和向後相容性問題,因為訊息可能會從執行新版本的節點發送到執行舊版本的節點,反之亦然。
+分散式的 Actor 框架實質上是將訊息代理和 actor 程式設計模型整合到一個框架中。但是,如果要執行基於 actor 的應用程式的滾動升級,則仍然需要擔心向前和向後相容性問題,因為訊息可能會從執行新版本的節點發送到執行舊版本的節點,反之亦然。
 
-三個流行的分散式actor框架處理訊息編碼如下:
+三個流行的分散式 actor 框架處理訊息編碼如下:
 
-* 預設情況下,Akka使用Java的內建序列化,不提供前向或後向相容性。 但是,你可以用類似Prototol Buffers的東西替代它,從而獲得滾動升級的能力【50】。
-* Orleans 預設使用不支援滾動升級部署的自定義資料編碼格式; 要部署新版本的應用程式,你需要設定一個新的叢集,將流量從舊叢集遷移到新叢集,然後關閉舊叢集【51,52】。 像Akka一樣,可以使用自定義序列化外掛。
-* 在Erlang OTP中,對記錄模式進行更改是非常困難的(儘管系統具有許多為高可用性設計的功能)。 滾動升級是可能的,但需要仔細計劃【53】。 一個新的實驗性的`maps`資料型別(2014年在Erlang R17中引入的類似於JSON的結構)可能使得這個資料型別在未來更容易【54】。
+* 預設情況下,Akka 使用 Java 的內建序列化,不提供前向或後向相容性。 但是,你可以用類似 Prototol Buffers 的東西替代它,從而獲得滾動升級的能力【50】。
+* Orleans 預設使用不支援滾動升級部署的自定義資料編碼格式;要部署新版本的應用程式,你需要設定一個新的叢集,將流量從舊叢集遷移到新叢集,然後關閉舊叢集【51,52】。 像 Akka 一樣,可以使用自定義序列化外掛。
+* 在 Erlang OTP 中,對記錄模式進行更改是非常困難的(儘管系統具有許多為高可用性設計的功能)。 滾動升級是可能的,但需要仔細計劃【53】。 一個新的實驗性的 `maps` 資料型別(2014 年在 Erlang R17 中引入的類似於 JSON 的結構)可能使得這個資料型別在未來更容易【54】。
 
 
 ## 本章小結
@@ -507,13 +507,13 @@ Actor模型是單個程序中併發的程式設計模型。邏輯被封裝在act
 我們討論了幾種資料編碼格式及其相容性屬性:
 
 * 程式語言特定的編碼僅限於單一程式語言,並且往往無法提供前向和後向相容性。
-* JSON、XML和CSV等文字格式非常普遍,其相容性取決於你如何使用它們。他們有可選的模式語言,這有時是有用的,有時是一個障礙。這些格式對於資料型別有些模糊,所以你必須小心數字和二進位制字串。
-* 像Thrift、Protocol Buffers和Avro這樣的二進位制模式驅動格式允許使用清晰定義的前向和後向相容性語義進行緊湊,高效的編碼。這些模式可以用於靜態型別語言的文件和程式碼生成。但是,他們有一個缺點,就是在資料可讀之前需要對資料進行解碼。
+* JSON、XML 和 CSV 等文字格式非常普遍,其相容性取決於你如何使用它們。他們有可選的模式語言,這有時是有用的,有時是一個障礙。這些格式對於資料型別有些模糊,所以你必須小心數字和二進位制字串。
+* 像 Thrift、Protocol Buffers 和 Avro 這樣的二進位制模式驅動格式允許使用清晰定義的前向和後向相容性語義進行緊湊,高效的編碼。這些模式可以用於靜態型別語言的文件和程式碼生成。但是,他們有一個缺點,就是在資料可讀之前需要對資料進行解碼。
 
 我們還討論了資料流的幾種模式,說明了資料編碼重要性的不同場景:
 
 * 資料庫,寫入資料庫的程序對資料進行編碼,並從資料庫讀取程序對其進行解碼
-* RPC和REST API,客戶端對請求進行編碼,伺服器對請求進行解碼並對響應進行編碼,客戶端最終對響應進行解碼
+* RPC 和 REST API,客戶端對請求進行編碼,伺服器對請求進行解碼並對響應進行編碼,客戶端最終對響應進行解碼
 * 非同步訊息傳遞(使用訊息代理或參與者),其中節點之間透過傳送訊息進行通訊,訊息由傳送者編碼並由接收者解碼
 
 我們可以小心地得出這樣的結論:前向相容性和滾動升級在某種程度上是可以實現的。願你的應用程式的演變迅速、敏捷部署。
diff --git a/zh-tw/ch5.md b/zh-tw/ch5.md
index 9f283ea..1c5ddca 100644
--- a/zh-tw/ch5.md
+++ b/zh-tw/ch5.md
@@ -2,73 +2,73 @@
 
 ![](../img/ch5.png)
 
-> 與可能出錯的東西比,'不可能'出錯的東西最顯著的特點就是:一旦真的出錯,通常就徹底玩完了。
+> 與可能出錯的東西比,“不可能”出錯的東西最顯著的特點就是:一旦真的出錯,通常就徹底玩完了。
 >
-> —— 道格拉斯·亞當斯(1992)
+> —— 道格拉斯・亞當斯(1992)
 
 ------
 
 [TOC]
 
-複製意味著在透過網路連線的多臺機器上保留相同資料的副本。正如在[第二部分](part-ii.md)的介紹中所討論的那樣,我們希望能複製資料,可能出於各種各樣的原因:
+複製意味著在透過網路連線的多臺機器上保留相同資料的副本。正如在 [第二部分](part-ii.md) 的介紹中所討論的那樣,我們希望能複製資料,可能出於各種各樣的原因:
 
 * 使得資料與使用者在地理上接近(從而減少延遲)
 * 即使系統的一部分出現故障,系統也能繼續工作(從而提高可用性)
 * 伸縮可以接受讀請求的機器數量(從而提高讀取吞吐量)
 
-本章將假設你的資料集非常小,每臺機器都可以儲存整個資料集的副本。在[第六章](ch6.md)中將放寬這個假設,討論對單個機器來說太大的資料集的分割(分片)。在後面的章節中,我們將討論複製資料系統中可能發生的各種故障,以及如何處理這些故障。
+本章將假設你的資料集非常小,每臺機器都可以儲存整個資料集的副本。在 [第六章](ch6.md) 中將放寬這個假設,討論對單個機器來說太大的資料集的分割(分片)。在後面的章節中,我們將討論複製資料系統中可能發生的各種故障,以及如何處理這些故障。
 
-如果複製中的資料不會隨時間而改變,那複製就很簡單:將資料複製到每個節點一次就萬事大吉。複製的困難之處在於處理複製資料的**變更(change)**,這就是本章所要講的。我們將討論三種流行的變更復制演算法:**單領導者(single leader)**,**多領導者(multi leader)** 和**無領導者(leaderless)**。幾乎所有分散式資料庫都使用這三種方法之一。
+如果複製中的資料不會隨時間而改變,那複製就很簡單:將資料複製到每個節點一次就萬事大吉。複製的困難之處在於處理複製資料的 **變更(change)**,這就是本章所要講的。我們將討論三種流行的變更復制演算法:**單領導者(single leader)**,**多領導者(multi leader)** 和 **無領導者(leaderless)**。幾乎所有分散式資料庫都使用這三種方法之一。
 
 在複製時需要進行許多權衡:例如,使用同步複製還是非同步複製?如何處理失敗的副本?這些通常是資料庫中的配置選項,細節因資料庫而異,但原理在許多不同的實現中都類似。本章會討論這些決策的後果。
 
-資料庫的複製算得上是老生常談了 ——70年代研究得出的基本原則至今沒有太大變化【1】,因為網路的基本約束仍保持不變。然而在研究之外,許多開發人員仍然假設一個數據庫只有一個節點。分散式資料庫變為主流只是最近發生的事。許多程式設計師都是這一領域的新手,因此對於諸如 **最終一致性(eventual consistency)** 等問題存在許多誤解。在“[複製延遲問題](#複製延遲問題)”一節,我們將更加精確地瞭解最終的一致性,並討論諸如 **讀己之寫(read-your-writes)** 和 **單調讀(monotonic read)** 保證等內容。
+資料庫的複製算得上是老生常談了 ——70 年代研究得出的基本原則至今沒有太大變化【1】,因為網路的基本約束仍保持不變。然而在研究之外,許多開發人員仍然假設一個數據庫只有一個節點。分散式資料庫變為主流只是最近發生的事。許多程式設計師都是這一領域的新手,因此對於諸如 **最終一致性(eventual consistency)** 等問題存在許多誤解。在 “[複製延遲問題](#複製延遲問題)” 一節,我們將更加精確地瞭解最終的一致性,並討論諸如 **讀己之寫(read-your-writes)** 和 **單調讀(monotonic read)** 保證等內容。
 
 ## 領導者與追隨者
 
 儲存資料庫副本的每個節點稱為 **副本(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)** 。當客戶端要向資料庫寫入時,它必須將請求傳送給**領導者**,領導者會將新資料寫入其本地儲存。
-2. 其他副本被稱為**追隨者(followers)**,亦稱為**只讀副本(read replicas)**,**從庫(slaves)**,**備庫( secondaries)**,**熱備(hot-standby)**[^i]。每當領導者將新資料寫入本地儲存時,它也會將資料變更傳送給所有的追隨者,稱之為**複製日誌(replication log)** 記錄或**變更流(change stream)**。每個跟隨者從領導者拉取日誌,並相應更新其本地資料庫副本,方法是按照領導者處理的相同順序應用所有寫入。
+1. 副本之一被指定為 **領導者(leader)**,也稱為 **主庫(master|primary)** 。當客戶端要向資料庫寫入時,它必須將請求傳送給 **領導者**,領導者會將新資料寫入其本地儲存。
+2. 其他副本被稱為 **追隨者(followers)**,亦稱為 **只讀副本(read replicas)**,**從庫(slaves)**,**備庫( secondaries)**,**熱備(hot-standby)**[^i]。每當領導者將新資料寫入本地儲存時,它也會將資料變更傳送給所有的追隨者,稱之為 **複製日誌(replication log)** 記錄或 **變更流(change stream)**。每個跟隨者從領導者拉取日誌,並相應更新其本地資料庫副本,方法是按照領導者處理的相同順序應用所有寫入。
 3. 當客戶想要從資料庫中讀取資料時,它可以向領導者或追隨者查詢。 但只有領導者才能接受寫操作(從客戶端的角度來看從庫都是隻讀的)。
 
-[^i]: 不同的人對 **熱(hot)**,**溫(warm)**,**冷(cold)** 備份伺服器有不同的定義。 例如在PostgreSQL中,**熱備(hot standby)** 指的是能接受客戶端讀請求的副本。而 **溫備(warm standby)** 只是追隨領導者,但不處理客戶端的任何查詢。 就本書而言,這些差異並不重要。
+[^i]: 不同的人對 **熱(hot)**,**溫(warm)**,**冷(cold)** 備份伺服器有不同的定義。 例如在 PostgreSQL 中,**熱備(hot standby)** 指的是能接受客戶端讀請求的副本。而 **溫備(warm standby)** 只是追隨領導者,但不處理客戶端的任何查詢。 就本書而言,這些差異並不重要。
 
 ![](../img/fig5-1.png)
-**圖5-1 基於領導者(主-從)的複製**
+**圖 5-1 基於領導者 (主 - 從) 的複製**
 
-這種複製模式是許多關係資料庫的內建功能,如PostgreSQL(從9.0版本開始),MySQL,Oracle Data Guard 【2】和SQL Server的AlwaysOn可用性組【3】。 它也被用於一些非關係資料庫,包括MongoDB,RethinkDB和Espresso 【4】。 最後,基於領導者的複製並不僅限於資料庫:像Kafka 【5】和RabbitMQ高可用佇列【6】這樣的分散式訊息代理也使用它。 某些網路檔案系統,例如DRBD這樣的塊複製裝置也與之類似。
+這種複製模式是許多關係資料庫的內建功能,如 PostgreSQL(從 9.0 版本開始),MySQL,Oracle Data Guard 【2】和 SQL Server 的 AlwaysOn 可用性組【3】。 它也被用於一些非關係資料庫,包括 MongoDB,RethinkDB 和 Espresso 【4】。 最後,基於領導者的複製並不僅限於資料庫:像 Kafka 【5】和 RabbitMQ 高可用佇列【6】這樣的分散式訊息代理也使用它。 某些網路檔案系統,例如 DRBD 這樣的塊複製裝置也與之類似。
 
 ### 同步複製與非同步複製
 
 複製系統的一個重要細節是:複製是 **同步(synchronously)** 發生還是 **非同步(asynchronously)** 發生。 (在關係型資料庫中這通常是一個配置項,其他系統通常硬編碼為其中一個)。
 
-想象[圖5-1](fig5-1.png)中發生的情況,網站的使用者更新他們的個人頭像。在某個時間點,客戶向主庫傳送更新請求;不久之後主庫就收到了請求。在某個時刻,主庫又會將資料變更轉發給自己的從庫。最後,主庫通知客戶更新成功。
+想象 [圖 5-1](fig5-1.png) 中發生的情況,網站的使用者更新他們的個人頭像。在某個時間點,客戶向主庫傳送更新請求;不久之後主庫就收到了請求。在某個時刻,主庫又會將資料變更轉發給自己的從庫。最後,主庫通知客戶更新成功。
 
-[圖5-2](../img/fig5-2.png)顯示了系統各個元件之間的通訊:使用者客戶端,主庫和兩個從庫。時間從左到右流動。請求或響應訊息用粗箭頭表示。
+[圖 5-2](../img/fig5-2.png) 顯示了系統各個元件之間的通訊:使用者客戶端,主庫和兩個從庫。時間從左到右流動。請求或響應訊息用粗箭頭表示。
 
 ![](../img/fig5-2.png)
-**圖5-2 基於領導者的複製:一個同步從庫和一個非同步從庫**
+**圖 5-2 基於領導者的複製:一個同步從庫和一個非同步從庫**
 
-在[圖5-2](../img/fig5-2.png)的示例中,從庫1的複製是同步的:在向用戶報告寫入成功,並使結果對其他使用者可見之前,主庫需要等待從庫1的確認,確保從庫1已經收到寫入操作。以及在使寫入對其他客戶端可見之前接收到寫入。跟隨者2的複製是非同步的:主庫傳送訊息,但不等待從庫的響應。
+在 [圖 5-2](../img/fig5-2.png) 的示例中,從庫 1 的複製是同步的:在向用戶報告寫入成功,並使結果對其他使用者可見之前,主庫需要等待從庫 1 的確認,確保從庫 1 已經收到寫入操作。以及在使寫入對其他客戶端可見之前接收到寫入。跟隨者 2 的複製是非同步的:主庫傳送訊息,但不等待從庫的響應。
 
-在這幅圖中,從庫2處理訊息前存在一個顯著的延遲。通常情況下,複製的速度相當快:大多數資料庫系統能在一秒向從庫應用變更,但它們不能提供複製用時的保證。有些情況下,從庫可能落後主庫幾分鐘或更久;例如:從庫正在從故障中恢復,系統在最大容量附近執行,或者如果節點間存在網路問題。
+在這幅圖中,從庫 2 處理訊息前存在一個顯著的延遲。通常情況下,複製的速度相當快:大多數資料庫系統能在一秒向從庫應用變更,但它們不能提供複製用時的保證。有些情況下,從庫可能落後主庫幾分鐘或更久;例如:從庫正在從故障中恢復,系統在最大容量附近執行,或者如果節點間存在網路問題。
 
 同步複製的優點是,從庫保證有與主庫一致的最新資料副本。如果主庫突然失效,我們可以確信這些資料仍然能在從庫上上找到。缺點是,如果同步從庫沒有響應(比如它已經崩潰,或者出現網路故障,或其它任何原因),主庫就無法處理寫入操作。主庫必須阻止所有寫入,並等待同步副本再次可用。
 
-因此,將所有從庫都設定為同步的是不切實際的:任何一個節點的中斷都會導致整個系統停滯不前。實際上,如果在資料庫上啟用同步複製,通常意味著其中**一個**跟隨者是同步的,而其他的則是非同步的。如果同步從庫變得不可用或緩慢,則使一個非同步從庫同步。這保證你至少在兩個節點上擁有最新的資料副本:主庫和同步從庫。 這種配置有時也被稱為 **半同步(semi-synchronous)**【7】。
+因此,將所有從庫都設定為同步的是不切實際的:任何一個節點的中斷都會導致整個系統停滯不前。實際上,如果在資料庫上啟用同步複製,通常意味著其中 **一個** 跟隨者是同步的,而其他的則是非同步的。如果同步從庫變得不可用或緩慢,則使一個非同步從庫同步。這保證你至少在兩個節點上擁有最新的資料副本:主庫和同步從庫。 這種配置有時也被稱為 **半同步(semi-synchronous)**【7】。
 
 通常情況下,基於領導者的複製都配置為完全非同步。 在這種情況下,如果主庫失效且不可恢復,則任何尚未複製給從庫的寫入都會丟失。 這意味著即使已經向客戶端確認成功,寫入也不能保證 **持久(Durable)** 。 然而,一個完全非同步的配置也有優點:即使所有的從庫都落後了,主庫也可以繼續處理寫入。
 
-弱化的永續性可能聽起來像是一個壞的折衷,然而非同步複製已經被廣泛使用了,特別當有很多追隨者,或追隨者異地分佈時。 稍後將在“[複製延遲問題](#複製延遲問題)”中回到這個問題。
+弱化的永續性可能聽起來像是一個壞的折衷,然而非同步複製已經被廣泛使用了,特別當有很多追隨者,或追隨者異地分佈時。 稍後將在 “[複製延遲問題](#複製延遲問題)” 中回到這個問題。
 
 > ### 關於複製的研究
 >
-> 對於非同步複製系統而言,主庫故障時有可能丟失資料。這可能是一個嚴重的問題,因此研究人員仍在研究不丟資料但仍能提供良好效能和可用性的複製方法。 例如,**鏈式複製**【8,9】]是同步複製的一種變體,已經在一些系統(如Microsoft Azure儲存【10,11】)中成功實現。
+> 對於非同步複製系統而言,主庫故障時有可能丟失資料。這可能是一個嚴重的問題,因此研究人員仍在研究不丟資料但仍能提供良好效能和可用性的複製方法。 例如,**鏈式複製**【8,9】] 是同步複製的一種變體,已經在一些系統(如 Microsoft Azure 儲存【10,11】)中成功實現。
 >
-> 複製的一致性與**共識**(consensus,使幾個節點就某個值達成一致)之間有著密切的聯絡,[第九章](ch9.md)將詳細地探討這一領域的理論。本章主要討論實踐中資料庫常用的簡單複製形式。
+> 複製的一致性與 **共識**(consensus,使幾個節點就某個值達成一致)之間有著密切的聯絡,[第九章](ch9.md) 將詳細地探討這一領域的理論。本章主要討論實踐中資料庫常用的簡單複製形式。
 >
 
 ### 設定新從庫
@@ -79,9 +79,9 @@
 
 可以透過鎖定資料庫(使其不可用於寫入)來使磁碟上的檔案保持一致,但是這會違背高可用的目標。幸運的是,拉起新的從庫通常並不需要停機。從概念上講,過程如下所示:
 
-1. 在某個時刻獲取主庫的一致性快照(如果可能),而不必鎖定整個資料庫。大多數資料庫都具有這個功能,因為它是備份必需的。對於某些場景,可能需要第三方工具,例如MySQL的innobackupex 【12】。
+1. 在某個時刻獲取主庫的一致性快照(如果可能),而不必鎖定整個資料庫。大多數資料庫都具有這個功能,因為它是備份必需的。對於某些場景,可能需要第三方工具,例如 MySQL 的 innobackupex 【12】。
 2. 將快照複製到新的從庫節點。
-3. 從庫連線到主庫,並拉取快照之後發生的所有資料變更。這要求快照與主庫複製日誌中的位置精確關聯。該位置有不同的名稱:例如,PostgreSQL將其稱為 **日誌序列號(log sequence number, LSN)**,MySQL將其稱為 **二進位制日誌座標(binlog coordinates)**。
+3. 從庫連線到主庫,並拉取快照之後發生的所有資料變更。這要求快照與主庫複製日誌中的位置精確關聯。該位置有不同的名稱:例如,PostgreSQL 將其稱為 **日誌序列號(log sequence number, LSN)**,MySQL 將其稱為 **二進位制日誌座標(binlog coordinates)**。
 4. 當從庫處理完快照之後積壓的資料變更,我們說它 **趕上(caught up)** 了主庫。現在它可以繼續處理主庫產生的資料變化了。
 
 建立從庫的實際步驟因資料庫而異。在某些系統中,這個過程是完全自動化的,而在另外一些系統中,它可能是一個需要由管理員手動執行的,有點神祕的多步驟工作流。
@@ -98,21 +98,21 @@
 
 #### 主庫失效:故障切換
 
-主庫失效處理起來相當棘手:其中一個從庫需要被提升為新的主庫,需要重新配置客戶端,以將它們的寫操作傳送給新的主庫,其他從庫需要開始拉取來自新主庫的資料變更。這個過程被稱為**故障切換(failover)**。
+主庫失效處理起來相當棘手:其中一個從庫需要被提升為新的主庫,需要重新配置客戶端,以將它們的寫操作傳送給新的主庫,其他從庫需要開始拉取來自新主庫的資料變更。這個過程被稱為 **故障切換(failover)**。
 
 故障切換可以手動進行(通知管理員主庫掛了,並採取必要的步驟來建立新的主庫)或自動進行。自動故障切換過程通常由以下步驟組成:
 
-1. 確認主庫失效。有很多事情可能會出錯:崩潰,停電,網路問題等等。沒有萬無一失的方法來檢測出現了什麼問題,所以大多數系統只是簡單使用 **超時(Timeout)** :節點頻繁地相互來回傳遞訊息,並且如果一個節點在一段時間內(例如30秒)沒有響應,就認為它掛了(因為計劃內維護而故意關閉主庫不算)。
-2. 選擇一個新的主庫。這可以透過選舉過程(主庫由剩餘副本以多數選舉產生)來完成,或者可以由之前選定的**控制器節點(controller node)** 來指定新的主庫。主庫的最佳人選通常是擁有舊主庫最新資料副本的從庫(最小化資料損失)。讓所有的節點同意一個新的領導者,是一個**共識**問題,將在[第九章](ch9.md)詳細討論。
-3. 重新配置系統以啟用新的主庫。客戶端現在需要將它們的寫請求傳送給新主庫(將在“[請求路由](ch6.md#請求路由)”中討論這個問題)。如果舊主庫恢復,可能仍然認為自己是主庫,而沒有意識到其他副本已經讓它失去領導權了。系統需要確保舊主庫意識到新主庫的存在,併成為一個從庫。
+1. 確認主庫失效。有很多事情可能會出錯:崩潰,停電,網路問題等等。沒有萬無一失的方法來檢測出現了什麼問題,所以大多數系統只是簡單使用 **超時(Timeout)** :節點頻繁地相互來回傳遞訊息,並且如果一個節點在一段時間內(例如 30 秒)沒有響應,就認為它掛了(因為計劃內維護而故意關閉主庫不算)。
+2. 選擇一個新的主庫。這可以透過選舉過程(主庫由剩餘副本以多數選舉產生)來完成,或者可以由之前選定的 **控制器節點(controller node)** 來指定新的主庫。主庫的最佳人選通常是擁有舊主庫最新資料副本的從庫(最小化資料損失)。讓所有的節點同意一個新的領導者,是一個 **共識** 問題,將在 [第九章](ch9.md) 詳細討論。
+3. 重新配置系統以啟用新的主庫。客戶端現在需要將它們的寫請求傳送給新主庫(將在 “[請求路由](ch6.md#請求路由)” 中討論這個問題)。如果舊主庫恢復,可能仍然認為自己是主庫,而沒有意識到其他副本已經讓它失去領導權了。系統需要確保舊主庫意識到新主庫的存在,併成為一個從庫。
 
 故障切換會出現很多大麻煩:
 
 * 如果使用非同步複製,則新主庫可能沒有收到老主庫宕機前最後的寫入操作。在選出新主庫後,如果老主庫重新加入叢集,新主庫在此期間可能會收到衝突的寫入,那這些寫入該如何處理?最常見的解決方案是簡單丟棄老主庫未複製的寫入,這很可能打破客戶對於資料永續性的期望。
 
-* 如果資料庫需要和其他外部儲存相協調,那麼丟棄寫入內容是極其危險的操作。例如在GitHub 【13】的一場事故中,一個過時的MySQL從庫被提升為主庫。資料庫使用自增ID作為主鍵,因為新主庫的計數器落後於老主庫的計數器,所以新主庫重新分配了一些已經被老主庫分配掉的ID作為主鍵。這些主鍵也在Redis中使用,主鍵重用使得MySQL和Redis中資料產生不一致,最後導致一些私有資料洩漏到錯誤的使用者手中。
+* 如果資料庫需要和其他外部儲存相協調,那麼丟棄寫入內容是極其危險的操作。例如在 GitHub 【13】的一場事故中,一個過時的 MySQL 從庫被提升為主庫。資料庫使用自增 ID 作為主鍵,因為新主庫的計數器落後於老主庫的計數器,所以新主庫重新分配了一些已經被老主庫分配掉的 ID 作為主鍵。這些主鍵也在 Redis 中使用,主鍵重用使得 MySQL 和 Redis 中資料產生不一致,最後導致一些私有資料洩漏到錯誤的使用者手中。
 
-* 發生某些故障時(見[第八章](ch8.md))可能會出現兩個節點都以為自己是主庫的情況。這種情況稱為 **腦裂(split brain)**,非常危險:如果兩個主庫都可以接受寫操作,卻沒有衝突解決機制(請參閱“[多主複製](#多主複製)”),那麼資料就可能丟失或損壞。一些系統採取了安全防範措施:當檢測到兩個主庫節點同時存在時會關閉其中一個節點[^ii],但設計粗糙的機制可能最後會導致兩個節點都被關閉【14】。
+* 發生某些故障時(見 [第八章](ch8.md))可能會出現兩個節點都以為自己是主庫的情況。這種情況稱為 **腦裂 (split brain)**,非常危險:如果兩個主庫都可以接受寫操作,卻沒有衝突解決機制(請參閱 “[多主複製](#多主複製)”),那麼資料就可能丟失或損壞。一些系統採取了安全防範措施:當檢測到兩個主庫節點同時存在時會關閉其中一個節點 [^ii],但設計粗糙的機制可能最後會導致兩個節點都被關閉【14】。
 
   [^ii]: 這種機制稱為 **遮蔽(fencing)**,充滿感情的術語是:**爆彼之頭(Shoot The Other Node In The Head, STONITH)**。
 
@@ -120,7 +120,7 @@
 
 這些問題沒有簡單的解決方案。因此,即使軟體支援自動故障切換,不少運維團隊還是更願意手動執行故障切換。
 
-節點故障、不可靠的網路、對副本一致性,永續性,可用性和延遲的權衡 ,這些問題實際上是分散式系統中的基本問題。[第八章](ch8.md)和[第九章](ch9.md)將更深入地討論它們。
+節點故障、不可靠的網路、對副本一致性,永續性,可用性和延遲的權衡 ,這些問題實際上是分散式系統中的基本問題。[第八章](ch8.md) 和 [第九章](ch9.md) 將更深入地討論它們。
 
 ### 複製日誌的實現
 
@@ -128,32 +128,32 @@
 
 #### 基於語句的複製
 
-在最簡單的情況下,主庫記錄下它執行的每個寫入請求(**語句**,即statement)並將該語句日誌傳送給其從庫。對於關係資料庫來說,這意味著每個`INSERT`、`UPDATE`或`DELETE`語句都被轉發給每個從庫,每個從庫解析並執行該SQL語句,就像從客戶端收到一樣。
+在最簡單的情況下,主庫記錄下它執行的每個寫入請求(**語句**,即 statement)並將該語句日誌傳送給其從庫。對於關係資料庫來說,這意味著每個 `INSERT`、`UPDATE` 或 `DELETE` 語句都被轉發給每個從庫,每個從庫解析並執行該 SQL 語句,就像從客戶端收到一樣。
 
 雖然聽上去很合理,但有很多問題會搞砸這種複製方式:
 
-* 任何呼叫 **非確定性函式(nondeterministic)** 的語句,可能會在每個副本上生成不同的值。例如,使用`NOW()`獲取當前日期時間,或使用`RAND()`獲取一個隨機數。
-* 如果語句使用了**自增列(auto increment)**,或者依賴於資料庫中的現有資料(例如,`UPDATE ... WHERE <某些條件>`),則必須在每個副本上按照完全相同的順序執行它們,否則可能會產生不同的效果。當有多個併發執行的事務時,這可能成為一個限制。
-* 有副作用的語句(例如,觸發器,儲存過程,使用者定義的函式)可能會在每個副本上產生不同的副作用,除非副作用是絕對確定的。
+* 任何呼叫 **非確定性函式(nondeterministic)** 的語句,可能會在每個副本上生成不同的值。例如,使用 `NOW()` 獲取當前日期時間,或使用 `RAND()` 獲取一個隨機數。
+* 如果語句使用了 **自增列(auto increment)**,或者依賴於資料庫中的現有資料(例如,`UPDATE ... WHERE <某些條件>`),則必須在每個副本上按照完全相同的順序執行它們,否則可能會產生不同的效果。當有多個併發執行的事務時,這可能成為一個限制。
+* 有副作用的語句(例如:觸發器、儲存過程、使用者定義的函式)可能會在每個副本上產生不同的副作用,除非副作用是絕對確定的。
 
-的確有辦法繞開這些問題 ——例如,當語句被記錄時,主庫可以用固定的返回值替換任何不確定的函式呼叫,以便從庫獲得相同的值。但是由於邊緣情況實在太多了,現在通常會選擇其他的複製方法。
+的確有辦法繞開這些問題 —— 例如,當語句被記錄時,主庫可以用固定的返回值替換任何不確定的函式呼叫,以便從庫獲得相同的值。但是由於邊緣情況實在太多了,現在通常會選擇其他的複製方法。
 
-基於語句的複製在5.1版本前的MySQL中使用。因為它相當緊湊,現在有時候也還在用。但現在在預設情況下,如果語句中存在任何不確定性,MySQL會切換到基於行的複製(稍後討論)。 VoltDB使用了基於語句的複製,但要求事務必須是確定性的,以此來保證安全【15】。
+基於語句的複製在 5.1 版本前的 MySQL 中使用。因為它相當緊湊,現在有時候也還在用。但現在在預設情況下,如果語句中存在任何不確定性,MySQL 會切換到基於行的複製(稍後討論)。 VoltDB 使用了基於語句的複製,但要求事務必須是確定性的,以此來保證安全【15】。
 
 #### 傳輸預寫式日誌(WAL)
 
-在[第三章](ch3.md)中,我們討論了儲存引擎如何在磁碟上表示資料,並且我們發現,通常寫操作都是追加到日誌中:
+在 [第三章](ch3.md) 中,我們討論了儲存引擎如何在磁碟上表示資料,並且我們發現,通常寫操作都是追加到日誌中:
 
-* 對於日誌結構儲存引擎(請參閱“[SSTables和LSM樹](ch3.md#SSTables和LSM樹)”),日誌是主要的儲存位置。日誌段在後臺壓縮,並進行垃圾回收。
-* 對於覆寫單個磁碟塊的[B樹](ch3.md#B樹),每次修改都會先寫入 **預寫式日誌(Write Ahead Log, WAL)**,以便崩潰後索引可以恢復到一個一致的狀態。
+* 對於日誌結構儲存引擎(請參閱 “[SSTables 和 LSM 樹](ch3.md#SSTables和LSM樹)”),日誌是主要的儲存位置。日誌段在後臺壓縮,並進行垃圾回收。
+* 對於覆寫單個磁碟塊的 [B 樹](ch3.md#B樹),每次修改都會先寫入 **預寫式日誌(Write Ahead Log, WAL)**,以便崩潰後索引可以恢復到一個一致的狀態。
 
 在任何一種情況下,日誌都是包含所有資料庫寫入的僅追加位元組序列。可以使用完全相同的日誌在另一個節點上構建副本:除了將日誌寫入磁碟之外,主庫還可以透過網路將其傳送給其從庫。
 
 當從庫應用這個日誌時,它會建立和主庫一模一樣資料結構的副本。
 
-PostgreSQL和Oracle等使用這種複製方法【16】。主要缺點是日誌記錄的資料非常底層:WAL包含哪些磁碟塊中的哪些位元組發生了更改。這使複製與儲存引擎緊密耦合。如果資料庫將其儲存格式從一個版本更改為另一個版本,通常不可能在主庫和從庫上執行不同版本的資料庫軟體。
+PostgreSQL 和 Oracle 等使用這種複製方法【16】。主要缺點是日誌記錄的資料非常底層:WAL 包含哪些磁碟塊中的哪些位元組發生了更改。這使複製與儲存引擎緊密耦合。如果資料庫將其儲存格式從一個版本更改為另一個版本,通常不可能在主庫和從庫上執行不同版本的資料庫軟體。
 
-看上去這可能只是一個微小的實現細節,但卻可能對運維產生巨大的影響。如果複製協議允許從庫使用比主庫更新的軟體版本,則可以先升級從庫,然後執行故障切換,使升級後的節點之一成為新的主庫,從而執行資料庫軟體的零停機升級。如果複製協議不允許版本不匹配(傳輸WAL經常出現這種情況),則此類升級需要停機。
+看上去這可能只是一個微小的實現細節,但卻可能對運維產生巨大的影響。如果複製協議允許從庫使用比主庫更新的軟體版本,則可以先升級從庫,然後執行故障切換,使升級後的節點之一成為新的主庫,從而執行資料庫軟體的零停機升級。如果複製協議不允許版本不匹配(傳輸 WAL 經常出現這種情況),則此類升級需要停機。
 
 #### 邏輯日誌複製(基於行)
 
@@ -165,36 +165,36 @@ PostgreSQL和Oracle等使用這種複製方法【16】。主要缺點是日誌
 * 對於刪除的行,日誌包含足夠的資訊來唯一標識已刪除的行。通常是主鍵,但是如果表上沒有主鍵,則需要記錄所有列的舊值。
 * 對於更新的行,日誌包含足夠的資訊來唯一標識更新的行,以及所有列的新值(或至少所有已更改的列的新值)。
 
-修改多行的事務會生成多個這樣的日誌記錄,後面跟著一條記錄,指出事務已經提交。 MySQL的二進位制日誌(當配置為使用基於行的複製時)使用這種方法【17】。
+修改多行的事務會生成多個這樣的日誌記錄,後面跟著一條記錄,指出事務已經提交。 MySQL 的二進位制日誌(當配置為使用基於行的複製時)使用這種方法【17】。
 
 由於邏輯日誌與儲存引擎內部分離,因此可以更容易地保持向後相容,從而使領導者和跟隨者能夠執行不同版本的資料庫軟體甚至不同的儲存引擎。
 
-對於外部應用程式來說,邏輯日誌格式也更容易解析。如果要將資料庫的內容傳送到外部系統,這一點很有用,例如複製到資料倉庫進行離線分析,或建立自定義索引和快取【18】。 這種技術被稱為 **資料變更捕獲(change data capture)**,[第十一章](ch11.md)將重新講到它。
+對於外部應用程式來說,邏輯日誌格式也更容易解析。如果要將資料庫的內容傳送到外部系統,這一點很有用,例如複製到資料倉庫進行離線分析,或建立自定義索引和快取【18】。 這種技術被稱為 **資料變更捕獲(change data capture)**,[第十一章](ch11.md) 將重新講到它。
 
 #### 基於觸發器的複製
 
-到目前為止描述的複製方法是由資料庫系統實現的,不涉及任何應用程式程式碼。在很多情況下,這就是你想要的。但在某些情況下需要更多的靈活性。例如,如果你只想複製資料的一個子集,或者想從一種資料庫複製到另一種資料庫,或者如果你需要衝突解決邏輯(請參閱“[處理寫入衝突](#處理寫入衝突)”),則可能需要將複製移動到應用程式層。
+到目前為止描述的複製方法是由資料庫系統實現的,不涉及任何應用程式程式碼。在很多情況下,這就是你想要的。但在某些情況下需要更多的靈活性。例如,如果你只想複製資料的一個子集,或者想從一種資料庫複製到另一種資料庫,或者如果你需要衝突解決邏輯(請參閱 “[處理寫入衝突](#處理寫入衝突)”),則可能需要將複製移動到應用程式層。
 
-一些工具,如Oracle Golden Gate 【19】,可以透過讀取資料庫日誌,使得其他應用程式可以使用資料。另一種方法是使用許多關係資料庫自帶的功能:觸發器和儲存過程。
+一些工具,如 Oracle Golden Gate 【19】,可以透過讀取資料庫日誌,使得其他應用程式可以使用資料。另一種方法是使用許多關係資料庫自帶的功能:觸發器和儲存過程。
 
-觸發器允許你註冊在資料庫系統中發生資料更改(寫入事務)時自動執行的自定義應用程式程式碼。觸發器有機會將更改記錄到一個單獨的表中,使用外部程式讀取這個表,再加上任何業務邏輯處理,會後將資料變更復制到另一個系統去。例如,Databus for Oracle 【20】和Bucardo for Postgres 【21】就是這樣工作的。
+觸發器允許你註冊在資料庫系統中發生資料更改(寫入事務)時自動執行的自定義應用程式程式碼。觸發器有機會將更改記錄到一個單獨的表中,使用外部程式讀取這個表,再加上任何業務邏輯處理,會後將資料變更復制到另一個系統去。例如,Databus for Oracle 【20】和 Bucardo for Postgres 【21】就是這樣工作的。
 
 基於觸發器的複製通常比其他複製方法具有更高的開銷,並且比資料庫的內建複製更容易出錯,也有很多限制。然而由於其靈活性,仍然是很有用的。
 
 
 ## 複製延遲問題
 
-容忍節點故障只是需要複製的一個原因。正如在[第二部分](part-ii.md)的介紹中提到的,另一個原因是可伸縮性(處理比單個機器更多的請求)和延遲(讓副本在地理位置上更接近使用者)。
+容忍節點故障只是需要複製的一個原因。正如在 [第二部分](part-ii.md) 的介紹中提到的,另一個原因是可伸縮性(處理比單個機器更多的請求)和延遲(讓副本在地理位置上更接近使用者)。
 
-基於主庫的複製要求所有寫入都由單個節點處理,但只讀查詢可以由任何副本處理。所以對於讀多寫少的場景(Web上的常見模式),一個有吸引力的選擇是建立很多從庫,並將讀請求分散到所有的從庫上去。這樣能減小主庫的負載,並允許向最近的副本傳送讀請求。
+基於主庫的複製要求所有寫入都由單個節點處理,但只讀查詢可以由任何副本處理。所以對於讀多寫少的場景(Web 上的常見模式),一個有吸引力的選擇是建立很多從庫,並將讀請求分散到所有的從庫上去。這樣能減小主庫的負載,並允許向最近的副本傳送讀請求。
 
-在這種伸縮體系結構中,只需新增更多的追隨者,就可以提高只讀請求的服務容量。但是,這種方法實際上只適用於非同步複製——如果嘗試同步複製到所有追隨者,則單個節點故障或網路中斷將使整個系統無法寫入。而且越多的節點越有可能會被關閉,所以完全同步的配置是非常不可靠的。
+在這種伸縮體系結構中,只需新增更多的追隨者,就可以提高只讀請求的服務容量。但是,這種方法實際上只適用於非同步複製 —— 如果嘗試同步複製到所有追隨者,則單個節點故障或網路中斷將使整個系統無法寫入。而且越多的節點越有可能會被關閉,所以完全同步的配置是非常不可靠的。
 
-不幸的是,當應用程式從非同步從庫讀取時,如果從庫落後,它可能會看到過時的資訊。這會導致資料庫中出現明顯的不一致:同時對主庫和從庫執行相同的查詢,可能得到不同的結果,因為並非所有的寫入都反映在從庫中。這種不一致只是一個暫時的狀態——如果停止寫入資料庫並等待一段時間,從庫最終會趕上並與主庫保持一致。出於這個原因,這種效應被稱為 **最終一致性(eventual consistency)**[^iii]【22,23】
+不幸的是,當應用程式從非同步從庫讀取時,如果從庫落後,它可能會看到過時的資訊。這會導致資料庫中出現明顯的不一致:同時對主庫和從庫執行相同的查詢,可能得到不同的結果,因為並非所有的寫入都反映在從庫中。這種不一致只是一個暫時的狀態 —— 如果停止寫入資料庫並等待一段時間,從庫最終會趕上並與主庫保持一致。出於這個原因,這種效應被稱為 **最終一致性(eventual consistency)**[^iii]【22,23】
 
-[^iii]: 道格拉斯·特里(Douglas Terry)等人創造了術語最終一致性。 【24】 並經由Werner Vogels 【22】推廣,成為許多NoSQL專案的口號。 然而,不只有NoSQL資料庫是最終一致的:關係型資料庫中的非同步複製追隨者也有相同的特性。
+[^iii]: 道格拉斯・特里(Douglas Terry)等人創造了術語最終一致性。 【24】 並經由 Werner Vogels 【22】推廣,成為許多 NoSQL 專案的口號。 然而,不只有 NoSQL 資料庫是最終一致的:關係型資料庫中的非同步複製追隨者也有相同的特性。
 
-“最終”一詞故意含糊不清:總的來說,副本落後的程度是沒有限制的。在正常的操作中,**複製延遲(replication lag)**,即寫入主庫到反映至從庫之間的延遲,可能僅僅是幾分之一秒,在實踐中並不顯眼。但如果系統在接近極限的情況下執行,或網路中存在問題,延遲可以輕而易舉地超過幾秒,甚至幾分鐘。
+“最終” 一詞故意含糊不清:總的來說,副本落後的程度是沒有限制的。在正常的操作中,**複製延遲(replication lag)**,即寫入主庫到反映至從庫之間的延遲,可能僅僅是幾分之一秒,在實踐中並不顯眼。但如果系統在接近極限的情況下執行,或網路中存在問題,延遲可以輕而易舉地超過幾秒,甚至幾分鐘。
 
 因為滯後時間太長引入的不一致性,可不僅是一個理論問題,更是應用設計中會遇到的真實問題。本節將重點介紹三個由複製延遲問題的例子,並簡述解決這些問題的一些方法。
 
@@ -202,27 +202,27 @@ PostgreSQL和Oracle等使用這種複製方法【16】。主要缺點是日誌
 
 許多應用讓使用者提交一些資料,然後檢視他們提交的內容。可能是使用者資料庫中的記錄,也可能是對討論主題的評論,或其他類似的內容。提交新資料時,必須將其傳送給領導者,但是當用戶檢視資料時,可以從追隨者讀取。如果資料經常被檢視,但只是偶爾寫入,這是非常合適的。
 
-但對於非同步複製,問題就來了。如[圖5-3](fig5-3.png)所示:如果使用者在寫入後馬上就檢視資料,則新資料可能尚未到達副本。對使用者而言,看起來好像是剛提交的資料丟失了,使用者會不高興,可以理解。
+但對於非同步複製,問題就來了。如 [圖 5-3](fig5-3.png) 所示:如果使用者在寫入後馬上就檢視資料,則新資料可能尚未到達副本。對使用者而言,看起來好像是剛提交的資料丟失了,使用者會不高興,可以理解。
 
 ![](../img/fig5-3.png)
 
-**圖5-3 使用者寫入後從舊副本中讀取資料。需要寫後讀(read-after-write)的一致性來防止這種異常**
+**圖 5-3 使用者寫入後從舊副本中讀取資料。需要寫後讀 (read-after-write) 的一致性來防止這種異常**
 
 在這種情況下,我們需要 **讀寫一致性(read-after-write consistency)**,也稱為 **讀己之寫一致性(read-your-writes consistency)**【24】。這是一個保證,如果使用者重新載入頁面,他們總會看到他們自己提交的任何更新。它不會對其他使用者的寫入做出承諾:其他使用者的更新可能稍等才會看到。它保證使用者自己的輸入已被正確儲存。
 
 如何在基於領導者的複製系統中實現讀後一致性?有各種可能的技術,這裡說一些:
 
-* 讀使用者**可能已經修改過**的內容時,都從主庫讀;這就要求有一些方法,不用實際查詢就可以知道使用者是否修改了某些東西。舉個例子,社交網路上的使用者個人資料資訊通常只能由使用者本人編輯,而不能由其他人編輯。因此一個簡單的規則是:從主庫讀取使用者自己的檔案,在從庫讀取其他使用者的檔案。
+* 讀使用者 **可能已經修改過** 的內容時,都從主庫讀;這就要求有一些方法,不用實際查詢就可以知道使用者是否修改了某些東西。舉個例子,社交網路上的使用者個人資料資訊通常只能由使用者本人編輯,而不能由其他人編輯。因此一個簡單的規則是:從主庫讀取使用者自己的檔案,在從庫讀取其他使用者的檔案。
 
 * 如果應用中的大部分內容都可能被使用者編輯,那這種方法就沒用了,因為大部分內容都必須從主庫讀取(擴容讀就沒效果了)。在這種情況下可以使用其他標準來決定是否從主庫讀取。例如可以跟蹤上次更新的時間,在上次更新後的一分鐘內,從主庫讀。還可以監控從庫的複製延遲,防止對任意比主庫滯後超過一分鐘的從庫發出查詢。
 
 * 客戶端可以記住最近一次寫入的時間戳,系統需要確保從庫為該使用者提供任何查詢時,該時間戳前的變更都已經傳播到了本從庫中。如果當前從庫不夠新,則可以從另一個從庫讀,或者等待從庫追趕上來。
 
-  時間戳可以是邏輯時間戳(指示寫入順序的東西,例如日誌序列號)或實際系統時鐘(在這種情況下,時鐘同步變得至關重要;請參閱“[不可靠的時鐘](ch8.md#不可靠的時鐘)”)。
+  時間戳可以是邏輯時間戳(指示寫入順序的東西,例如日誌序列號)或實際系統時鐘(在這種情況下,時鐘同步變得至關重要;請參閱 “[不可靠的時鐘](ch8.md#不可靠的時鐘)”)。
 
 * 如果你的副本分佈在多個數據中心(出於可用性目的與使用者儘量在地理上接近),則會增加複雜性。任何需要由領導者提供服務的請求都必須路由到包含主庫的資料中心。
 
-另一種複雜的情況是:如果同一個使用者從多個裝置請求服務,例如桌面瀏覽器和移動APP。這種情況下可能就需要提供跨裝置的寫後讀一致性:如果使用者在某個裝置上輸入了一些資訊,然後在另一個裝置上檢視,則應該看到他們剛輸入的資訊。
+另一種複雜的情況是:如果同一個使用者從多個裝置請求服務,例如桌面瀏覽器和移動 APP。這種情況下可能就需要提供跨裝置的寫後讀一致性:如果使用者在某個裝置上輸入了一些資訊,然後在另一個裝置上檢視,則應該看到他們剛輸入的資訊。
 
 在這種情況下,還有一些需要考慮的問題:
 
@@ -234,20 +234,20 @@ PostgreSQL和Oracle等使用這種複製方法【16】。主要缺點是日誌
 
 從非同步從庫讀取第二個異常例子是,使用者可能會遇到 **時光倒流(moving backward in time)**。
 
-如果使用者從不同從庫進行多次讀取,就可能發生這種情況。例如,[圖5-4](../img/fig5-4.png)顯示了使用者2345兩次進行相同的查詢,首先查詢了一個延遲很小的從庫,然後是一個延遲較大的從庫(如果使用者重新整理網頁,而每個請求被路由到一個隨機的伺服器,這種情況是很有可能的)。第一個查詢返回最近由使用者1234新增的評論,但是第二個查詢不返回任何東西,因為滯後的從庫還沒有拉取寫入內容。在效果上相比第一個查詢,第二個查詢是在更早的時間點來觀察系統。如果第一個查詢沒有返回任何內容,那問題並不大,因為使用者2345可能不知道使用者1234最近添加了評論。但如果使用者2345先看見使用者1234的評論,然後又看到它消失,那麼對於使用者2345,就很讓人頭大了。
+如果使用者從不同從庫進行多次讀取,就可能發生這種情況。例如,[圖 5-4](../img/fig5-4.png) 顯示了使用者 2345 兩次進行相同的查詢,首先查詢了一個延遲很小的從庫,然後是一個延遲較大的從庫(如果使用者重新整理網頁,而每個請求被路由到一個隨機的伺服器,這種情況是很有可能的)。第一個查詢返回最近由使用者 1234 新增的評論,但是第二個查詢不返回任何東西,因為滯後的從庫還沒有拉取寫入內容。在效果上相比第一個查詢,第二個查詢是在更早的時間點來觀察系統。如果第一個查詢沒有返回任何內容,那問題並不大,因為使用者 2345 可能不知道使用者 1234 最近添加了評論。但如果使用者 2345 先看見使用者 1234 的評論,然後又看到它消失,那麼對於使用者 2345,就很讓人頭大了。
 
 ![](../img/fig5-4.png)
 
-**圖5-4 使用者首先從新副本讀取,然後從舊副本讀取。時光倒流。為了防止這種異常,我們需要單調的讀取。**
+**圖 5-4 使用者首先從新副本讀取,然後從舊副本讀取。時光倒流。為了防止這種異常,我們需要單調的讀取。**
 
 **單調讀(Monotonic reads)**【23】保證這種異常不會發生。這是一個比 **強一致性(strong consistency)** 更弱,但比 **最終一致性(eventual consistency)** 更強的保證。當讀取資料時,你可能會看到一箇舊值;單調讀取僅意味著如果一個使用者順序地進行多次讀取,則他們不會看到時間後退,即,如果先前讀取到較新的資料,後續讀取不會得到更舊的資料。
 
-實現單調讀取的一種方式是確保每個使用者總是從同一個副本進行讀取(不同的使用者可以從不同的副本讀取)。例如,可以基於使用者ID的雜湊來選擇副本,而不是隨機選擇副本。但是,如果該副本失敗,使用者的查詢將需要重新路由到另一個副本。
+實現單調讀取的一種方式是確保每個使用者總是從同一個副本進行讀取(不同的使用者可以從不同的副本讀取)。例如,可以基於使用者 ID 的雜湊來選擇副本,而不是隨機選擇副本。但是,如果該副本失敗,使用者的查詢將需要重新路由到另一個副本。
 
 
 ### 一致字首讀
 
-第三個複製延遲例子違反了因果律。 想象一下Poons先生和Cake夫人之間的以下簡短對話:
+第三個複製延遲例子違反了因果律。 想象一下 Poons 先生和 Cake 夫人之間的以下簡短對話:
 
 *Mr. Poons*
 > Mrs. Cake,你能看到多遠的未來?
@@ -255,9 +255,9 @@ PostgreSQL和Oracle等使用這種複製方法【16】。主要缺點是日誌
 *Mrs. Cake*
 > 通常約十秒鐘,Mr. Poons.
 
-這兩句話之間有因果關係:Cake夫人聽到了Poons先生的問題並回答了這個問題。
+這兩句話之間有因果關係:Cake 夫人聽到了 Poons 先生的問題並回答了這個問題。
 
-現在,想象第三個人正在透過從庫來聽這個對話。 Cake夫人說的內容是從一個延遲很低的從庫讀取的,但Poons先生所說的內容,從庫的延遲要大的多(見[圖5-5](../img/fig5-5.png))。 於是,這個觀察者會聽到以下內容:
+現在,想象第三個人正在透過從庫來聽這個對話。 Cake 夫人說的內容是從一個延遲很低的從庫讀取的,但 Poons 先生所說的內容,從庫的延遲要大的多(見 [圖 5-5](../img/fig5-5.png))。 於是,這個觀察者會聽到以下內容:
 
 *Mrs. Cake*
 > 通常約十秒鐘,Mr. Poons.
@@ -265,26 +265,26 @@ PostgreSQL和Oracle等使用這種複製方法【16】。主要缺點是日誌
 *Mr. Poons*
 > Mrs. Cake,你能看到多遠的未來?
 
-對於觀察者來說,看起來好像Cake夫人在Poons先生提問前就回答了這個問題。
+對於觀察者來說,看起來好像 Cake 夫人在 Poons 先生提問前就回答了這個問題。
  這種超能力讓人印象深刻,但也會把人搞糊塗。【25】。
 
 ![](../img/fig5-5.png)
 
-**圖5-5 如果某些分割槽的複製速度慢於其他分割槽,那麼觀察者在看到問題之前可能會看到答案。**
+**圖 5-5 如果某些分割槽的複製速度慢於其他分割槽,那麼觀察者在看到問題之前可能會看到答案。**
 
 防止這種異常,需要另一種型別的保證:**一致字首讀(consistent prefix reads)**【23】。 這個保證說:如果一系列寫入按某個順序發生,那麼任何人讀取這些寫入時,也會看見它們以同樣的順序出現。
 
-這是**分割槽(partitioned)** 或 **分片(sharded)** 資料庫中的一個特殊問題,將在[第六章](ch6.md)中討論。如果資料庫總是以相同的順序應用寫入,則讀取總是會看到一致的字首,所以這種異常不會發生。但是在許多分散式資料庫中,不同的分割槽獨立執行,因此不存在**全域性寫入順序**:當用戶從資料庫中讀取資料時,可能會看到資料庫的某些部分處於較舊的狀態,而某些處於較新的狀態。
+這是 **分割槽(partitioned)** 或 **分片(sharded)** 資料庫中的一個特殊問題,將在 [第六章](ch6.md) 中討論。如果資料庫總是以相同的順序應用寫入,則讀取總是會看到一致的字首,所以這種異常不會發生。但是在許多分散式資料庫中,不同的分割槽獨立執行,因此不存在 **全域性寫入順序**:當用戶從資料庫中讀取資料時,可能會看到資料庫的某些部分處於較舊的狀態,而某些處於較新的狀態。
 
-一種解決方案是,確保任何因果相關的寫入都寫入相同的分割槽。對於某些無法高效完成這種操作的應用,還有一些顯式跟蹤因果依賴關係的演算法,本書將在“[“此前發生”的關係和併發](#“此前發生”的關係和併發)”一節中返回這個主題。
+一種解決方案是,確保任何因果相關的寫入都寫入相同的分割槽。對於某些無法高效完成這種操作的應用,還有一些顯式跟蹤因果依賴關係的演算法,本書將在 “[“此前發生” 的關係和併發](#“此前發生”的關係和併發)” 一節中返回這個主題。
 
 ### 複製延遲的解決方案
 
-在使用最終一致的系統時,如果複製延遲增加到幾分鐘甚至幾小時,則應該考慮應用程式的行為。如果答案是“沒問題”,那很好。但如果結果對於使用者來說是不好體驗,那麼設計系統來提供更強的保證是很重要的,例如**寫後讀**。明明是非同步複製卻假設複製是同步的,這是很多麻煩的根源。
+在使用最終一致的系統時,如果複製延遲增加到幾分鐘甚至幾小時,則應該考慮應用程式的行為。如果答案是 “沒問題”,那很好。但如果結果對於使用者來說是不好體驗,那麼設計系統來提供更強的保證是很重要的,例如 **寫後讀**。明明是非同步複製卻假設複製是同步的,這是很多麻煩的根源。
 
 如前所述,應用程式可以提供比底層資料庫更強有力的保證,例如透過主庫進行某種讀取。但在應用程式程式碼中處理這些問題是複雜的,容易出錯。
 
-如果應用程式開發人員不必擔心微妙的複製問題,並可以信賴他們的資料庫“做了正確的事情”,那該多好呀。這就是 **事務(transaction)** 存在的原因:**資料庫透過事務提供強大的保證**,所以應用程式可以更加簡單。
+如果應用程式開發人員不必擔心微妙的複製問題,並可以信賴他們的資料庫 “做了正確的事情”,那該多好呀。這就是 **事務(transaction)** 存在的原因:**資料庫透過事務提供強大的保證**,所以應用程式可以更加簡單。
 
 單節點事務已經存在了很長時間。然而在走向分散式(複製和分割槽)資料庫時,許多系統放棄了事務。聲稱事務在效能和可用性上的代價太高,並斷言在可伸縮系統中最終一致性是不可避免的。這個敘述有一些道理,但過於簡單了,本書其餘部分將提出更為細緻的觀點。第七章和第九章將回到事務的話題,並討論一些替代機制。
 
@@ -293,11 +293,11 @@ PostgreSQL和Oracle等使用這種複製方法【16】。主要缺點是日誌
 
 本章到目前為止,我們只考慮使用單個領導者的複製架構。 雖然這是一種常見的方法,但也有一些有趣的選擇。
 
-基於領導者的複製有一個主要的缺點:只有一個主庫,而所有的寫入都必須透過它[^iv]。如果出於任何原因(例如和主庫之間的網路連線中斷)無法連線到主庫, 就無法向資料庫寫入。
+基於領導者的複製有一個主要的缺點:只有一個主庫,而所有的寫入都必須透過它 [^iv]。如果出於任何原因(例如和主庫之間的網路連線中斷)無法連線到主庫, 就無法向資料庫寫入。
 
-[^iv]: 如果資料庫被分割槽(見[第六章](ch6.md)),每個分割槽都有一個領導。 不同的分割槽可能在不同的節點上有其領導者,但是每個分割槽必須有一個領導者節點。
+[^iv]: 如果資料庫被分割槽(見 [第六章](ch6.md)),每個分割槽都有一個領導。 不同的分割槽可能在不同的節點上有其領導者,但是每個分割槽必須有一個領導者節點。
 
-基於領導者的複製模型的自然延伸是允許多個節點接受寫入。 複製仍然以同樣的方式發生:處理寫入的每個節點都必須將該資料更改轉發給所有其他節點。 稱之為**多領導者配置**(也稱多主、多活複製)。 在這種情況下,每個領導者同時扮演其他領導者的追隨者。
+基於領導者的複製模型的自然延伸是允許多個節點接受寫入。 複製仍然以同樣的方式發生:處理寫入的每個節點都必須將該資料更改轉發給所有其他節點。 稱之為 **多領導者配置**(也稱多主、多活複製)。 在這種情況下,每個領導者同時扮演其他領導者的追隨者。
 
 ### 多主複製的應用場景
 
@@ -307,11 +307,11 @@ PostgreSQL和Oracle等使用這種複製方法【16】。主要缺點是日誌
 
 假如你有一個數據庫,副本分散在好幾個不同的資料中心(也許這樣可以容忍單個數據中心的故障,或地理上更接近使用者)。 使用常規的基於領導者的複製設定,主庫必須位於其中一個數據中心,且所有寫入都必須經過該資料中心。
 
-多領導者配置中可以在每個資料中心都有主庫。 [圖5-6](../img/fig5-6.png)展示了這個架構的樣子。 在每個資料中心內使用常規的主從複製;在資料中心之間,每個資料中心的主庫都會將其更改複製到其他資料中心的主庫中。
+多領導者配置中可以在每個資料中心都有主庫。 [圖 5-6](../img/fig5-6.png) 展示了這個架構的樣子。 在每個資料中心內使用常規的主從複製;在資料中心之間,每個資料中心的主庫都會將其更改複製到其他資料中心的主庫中。
 
 ![](../img/fig5-6.png)
 
-**圖5-6 跨多個數據中心的多主複製**
+**圖 5-6 跨多個數據中心的多主複製**
 
 我們來比較一下在運維多個數據中心時,單主和多主的適應情況。
 
@@ -327,9 +327,9 @@ PostgreSQL和Oracle等使用這種複製方法【16】。主要缺點是日誌
 
   資料中心之間的通訊通常穿過公共網際網路,這可能不如資料中心內的本地網路可靠。單主配置對這資料中心間的連線問題非常敏感,因為透過這個連線進行的寫操作是同步的。採用非同步複製功能的多主配置通常能更好地承受網路問題:臨時的網路中斷並不會妨礙正在處理的寫入。
 
-有些資料庫預設情況下支援多主配置,但使用外部工具實現也很常見,例如用於MySQL的Tungsten Replicator 【26】,用於PostgreSQL的BDR【27】以及用於Oracle的GoldenGate 【19】。
+有些資料庫預設情況下支援多主配置,但使用外部工具實現也很常見,例如用於 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】。
 
@@ -341,13 +341,13 @@ PostgreSQL和Oracle等使用這種複製方法【16】。主要缺點是日誌
 
 在這種情況下,每個裝置都有一個充當領導者的本地資料庫(它接受寫請求),並且在所有裝置上的日曆副本之間同步時,存在非同步的多主複製過程。複製延遲可能是幾小時甚至幾天,具體取決於何時可以訪問網際網路。
 
-從架構的角度來看,這種設定實際上與資料中心之間的多領導者複製類似,每個裝置都是一個“資料中心”,而它們之間的網路連線是極度不可靠的。從歷史上各類日曆同步功能的破爛實現可以看出,想把多活配好是多麼困難的一件事。
+從架構的角度來看,這種設定實際上與資料中心之間的多領導者複製類似,每個裝置都是一個 “資料中心”,而它們之間的網路連線是極度不可靠的。從歷史上各類日曆同步功能的破爛實現可以看出,想把多活配好是多麼困難的一件事。
 
-有一些工具旨在使這種多領導者配置更容易。例如,CouchDB就是為這種操作模式而設計的【29】。
+有一些工具旨在使這種多領導者配置更容易。例如,CouchDB 就是為這種操作模式而設計的【29】。
 
 #### 協同編輯
 
-實時協作編輯應用程式允許多個人同時編輯文件。例如,Etherpad 【30】和Google Docs 【31】允許多人同時編輯文字文件或電子表格(該演算法在“[自動衝突解決](#自動衝突解決)”中簡要討論)。我們通常不會將協作式編輯視為資料庫複製問題,但與前面提到的離線編輯用例有許多相似之處。當一個使用者編輯文件時,所做的更改將立即應用到其本地副本(Web瀏覽器或客戶端應用程式中的文件狀態),並非同步複製到伺服器和編輯同一文件的任何其他使用者。
+實時協作編輯應用程式允許多個人同時編輯文件。例如,Etherpad 【30】和 Google Docs 【31】允許多人同時編輯文字文件或電子表格(該演算法在 “[自動衝突解決](#自動衝突解決)” 中簡要討論)。我們通常不會將協作式編輯視為資料庫複製問題,但與前面提到的離線編輯用例有許多相似之處。當一個使用者編輯文件時,所做的更改將立即應用到其本地副本(Web 瀏覽器或客戶端應用程式中的文件狀態),並非同步複製到伺服器和編輯同一文件的任何其他使用者。
 
 如果要保證不會發生編輯衝突,則應用程式必須先取得文件的鎖定,然後使用者才能對其進行編輯。如果另一個使用者想要編輯同一個文件,他們首先必須等到第一個使用者提交修改並釋放鎖定。這種協作模式相當於主從複製模型下在主節點上執行事務操作。
 
@@ -357,11 +357,11 @@ PostgreSQL和Oracle等使用這種複製方法【16】。主要缺點是日誌
 
 多領導者複製的最大問題是可能發生寫衝突,這意味著需要解決衝突。
 
-例如,考慮一個由兩個使用者同時編輯的維基頁面,如[圖5-7](../img/fig5-7.png)所示。使用者1將頁面的標題從A更改為B,並且使用者2同時將標題從A更改為C。每個使用者的更改已成功應用到其本地主庫。但當非同步複製時,會發現衝突【33】。單主資料庫中不會出現此問題。
+例如,考慮一個由兩個使用者同時編輯的維基頁面,如 [圖 5-7](../img/fig5-7.png) 所示。使用者 1 將頁面的標題從 A 更改為 B,並且使用者 2 同時將標題從 A 更改為 C。每個使用者的更改已成功應用到其本地主庫。但當非同步複製時,會發現衝突【33】。單主資料庫中不會出現此問題。
 
 ![](../img/fig5-7.png)
 
-**圖5-7 兩個主庫同時更新同一記錄引起的寫入衝突**
+**圖 5-7 兩個主庫同時更新同一記錄引起的寫入衝突**
 
 #### 同步與非同步衝突檢測
 
@@ -373,23 +373,23 @@ PostgreSQL和Oracle等使用這種複製方法【16】。主要缺點是日誌
 
 處理衝突的最簡單的策略就是避免它們:如果應用程式可以確保特定記錄的所有寫入都透過同一個領導者,那麼衝突就不會發生。由於許多的多領導者複製實現在處理衝突時處理得相當不好,避免衝突是一個經常推薦的方法【34】。
 
-例如,在使用者可以編輯自己的資料的應用程式中,可以確保來自特定使用者的請求始終路由到同一資料中心,並使用該資料中心的領導者進行讀寫。不同的使用者可能有不同的“家庭”資料中心(可能根據使用者的地理位置選擇),但從任何使用者的角度來看,配置基本上都是單一的領導者。
+例如,在使用者可以編輯自己的資料的應用程式中,可以確保來自特定使用者的請求始終路由到同一資料中心,並使用該資料中心的領導者進行讀寫。不同的使用者可能有不同的 “家庭” 資料中心(可能根據使用者的地理位置選擇),但從任何使用者的角度來看,配置基本上都是單一的領導者。
 
-但是,有時你可能需要更改指定的記錄的主庫——可能是因為一個數據中心出現故障,你需要將流量重新路由到另一個數據中心,或者可能是因為使用者已經遷移到另一個位置,現在更接近不同的資料中心。在這種情況下,衝突避免會中斷,你必須處理不同主庫同時寫入的可能性。
+但是,有時你可能需要更改指定的記錄的主庫 —— 可能是因為一個數據中心出現故障,你需要將流量重新路由到另一個數據中心,或者可能是因為使用者已經遷移到另一個位置,現在更接近不同的資料中心。在這種情況下,衝突避免會中斷,你必須處理不同主庫同時寫入的可能性。
 
 #### 收斂至一致的狀態
 
 單主資料庫按順序進行寫操作:如果同一個欄位有多個更新,則最後一個寫操作將決定該欄位的最終值。
 
-在多主配置中,沒有明確的寫入順序,所以最終值應該是什麼並不清楚。在[圖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)** 的方式解決衝突,這意味著所有副本必須在所有變更復制完成時收斂至一個相同的最終值。
 
 實現衝突合併解決有多種途徑:
 
-* 給每個寫入一個唯一的ID(例如,一個時間戳,一個長的隨機數,一個UUID或者一個鍵和值的雜湊),挑選最高ID的寫入作為勝利者,並丟棄其他寫入。如果使用時間戳,這種技術被稱為**最後寫入勝利(LWW, last write wins)**。雖然這種方法很流行,但是很容易造成資料丟失【35】。我們將在本章末尾的[檢測併發寫入](#檢測併發寫入)更詳細地討論LWW。
-* 為每個副本分配一個唯一的ID,ID編號更高的寫入具有更高的優先順序。這種方法也意味著資料丟失。
-* 以某種方式將這些值合併在一起 - 例如,按字母順序排序,然後連線它們(在[圖5-7](../img/fig5-7.png)中,合併的標題可能類似於“B/C”)。
+* 給每個寫入一個唯一的 ID(例如,一個時間戳,一個長的隨機數,一個 UUID 或者一個鍵和值的雜湊),挑選最高 ID 的寫入作為勝利者,並丟棄其他寫入。如果使用時間戳,這種技術被稱為 **最後寫入勝利(LWW, last write wins)**。雖然這種方法很流行,但是很容易造成資料丟失【35】。我們將在本章末尾的 [檢測併發寫入](#檢測併發寫入) 更詳細地討論 LWW。
+* 為每個副本分配一個唯一的 ID,ID 編號更高的寫入具有更高的優先順序。這種方法也意味著資料丟失。
+* 以某種方式將這些值合併在一起 - 例如,按字母順序排序,然後連線它們(在 [圖 5-7](../img/fig5-7.png) 中,合併的標題可能類似於 “B/C”)。
 * 用一種可保留所有資訊的顯式資料結構來記錄衝突,並編寫解決衝突的應用程式程式碼(也許透過提示使用者的方式)。
 
 
@@ -399,13 +399,13 @@ PostgreSQL和Oracle等使用這種複製方法【16】。主要缺點是日誌
 
 * 寫時執行
 
-  只要資料庫系統檢測到複製更改日誌中存在衝突,就會呼叫衝突處理程式。例如,Bucardo允許你為此編寫一段Perl程式碼。這個處理程式通常不能提示使用者——它在後臺程序中執行,並且必須快速執行。
+  只要資料庫系統檢測到複製更改日誌中存在衝突,就會呼叫衝突處理程式。例如,Bucardo 允許你為此編寫一段 Perl 程式碼。這個處理程式通常不能提示使用者 —— 它在後臺程序中執行,並且必須快速執行。
 
 * 讀時執行
 
-  當檢測到衝突時,所有衝突寫入被儲存。下一次讀取資料時,會將這些多個版本的資料返回給應用程式。應用程式可能會提示使用者或自動解決衝突,並將結果寫回資料庫。例如,CouchDB以這種方式工作。
+  當檢測到衝突時,所有衝突寫入被儲存。下一次讀取資料時,會將這些多個版本的資料返回給應用程式。應用程式可能會提示使用者或自動解決衝突,並將結果寫回資料庫。例如,CouchDB 以這種方式工作。
 
-請注意,衝突解決通常適用於單個行或文件層面,而不是整個事務【36】。因此,如果你有一個事務會原子性地進行幾次不同的寫入(請參閱[第七章](ch7.md),對於衝突解決而言,每個寫入仍需分開單獨考慮。
+請注意,衝突解決通常適用於單個行或文件層面,而不是整個事務【36】。因此,如果你有一個事務會原子性地進行幾次不同的寫入(請參閱 [第七章](ch7.md),對於衝突解決而言,每個寫入仍需分開單獨考慮。
 
 
 > #### 自動衝突解決
@@ -414,9 +414,9 @@ PostgreSQL和Oracle等使用這種複製方法【16】。主要缺點是日誌
 >
 > 已經有一些有趣的研究來自動解決由於資料修改引起的衝突。有幾行研究值得一提:
 >
-> * **無衝突複製資料型別(Conflict-free replicated datatypes,CRDT)**【32,38】是可以由多個使用者同時編輯的集合,對映,有序列表,計數器等的一系列資料結構,它們以合理的方式自動解決衝突。一些CRDT已經在Riak 2.0中實現【39,40】。
-> * **可合併的持久資料結構(Mergeable persistent data structures)**【41】顯式跟蹤歷史記錄,類似於Git版本控制系統,並使用三向合併功能(而CRDT使用雙向合併)。
-> * **可執行的轉換(operational transformation)**[42]是Etherpad 【30】和Google Docs 【31】等合作編輯應用背後的衝突解決演算法。它是專為同時編輯專案的有序列表而設計的,例如構成文字文件的字元列表。
+> * **無衝突複製資料型別(Conflict-free replicated datatypes,CRDT)**【32,38】是可以由多個使用者同時編輯的集合,對映,有序列表,計數器等的一系列資料結構,它們以合理的方式自動解決衝突。一些 CRDT 已經在 Riak 2.0 中實現【39,40】。
+> * **可合併的持久資料結構(Mergeable persistent data structures)**【41】顯式跟蹤歷史記錄,類似於 Git 版本控制系統,並使用三向合併功能(而 CRDT 使用雙向合併)。
+> * **可執行的轉換(operational transformation)**[42] 是 Etherpad 【30】和 Google Docs 【31】等合作編輯應用背後的衝突解決演算法。它是專為同時編輯專案的有序列表而設計的,例如構成文字文件的字元列表。
 >
 > 這些演算法在資料庫中的實現還很年輕,但很可能將來它們將被整合到更多的複製資料系統中。自動衝突解決方案可以使應用程式處理多領導者資料同步更為簡單。
 >
@@ -424,51 +424,51 @@ PostgreSQL和Oracle等使用這種複製方法【16】。主要缺點是日誌
 
 #### 什麼是衝突?
 
-有些衝突是顯而易見的。在[圖5-7](../img/fig5-7.png)的例子中,兩個寫操作併發地修改了同一條記錄中的同一個欄位,並將其設定為兩個不同的值。毫無疑問這是一個衝突。
+有些衝突是顯而易見的。在 [圖 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]。一個指定的根節點將寫入轉發給所有其他節點。星形拓撲可以推廣到樹。
+最普遍的拓撲是全部到全部([圖 5-8 (c)](../img/fig5-8.png)),其中每個領導者將其寫入每個其他領導。但是,也會使用更多受限制的拓撲:例如,預設情況下,MySQL 僅支援 **環形拓撲(circular topology)**【34】,其中每個節點接收來自一個節點的寫入,並將這些寫入(加上自己的任何寫入)轉發給另一個節點。另一種流行的拓撲結構具有星形的形狀 [^v]。一個指定的根節點將寫入轉發給所有其他節點。星形拓撲可以推廣到樹。
 
-[^v]: 不要與星型模式混淆(請參閱“[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”),其中描述了資料模型的結構,而不是節點之間的通訊拓撲。
+[^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】。
 
 如果你正在使用具有多領導者複製功能的系統,那麼應該瞭解這些問題,仔細閱讀文件,並徹底測試你的資料庫,以確保它確實提供了你認為具有的保證。
 
 
 ## 無主複製
 
-我們在本章到目前為止所討論的複製方法 ——單主複製、多主複製——都是這樣的想法:客戶端向一個主庫傳送寫請求,而資料庫系統負責將寫入複製到其他副本。主庫決定寫入的順序,而從庫按相同順序應用主庫的寫入。
+我們在本章到目前為止所討論的複製方法 —— 單主複製、多主複製 —— 都是這樣的想法:客戶端向一個主庫傳送寫請求,而資料庫系統負責將寫入複製到其他副本。主庫決定寫入的順序,而從庫按相同順序應用主庫的寫入。
 
-一些資料儲存系統採用不同的方法,放棄主庫的概念,並允許任何副本直接接受來自客戶端的寫入。最早的一些的複製資料系統是**無領導的(leaderless)**【1,44】,但是在關係資料庫主導的時代,這個想法幾乎已被忘卻。在亞馬遜將其用於其內部的Dynamo系統[^vi]之後,它再一次成為資料庫的一種時尚架構【37】。 Riak,Cassandra和Voldemort是由Dynamo啟發的無領導複製模型的開源資料儲存,所以這類資料庫也被稱為*Dynamo風格*。
+一些資料儲存系統採用不同的方法,放棄主庫的概念,並允許任何副本直接接受來自客戶端的寫入。最早的一些的複製資料系統是 **無領導的(leaderless)**【1,44】,但是在關係資料庫主導的時代,這個想法幾乎已被忘卻。在亞馬遜將其用於其內部的 Dynamo 系統 [^vi] 之後,它再一次成為資料庫的一種時尚架構【37】。 Riak,Cassandra 和 Voldemort 是由 Dynamo 啟發的無領導複製模型的開源資料儲存,所以這類資料庫也被稱為 *Dynamo 風格*。
 
-[^vi]: Dynamo不適用於Amazon以外的使用者。 令人困惑的是,AWS提供了一個名為DynamoDB的託管資料庫產品,它使用了完全不同的體系結構:它基於單領導者複製。
+[^vi]: Dynamo 不適用於 Amazon 以外的使用者。 令人困惑的是,AWS 提供了一個名為 DynamoDB 的託管資料庫產品,它使用了完全不同的體系結構:它基於單領導者複製。
 
 在一些無領導者的實現中,客戶端直接將寫入傳送到幾個副本中,而另一些情況下,一個 **協調者(coordinator)** 節點代表客戶端進行寫入。但與主庫資料庫不同,協調者不執行特定的寫入順序。我們將會看到,這種設計上的差異對資料庫的使用方式有著深遠的影響。
 
@@ -476,84 +476,84 @@ PostgreSQL和Oracle等使用這種複製方法【16】。主要缺點是日誌
 
 假設你有一個帶有三個副本的資料庫,而其中一個副本目前不可用,或許正在重新啟動以安裝系統更新。在基於主機的配置中,如果要繼續處理寫入,則可能需要執行故障切換(請參閱「[處理節點宕機](#處理節點宕機)」)。
 
-另一方面,在無領導配置中,故障切換不存在。[圖5-10](../img/fig5-10.png)顯示了發生了什麼事情:客戶端(使用者1234)並行傳送寫入到所有三個副本,並且兩個可用副本接受寫入,但是不可用副本錯過了它。假設三個副本中的兩個承認寫入是足夠的:在使用者1234已經收到兩個確定的響應之後,我們認為寫入成功。客戶簡單地忽略了其中一個副本錯過了寫入的事實。
+另一方面,在無領導配置中,故障切換不存在。[圖 5-10](../img/fig5-10.png) 顯示了發生了什麼事情:客戶端(使用者 1234)並行傳送寫入到所有三個副本,並且兩個可用副本接受寫入,但是不可用副本錯過了它。假設三個副本中的兩個承認寫入是足夠的:在使用者 1234 已經收到兩個確定的響應之後,我們認為寫入成功。客戶簡單地忽略了其中一個副本錯過了寫入的事實。
 
 ![](../img/fig5-10.png)
 
-**圖5-10 法定寫入,法定讀取,並在節點中斷後讀修復。**
+**圖 5-10 法定寫入,法定讀取,並在節點中斷後讀修復。**
 
 現在想象一下,不可用的節點重新聯機,客戶端開始讀取它。節點關閉時發生的任何寫入都從該節點丟失。因此,如果你從該節點讀取資料,則可能會將陳舊(過時)值視為響應。
 
-為了解決這個問題,當一個客戶端從資料庫中讀取資料時,它不僅僅傳送它的請求到一個副本:讀請求也被並行地傳送到多個節點。客戶可能會從不同的節點獲得不同的響應。即來自一個節點的最新值和來自另一個節點的陳舊值。版本號用於確定哪個值更新(請參閱“[檢測併發寫入](#檢測併發寫入)”)。
+為了解決這個問題,當一個客戶端從資料庫中讀取資料時,它不僅僅傳送它的請求到一個副本:讀請求也被並行地傳送到多個節點。客戶可能會從不同的節點獲得不同的響應。即來自一個節點的最新值和來自另一個節點的陳舊值。版本號用於確定哪個值更新(請參閱 “[檢測併發寫入](#檢測併發寫入)”)。
 
 #### 讀修復和反熵
 
 複製方案應確保最終將所有資料複製到每個副本。在一個不可用的節點重新聯機之後,它如何趕上它錯過的寫入?
 
-在Dynamo風格的資料儲存中經常使用兩種機制:
+在 Dynamo 風格的資料儲存中經常使用兩種機制:
 
 * 讀修復(Read repair)
 
-  當客戶端並行讀取多個節點時,它可以檢測到任何陳舊的響應。例如,在[圖5-10](../img/fig5-10.png)中,使用者2345獲得了來自副本3的版本6值和來自副本1和2的版本7值。客戶端發現副本3具有陳舊值,並將新值寫回到該副本。這種方法適用於讀頻繁的值。
+  當客戶端並行讀取多個節點時,它可以檢測到任何陳舊的響應。例如,在 [圖 5-10](../img/fig5-10.png) 中,使用者 2345 獲得了來自副本 3 的版本 6 值和來自副本 1 和 2 的版本 7 值。客戶端發現副本 3 具有陳舊值,並將新值寫回到該副本。這種方法適用於讀頻繁的值。
 
 * 反熵過程(Anti-entropy process)
 
   此外,一些資料儲存具有後臺程序,該程序不斷查詢副本之間的資料差異,並將任何缺少的資料從一個副本複製到另一個副本。與基於領導者的複製中的複製日誌不同,此反熵過程不會以任何特定的順序複製寫入,並且在複製資料之前可能會有顯著的延遲。
 
-並不是所有的系統都實現了這兩個,例如,Voldemort目前沒有反熵過程。請注意,如果沒有反熵過程,某些副本中很少讀取的值可能會丟失,從而降低了永續性,因為只有在應用程式讀取值時才執行讀修復。
+並不是所有的系統都實現了這兩個,例如,Voldemort 目前沒有反熵過程。請注意,如果沒有反熵過程,某些副本中很少讀取的值可能會丟失,從而降低了永續性,因為只有在應用程式讀取值時才執行讀修復。
 
 #### 讀寫的法定人數
 
-在[圖5-10](../img/fig5-10.png)的示例中,我們認為即使僅在三個副本中的兩個上進行處理,寫入仍然是成功的。如果三個副本中只有一個接受了寫入,會怎樣?以此類推,究竟多少個副本完成才可以認為寫成功?
+在 [圖 5-10](../img/fig5-10.png) 的示例中,我們認為即使僅在三個副本中的兩個上進行處理,寫入仍然是成功的。如果三個副本中只有一個接受了寫入,會怎樣?以此類推,究竟多少個副本完成才可以認為寫成功?
 
 如果我們知道,每個成功的寫操作意味著在三個副本中至少有兩個出現,這意味著至多有一個副本可能是陳舊的。因此,如果我們從至少兩個副本讀取,我們可以確定至少有一個是最新的。如果第三個副本停機或響應速度緩慢,則讀取仍可以繼續返回最新值。
 
-更一般地說,如果有n個副本,每個寫入必須由w節點確認才能被認為是成功的,並且我們必須至少為每個讀取查詢r個節點。 (在我們的例子中,$n = 3,w = 2,r = 2$)。只要$w + r> n$,我們期望在讀取時獲得最新的值,因為r個讀取中至少有一個節點是最新的。遵循這些r值,w值的讀寫稱為**法定人數(quorum)**[^vii]的讀和寫【44】。你可以認為,r和w是有效讀寫所需的最低票數。
+更一般地說,如果有 n 個副本,每個寫入必須由 w 節點確認才能被認為是成功的,並且我們必須至少為每個讀取查詢 r 個節點。 (在我們的例子中,$n = 3,w = 2,r = 2$)。只要 $w + r> n$,我們期望在讀取時獲得最新的值,因為 r 個讀取中至少有一個節點是最新的。遵循這些 r 值,w 值的讀寫稱為 **法定人數(quorum)**[^vii] 的讀和寫【44】。你可以認為,r 和 w 是有效讀寫所需的最低票數。
 
-[^vii]: 有時候這種法定人數被稱為嚴格的法定人數,相對“寬鬆的法定人數”而言(見“[寬鬆的法定人數與提示移交](#寬鬆的法定人數與提示移交)”)
+[^vii]: 有時候這種法定人數被稱為嚴格的法定人數,相對 “寬鬆的法定人數” 而言(見 “[寬鬆的法定人數與提示移交](#寬鬆的法定人數與提示移交)”)
 
-在Dynamo風格的資料庫中,引數n,w和r通常是可配置的。一個常見的選擇是使n為奇數(通常為3或5)並設定 $w = r =(n + 1)/ 2$(向上取整)。但是可以根據需要更改數字。例如,設定$w = n$和$r = 1$的寫入很少且讀取次數較多的工作負載可能會受益。這使得讀取速度更快,但具有隻有一個失敗節點導致所有資料庫寫入失敗的缺點。
+在 Dynamo 風格的資料庫中,引數 n,w 和 r 通常是可配置的。一個常見的選擇是使 n 為奇數(通常為 3 或 5)並設定 $w = r =(n + 1)/ 2$(向上取整)。但是可以根據需要更改數字。例如,設定 $w = n$ 和 $r = 1$ 的寫入很少且讀取次數較多的工作負載可能會受益。這使得讀取速度更快,但具有隻有一個失敗節點導致所有資料庫寫入失敗的缺點。
 
-> 叢集中可能有多於n的節點。(叢集的機器數可能多於副本數目),但是任何給定的值只能儲存在n個節點上。這允許對資料集進行分割槽,從而可以支援比單個節點的儲存能力更大的資料集。我們將在[第六章](ch6.md)繼續討論分割槽。
+> 叢集中可能有多於 n 的節點。(叢集的機器數可能多於副本數目),但是任何給定的值只能儲存在 n 個節點上。這允許對資料集進行分割槽,從而可以支援比單個節點的儲存能力更大的資料集。我們將在 [第六章](ch6.md) 繼續討論分割槽。
 >
 
-法定人數條件$w + r> n$允許系統容忍不可用的節點,如下所示:
+法定人數條件 $w + r> n$ 允許系統容忍不可用的節點,如下所示:
 
-* 如果$w <n$,如果節點不可用,我們仍然可以處理寫入。
-* 如果$r <n$,如果節點不可用,我們仍然可以處理讀取。
-* 對於$n = 3,w = 2,r = 2$,我們可以容忍一個不可用的節點。
-* 對於$n = 5,w = 3,r = 3$,我們可以容忍兩個不可用的節點。 這個案例如[圖5-11](../img/fig5-11.png)所示。
-* 通常,讀取和寫入操作始終並行傳送到所有n個副本。 引數w和r決定我們等待多少個節點,即在我們認為讀或寫成功之前,有多少個節點需要報告成功。
+* 如果 $w <n$,如果節點不可用,我們仍然可以處理寫入。
+* 如果 $r <n$,如果節點不可用,我們仍然可以處理讀取。
+* 對於 $n = 3,w = 2,r = 2$,我們可以容忍一個不可用的節點。
+* 對於 $n = 5,w = 3,r = 3$,我們可以容忍兩個不可用的節點。 這個案例如 [圖 5-11](../img/fig5-11.png) 所示。
+* 通常,讀取和寫入操作始終並行傳送到所有 n 個副本。 引數 w 和 r 決定我們等待多少個節點,即在我們認為讀或寫成功之前,有多少個節點需要報告成功。
 
 ![](../img/fig5-11.png)
 
-**圖5-11  如果$w + r > n$,讀取r個副本,至少有一個r副本必然包含了最近的成功寫入**
+**圖 5-11  如果 $w + r > n$,讀取 r 個副本,至少有一個 r 副本必然包含了最近的成功寫入**
 
-如果少於所需的w或r節點可用,則寫入或讀取將返回錯誤。 由於許多原因,節點可能不可用:因為執行操作的錯誤(由於磁碟已滿而無法寫入),因為節點關閉(崩潰,關閉電源),由於客戶端和伺服器節點之間的網路中斷,或任何其他原因。 我們只關心節點是否返回了成功的響應,而不需要區分不同型別的錯誤。
+如果少於所需的 w 或 r 節點可用,則寫入或讀取將返回錯誤。 由於許多原因,節點可能不可用:因為執行操作的錯誤(由於磁碟已滿而無法寫入),因為節點關閉(崩潰,關閉電源),由於客戶端和伺服器節點之間的網路中斷,或任何其他原因。 我們只關心節點是否返回了成功的響應,而不需要區分不同型別的錯誤。
 
 
 ### 法定人數一致性的侷限性
 
-如果你有n個副本,並且你選擇w和r,使得$w + r> n$,你通常可以期望每個鍵的讀取都能返回最近寫入的值。情況就是這樣,因為你寫入的節點集合和你讀取的節點集合必須重疊。也就是說,你讀取的節點中必須至少有一個具有最新值的節點(如[圖5-11](../img/fig5-11.png)所示)。
+如果你有 n 個副本,並且你選擇 w 和 r,使得 $w + r> n$,你通常可以期望每個鍵的讀取都能返回最近寫入的值。情況就是這樣,因為你寫入的節點集合和你讀取的節點集合必須重疊。也就是說,你讀取的節點中必須至少有一個具有最新值的節點(如 [圖 5-11](../img/fig5-11.png) 所示)。
 
-通常,r和w被選為多數(超過 $n/2$ )節點,因為這確保了$w + r> n$,同時仍然容忍多達$n/2$個節點故障。但是,法定人數不一定必須是大多數,只是讀寫使用的節點交集至少需要包括一個節點。其他法定人數的配置是可能的,這使得分散式演算法的設計有一定的靈活性【45】。
+通常,r 和 w 被選為多數(超過 $n/2$ )節點,因為這確保了 $w + r> n$,同時仍然容忍多達 $n/2$ 個節點故障。但是,法定人數不一定必須是大多數,只是讀寫使用的節點交集至少需要包括一個節點。其他法定人數的配置是可能的,這使得分散式演算法的設計有一定的靈活性【45】。
 
-你也可以將w和r設定為較小的數字,以使$w + r≤n$(即法定條件不滿足)。在這種情況下,讀取和寫入操作仍將被傳送到n個節點,但操作成功只需要少量的成功響應。
+你也可以將 w 和 r 設定為較小的數字,以使 $w + r≤n$(即法定條件不滿足)。在這種情況下,讀取和寫入操作仍將被傳送到 n 個節點,但操作成功只需要少量的成功響應。
 
-較小的w和r更有可能會讀取過時的資料,因為你的讀取更有可能不包含具有最新值的節點。另一方面,這種配置允許更低的延遲和更高的可用性:如果存在網路中斷,並且許多副本變得無法訪問,則可以繼續處理讀取和寫入的機會更大。只有當可達副本的數量低於w或r時,資料庫才分別變得不可用於寫入或讀取。
+較小的 w 和 r 更有可能會讀取過時的資料,因為你的讀取更有可能不包含具有最新值的節點。另一方面,這種配置允許更低的延遲和更高的可用性:如果存在網路中斷,並且許多副本變得無法訪問,則可以繼續處理讀取和寫入的機會更大。只有當可達副本的數量低於 w 或 r 時,資料庫才分別變得不可用於寫入或讀取。
 
-但是,即使在$w + r> n$的情況下,也可能存在返回陳舊值的邊緣情況。這取決於實現,但可能的情況包括:
+但是,即使在 $w + r> n$ 的情況下,也可能存在返回陳舊值的邊緣情況。這取決於實現,但可能的情況包括:
 
-* 如果使用寬鬆的法定人數(見“[寬鬆的法定人數與提示移交](#寬鬆的法定人數與提示移交)”),w個寫入和r個讀取落在完全不同的節點上,因此r節點和w之間不再保證有重疊節點【46】。
-* 如果兩個寫入同時發生,不清楚哪一個先發生。在這種情況下,唯一安全的解決方案是合併併發寫入(請參閱“[處理寫入衝突](#處理寫入衝突)”)。如果根據時間戳(最後寫入勝利)挑選出一個勝者,則由於時鐘偏差【35】,寫入可能會丟失。我們將在“[檢測併發寫入](#檢測併發寫入)”繼續討論此話題。
+* 如果使用寬鬆的法定人數(見 “[寬鬆的法定人數與提示移交](#寬鬆的法定人數與提示移交)”),w 個寫入和 r 個讀取落在完全不同的節點上,因此 r 節點和 w 之間不再保證有重疊節點【46】。
+* 如果兩個寫入同時發生,不清楚哪一個先發生。在這種情況下,唯一安全的解決方案是合併併發寫入(請參閱 “[處理寫入衝突](#處理寫入衝突)”)。如果根據時間戳(最後寫入勝利)挑選出一個勝者,則由於時鐘偏差【35】,寫入可能會丟失。我們將在 “[檢測併發寫入](#檢測併發寫入)” 繼續討論此話題。
 * 如果寫操作與讀操作同時發生,寫操作可能僅反映在某些副本上。在這種情況下,不確定讀取是返回舊值還是新值。
-* 如果寫操作在某些副本上成功,而在其他節點上失敗(例如,因為某些節點上的磁碟已滿),在小於w個副本上寫入成功。所以整體判定寫入失敗,但整體寫入失敗並沒有在寫入成功的副本上回滾。這意味著如果一個寫入雖然報告失敗,後續的讀取仍然可能會讀取這次失敗寫入的值【47】。
-* 如果攜帶新值的節點失敗,需要讀取其他帶有舊值的副本。並且其資料從帶有舊值的副本中恢復,則儲存新值的副本數可能會低於w,從而打破法定人數條件。
-* 即使一切工作正常,有時也會不幸地出現關於**時序(timing)** 的邊緣情況,我們將在“[線性一致性和法定人數](ch9.md#線性一致性和法定人數)”中看到這點。
+* 如果寫操作在某些副本上成功,而在其他節點上失敗(例如,因為某些節點上的磁碟已滿),在小於 w 個副本上寫入成功。所以整體判定寫入失敗,但整體寫入失敗並沒有在寫入成功的副本上回滾。這意味著如果一個寫入雖然報告失敗,後續的讀取仍然可能會讀取這次失敗寫入的值【47】。
+* 如果攜帶新值的節點失敗,需要讀取其他帶有舊值的副本。並且其資料從帶有舊值的副本中恢復,則儲存新值的副本數可能會低於 w,從而打破法定人數條件。
+* 即使一切工作正常,有時也會不幸地出現關於 **時序(timing)** 的邊緣情況,我們將在 “[線性一致性和法定人數](ch9.md#線性一致性和法定人數)” 中看到這點。
 
-因此,儘管法定人數似乎保證讀取返回最新的寫入值,但在實踐中並不那麼簡單。 Dynamo風格的資料庫通常針對可以忍受最終一致性的用例進行最佳化。允許透過引數w和r來調整讀取陳舊值的概率,但把它們當成絕對的保證是不明智的。
+因此,儘管法定人數似乎保證讀取返回最新的寫入值,但在實踐中並不那麼簡單。 Dynamo 風格的資料庫通常針對可以忍受最終一致性的用例進行最佳化。允許透過引數 w 和 r 來調整讀取陳舊值的概率,但把它們當成絕對的保證是不明智的。
 
-尤其是,因為通常沒有得到“[複製延遲問題](#複製延遲問題)”中討論的保證(讀己之寫,單調讀,一致字首讀),前面提到的異常可能會發生在應用程式中。更強有力的保證通常需要**事務**或**共識**。我們將在[第七章](ch7.md)和[第九章](ch9.md)回到這些話題。
+尤其是,因為通常沒有得到 “[複製延遲問題](#複製延遲問題)” 中討論的保證(讀己之寫,單調讀,一致字首讀),前面提到的異常可能會發生在應用程式中。更強有力的保證通常需要 **事務** 或 **共識**。我們將在 [第七章](ch7.md) 和 [第九章](ch9.md) 回到這些話題。
 
 #### 監控陳舊度
 
@@ -563,42 +563,42 @@ PostgreSQL和Oracle等使用這種複製方法【16】。主要缺點是日誌
 
 然而,在無領導者複製的系統中,沒有固定的寫入順序,這使得監控變得更加困難。而且,如果資料庫只使用讀修復(沒有反熵過程),那麼對於一個值可能會有多大的限制是沒有限制的 - 如果一個值很少被讀取,那麼由一個陳舊副本返回的值可能是古老的。
 
-已經有一些關於衡量無主複製資料庫中的複製陳舊度的研究,並根據引數n,w和r來預測陳舊讀取的預期百分比【48】。不幸的是,這還不是很常見的做法,但是將陳舊測量值包含在資料庫的度量標準集中是一件好事。雖然最終一致性是一種有意模糊的保證,但是從可操作性角度來說,能夠量化“最終”也是很重要的。
+已經有一些關於衡量無主複製資料庫中的複製陳舊度的研究,並根據引數 n,w 和 r 來預測陳舊讀取的預期百分比【48】。不幸的是,這還不是很常見的做法,但是將陳舊測量值包含在資料庫的度量標準集中是一件好事。雖然最終一致性是一種有意模糊的保證,但是從可操作性角度來說,能夠量化 “最終” 也是很重要的。
 
 ### 寬鬆的法定人數與提示移交
 
-合理配置的法定人數可以使資料庫無需故障切換即可容忍個別節點的故障。也可以容忍個別節點變慢,因為請求不必等待所有n個節點響應——當w或r節點響應時它們可以返回。對於需要高可用、低延時、且能夠容忍偶爾讀到陳舊值的應用場景來說,這些特性使無主複製的資料庫很有吸引力。
+合理配置的法定人數可以使資料庫無需故障切換即可容忍個別節點的故障。也可以容忍個別節點變慢,因為請求不必等待所有 n 個節點響應 —— 當 w 或 r 節點響應時它們可以返回。對於需要高可用、低延時、且能夠容忍偶爾讀到陳舊值的應用場景來說,這些特性使無主複製的資料庫很有吸引力。
 
-然而,法定人數(如迄今為止所描述的)並不像它們可能的那樣具有容錯性。網路中斷可以很容易地將客戶端從大量的資料庫節點上切斷。雖然這些節點是活著的,而其他客戶端可能能夠連線到它們,但是從資料庫節點切斷的客戶端來看,它們也可能已經死亡。在這種情況下,剩餘的可用節點可能會少於w或r,因此客戶端不再能達到法定人數。
+然而,法定人數(如迄今為止所描述的)並不像它們可能的那樣具有容錯性。網路中斷可以很容易地將客戶端從大量的資料庫節點上切斷。雖然這些節點是活著的,而其他客戶端可能能夠連線到它們,但是從資料庫節點切斷的客戶端來看,它們也可能已經死亡。在這種情況下,剩餘的可用節點可能會少於 w 或 r,因此客戶端不再能達到法定人數。
 
-在一個大型的叢集中(節點數量明顯多於n個),網路中斷期間客戶端可能仍能連線到一些資料庫節點,但又不足以組成一個特定值的法定人數。在這種情況下,資料庫設計人員需要權衡一下:
+在一個大型的叢集中(節點數量明顯多於 n 個),網路中斷期間客戶端可能仍能連線到一些資料庫節點,但又不足以組成一個特定值的法定人數。在這種情況下,資料庫設計人員需要權衡一下:
 
-* 對於所有無法達到w或r節點法定人數的請求,是否返回錯誤是更好的?
-* 或者我們是否應該接受寫入,然後將它們寫入一些可達的節點,但不在這些值通常所存在的n個節點上?
+* 對於所有無法達到 w 或 r 節點法定人數的請求,是否返回錯誤是更好的?
+* 或者我們是否應該接受寫入,然後將它們寫入一些可達的節點,但不在這些值通常所存在的 n 個節點上?
 
-後者被認為是一個**寬鬆的法定人數(sloppy quorum)**【37】:寫和讀仍然需要w和r成功的響應,但這些響應可能來自不在指定的n個“主”節點中的其它節點。比方說,如果你把自己鎖在房子外面,你可能會敲開鄰居的門,問你是否可以暫時呆在沙發上。
+後者被認為是一個 **寬鬆的法定人數(sloppy quorum)**【37】:寫和讀仍然需要 w 和 r 成功的響應,但這些響應可能來自不在指定的 n 個 “主” 節點中的其它節點。比方說,如果你把自己鎖在房子外面,你可能會敲開鄰居的門,問你是否可以暫時呆在沙發上。
 
-一旦網路中斷得到解決,代表另一個節點臨時接受的一個節點的任何寫入都被傳送到適當的“主”節點。這就是所謂的**提示移交(hinted handoff)**(一旦你再次找到你的房子的鑰匙,你的鄰居禮貌地要求你離開沙發回家)。
+一旦網路中斷得到解決,代表另一個節點臨時接受的一個節點的任何寫入都被傳送到適當的 “主” 節點。這就是所謂的 **提示移交(hinted handoff)**(一旦你再次找到你的房子的鑰匙,你的鄰居禮貌地要求你離開沙發回家)。
 
-寬鬆的法定人數對寫入可用性的提高特別有用:只要有任何w節點可用,資料庫就可以接受寫入。然而,這意味著即使當$w + r> n$時,也不能確定讀取某個鍵的最新值,因為最新的值可能已經臨時寫入了n之外的某些節點【47】。
+寬鬆的法定人數對寫入可用性的提高特別有用:只要有任何 w 節點可用,資料庫就可以接受寫入。然而,這意味著即使當 $w + r> n$ 時,也不能確定讀取某個鍵的最新值,因為最新的值可能已經臨時寫入了 n 之外的某些節點【47】。
 
-因此,在傳統意義上,一個寬鬆的法定人數實際上不是一個法定人數。這只是一個保證,即資料儲存在w節點的地方。但不能保證r節點的讀取,直到提示移交已經完成。
+因此,在傳統意義上,一個寬鬆的法定人數實際上不是一個法定人數。這只是一個保證,即資料儲存在 w 節點的地方。但不能保證 r 節點的讀取,直到提示移交已經完成。
 
-在所有常見的Dynamo實現中,寬鬆的法定人數是可選的。在Riak中,它們預設是啟用的,而在Cassandra和Voldemort中它們預設是禁用的【46,49,50】。
+在所有常見的 Dynamo 實現中,寬鬆的法定人數是可選的。在 Riak 中,它們預設是啟用的,而在 Cassandra 和 Voldemort 中它們預設是禁用的【46,49,50】。
 
 #### 運維多個數據中心
 
-我們先前討論了跨資料中心複製作為多主複製的用例(請參閱“[多主複製](#多主複製)”)。無主複製也適用於多資料中心操作,因為它旨在容忍衝突的併發寫入,網路中斷和延遲尖峰。
+我們先前討論了跨資料中心複製作為多主複製的用例(請參閱 “[多主複製](#多主複製)”)。無主複製也適用於多資料中心操作,因為它旨在容忍衝突的併發寫入,網路中斷和延遲尖峰。
 
-Cassandra和Voldemort在正常的無主模型中實現了他們的多資料中心支援:副本的數量n包括所有資料中心的節點,在配置中,你可以指定每個資料中心中你想擁有的副本的數量。無論資料中心如何,每個來自客戶端的寫入都會發送到所有副本,但客戶端通常只等待來自其本地資料中心內的法定節點的確認,從而不會受到跨資料中心鏈路延遲和中斷的影響。對其他資料中心的高延遲寫入通常被配置為非同步發生,儘管配置有一定的靈活性【50,51】。
+Cassandra 和 Voldemort 在正常的無主模型中實現了他們的多資料中心支援:副本的數量 n 包括所有資料中心的節點,在配置中,你可以指定每個資料中心中你想擁有的副本的數量。無論資料中心如何,每個來自客戶端的寫入都會發送到所有副本,但客戶端通常只等待來自其本地資料中心內的法定節點的確認,從而不會受到跨資料中心鏈路延遲和中斷的影響。對其他資料中心的高延遲寫入通常被配置為非同步發生,儘管配置有一定的靈活性【50,51】。
 
-Riak將客戶端和資料庫節點之間的所有通訊保持在一個數據中心本地,因此n描述了一個數據中心內的副本數量。資料庫叢集之間的跨資料中心複製在後臺非同步發生,其風格類似於多領導者複製【52】。
+Riak 將客戶端和資料庫節點之間的所有通訊保持在一個數據中心本地,因此 n 描述了一個數據中心內的副本數量。資料庫叢集之間的跨資料中心複製在後臺非同步發生,其風格類似於多領導者複製【52】。
 
 ### 檢測併發寫入
 
-Dynamo風格的資料庫允許多個客戶端同時寫入相同的Key,這意味著即使使用嚴格的法定人數也會發生衝突。這種情況與多領導者複製相似(請參閱“[處理寫入衝突](#處理寫入衝突)”),但在Dynamo樣式的資料庫中,在**讀修復**或**提示移交**期間也可能會產生衝突。
+Dynamo 風格的資料庫允許多個客戶端同時寫入相同的 Key,這意味著即使使用嚴格的法定人數也會發生衝突。這種情況與多領導者複製相似(請參閱 “[處理寫入衝突](#處理寫入衝突)”),但在 Dynamo 樣式的資料庫中,在 **讀修復** 或 **提示移交** 期間也可能會產生衝突。
 
-問題在於,由於可變的網路延遲和部分故障,事件可能在不同的節點以不同的順序到達。例如,[圖5-12](../img/fig5-12.png)顯示了兩個客戶機A和B同時寫入三節點資料儲存區中的鍵X:
+問題在於,由於可變的網路延遲和部分故障,事件可能在不同的節點以不同的順序到達。例如,[圖 5-12](../img/fig5-12.png) 顯示了兩個客戶機 A 和 B 同時寫入三節點資料儲存區中的鍵 X:
 
 * 節點 1 接收來自 A 的寫入,但由於暫時中斷,未接收到來自 B 的寫入。
 * 節點 2 首先接收來自 A 的寫入,然後接收來自 B 的寫入。
@@ -606,72 +606,72 @@ Dynamo風格的資料庫允許多個客戶端同時寫入相同的Key,這意
 
 ![](../img/fig5-12.png)
 
-**圖5-12 併發寫入Dynamo風格的資料儲存:沒有明確定義的順序。**
+**圖 5-12 併發寫入 Dynamo 風格的資料儲存:沒有明確定義的順序。**
 
-如果每個節點只要接收到來自客戶端的寫入請求就簡單地覆蓋了某個鍵的值,那麼節點就會永久地不一致,如[圖5-12](../img/fig5-12.png)中的最終獲取請求所示:節點2認為 X 的最終值是 B,而其他節點認為值是 A 。
+如果每個節點只要接收到來自客戶端的寫入請求就簡單地覆蓋了某個鍵的值,那麼節點就會永久地不一致,如 [圖 5-12](../img/fig5-12.png) 中的最終獲取請求所示:節點 2 認為 X 的最終值是 B,而其他節點認為值是 A 。
 
 為了最終達成一致,副本應該趨於相同的值。如何做到這一點?有人可能希望複製的資料庫能夠自動處理,但不幸的是,大多數的實現都很糟糕:如果你想避免丟失資料,你(應用程式開發人員)需要知道很多有關資料庫衝突處理的內部資訊。
 
-在“[處理寫入衝突](#處理寫入衝突)”一節中已經簡要介紹了一些解決衝突的技術。在總結本章之前,讓我們來更詳細地探討這個問題。
+在 “[處理寫入衝突](#處理寫入衝突)” 一節中已經簡要介紹了一些解決衝突的技術。在總結本章之前,讓我們來更詳細地探討這個問題。
 
 #### 最後寫入勝利(丟棄併發寫入)
 
-實現最終融合的一種方法是宣告每個副本只需要儲存最 **“最近”** 的值,並允許 **“更舊”** 的值被覆蓋和拋棄。然後,只要我們有一種明確的方式來確定哪個寫是“最近的”,並且每個寫入最終都被複制到每個副本,那麼複製最終會收斂到相同的值。
+實現最終融合的一種方法是宣告每個副本只需要儲存最 **“最近”** 的值,並允許 **“更舊”** 的值被覆蓋和拋棄。然後,只要我們有一種明確的方式來確定哪個寫是 “最近的”,並且每個寫入最終都被複制到每個副本,那麼複製最終會收斂到相同的值。
 
-正如 **“最近”** 的引號所表明的,這個想法其實頗具誤導性。在[圖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# 有序事件的時間戳)” 中討論。
 
-有一些情況,如快取,其中丟失的寫入可能是可以接受的。如果丟失資料不可接受,LWW是解決衝突的一個很爛的選擇。
+有一些情況,如快取,其中丟失的寫入可能是可以接受的。如果丟失資料不可接受,LWW 是解決衝突的一個很爛的選擇。
 
-與LWW一起使用資料庫的唯一安全方法是確保一個鍵只寫入一次,然後視為不可變,從而避免對同一個鍵進行併發更新。例如,Cassandra推薦使用的方法是使用UUID作為鍵,從而為每個寫操作提供一個唯一的鍵【53】。
+與 LWW 一起使用資料庫的唯一安全方法是確保一個鍵只寫入一次,然後視為不可變,從而避免對同一個鍵進行併發更新。例如,Cassandra 推薦使用的方法是使用 UUID 作為鍵,從而為每個寫操作提供一個唯一的鍵【53】。
 
 #### “此前發生”的關係和併發
 
 我們如何判斷兩個操作是否是併發的?為了建立一個直覺,讓我們看看一些例子:
 
-* 在[圖5-9](fig5-9.png)中,兩個寫入不是併發的:A的插入發生在B的遞增之前,因為B遞增的值是A插入的值。換句話說,B的操作建立在A的操作上,所以B的操作必須有後來發生。我們也可以說B是 **因果依賴(causally dependent)** 於A。
-* 另一方面,[圖5-12](fig5-12.png)中的兩個寫入是併發的:當每個客戶端啟動操作時,它不知道另一個客戶端也正在執行操作同樣的鍵。因此,操作之間不存在因果關係。
+* 在 [圖 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併發。我們需要的是一個演算法來告訴我們兩個操作是否是併發的。如果一個操作發生在另一個操作之前,則後面的操作應該覆蓋較早的操作,但是如果這些操作是併發的,則存在需要解決的衝突。
+因此,只要有兩個操作 A 和 B,就有三種可能性:A 在 B 之前發生,或者 B 在 A 之前發生,或者 A 和 B 併發。我們需要的是一個演算法來告訴我們兩個操作是否是併發的。如果一個操作發生在另一個操作之前,則後面的操作應該覆蓋較早的操作,但是如果這些操作是併發的,則存在需要解決的衝突。
 
 
 > #### 併發性,時間和相對性
 >
-> 如果兩個操作 **“同時”** 發生,似乎應該稱為併發——但事實上,它們在字面時間上重疊與否並不重要。由於分散式系統中的時鐘問題,現實中是很難判斷兩個事件是否**同時**發生的,這個問題我們將在[第八章](ch8.md)中詳細討論。
+> 如果兩個操作 **“同時”** 發生,似乎應該稱為併發 —— 但事實上,它們在字面時間上重疊與否並不重要。由於分散式系統中的時鐘問題,現實中是很難判斷兩個事件是否 **同時** 發生的,這個問題我們將在 [第八章](ch8.md) 中詳細討論。
 >
-> 為了定義併發性,確切的時間並不重要:如果兩個操作都意識不到對方的存在,就稱這兩個操作**併發**,而不管它們發生的物理時間。人們有時把這個原理和狹義相對論的物理學聯絡起來【54】,它引入了資訊不能比光速更快的思想。因此,如果兩個事件發生的時間差小於光透過它們之間的距離所需要的時間,那麼這兩個事件不可能相互影響。
+> 為了定義併發性,確切的時間並不重要:如果兩個操作都意識不到對方的存在,就稱這兩個操作 **併發**,而不管它們發生的物理時間。人們有時把這個原理和狹義相對論的物理學聯絡起來【54】,它引入了資訊不能比光速更快的思想。因此,如果兩個事件發生的時間差小於光透過它們之間的距離所需要的時間,那麼這兩個事件不可能相互影響。
 >
-> 在計算機系統中,即使光速原則上允許一個操作影響另一個操作,但兩個操作也可能是**並行的**。例如,如果網路緩慢或中斷,兩個操作間可能會出現一段時間間隔,但仍然是併發的,因為網路問題阻止一個操作意識到另一個操作的存在。
+> 在計算機系統中,即使光速原則上允許一個操作影響另一個操作,但兩個操作也可能是 **並行的**。例如,如果網路緩慢或中斷,兩個操作間可能會出現一段時間間隔,但仍然是併發的,因為網路問題阻止一個操作意識到另一個操作的存在。
 
 
 #### 捕獲"此前發生"關係
 
 來看一個演算法,它確定兩個操作是否為併發的,還是一個在另一個之前。為了簡單起見,我們從一個只有一個副本的資料庫開始。一旦我們已經制定了如何在單個副本上完成這項工作,我們可以將該方法推廣到具有多個副本的無領導者資料庫。
 
-[圖5-13](../img/fig5-13.png)顯示了兩個客戶端同時向同一購物車新增專案。 (如果這樣的例子讓你覺得太麻煩了,那麼可以想象,兩個空中交通管制員同時把飛機新增到他們正在跟蹤的區域)最初,購物車是空的。在它們之間,客戶端向資料庫發出五次寫入:
+[圖 5-13](../img/fig5-13.png) 顯示了兩個客戶端同時向同一購物車新增專案。 (如果這樣的例子讓你覺得太麻煩了,那麼可以想象,兩個空中交通管制員同時把飛機新增到他們正在跟蹤的區域)最初,購物車是空的。在它們之間,客戶端向資料庫發出五次寫入:
 
-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:[雞蛋,牛奶,火腿]
-5. 最後,客戶端 1 想要加培根。它以前在v3中從伺服器接收[牛奶,麵粉]和[雞蛋],所以它合併這些,新增培根,並將最終值[牛奶,麵粉,雞蛋,培根]連同版本號v3發往伺服器。這會覆蓋v3[牛奶,麵粉](請注意[雞蛋]已經在最後一步被覆蓋),但與v4[雞蛋,牛奶,火腿]併發,所以伺服器保留這兩個併發值。
+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:[雞蛋,牛奶,火腿]
+5. 最後,客戶端 1 想要加培根。它以前在 v3 中從伺服器接收 [牛奶,麵粉] 和 [雞蛋],所以它合併這些,新增培根,並將最終值 [牛奶,麵粉,雞蛋,培根] 連同版本號 v3 發往伺服器。這會覆蓋 v3 [牛奶,麵粉](請注意 [雞蛋] 已經在最後一步被覆蓋),但與 v4 [雞蛋,牛奶,火腿] 併發,所以伺服器保留這兩個併發值。
 
 ![](../img/fig5-13.png)
 
-**圖5-13  捕獲兩個客戶端之間的因果關係,同時編輯購物車。**
+**圖 5-13  捕獲兩個客戶端之間的因果關係,同時編輯購物車。**
 
-[圖5-13](../img/fig5-13.png)中的操作之間的資料流如[圖5-14](../img/fig5-14.png)所示。 箭頭表示哪個操作發生在其他操作之前,意味著後面的操作知道或依賴於較早的操作。 在這個例子中,客戶端永遠不會完全掌握伺服器上的資料,因為總是有另一個操作同時進行。 但是,舊版本的值最終會被覆蓋,並且不會丟失任何寫入。
+[圖 5-13](../img/fig5-13.png) 中的操作之間的資料流如 [圖 5-14](../img/fig5-14.png) 所示。 箭頭表示哪個操作發生在其他操作之前,意味著後面的操作知道或依賴於較早的操作。 在這個例子中,客戶端永遠不會完全掌握伺服器上的資料,因為總是有另一個操作同時進行。 但是,舊版本的值最終會被覆蓋,並且不會丟失任何寫入。
 
 ![](../img/fig5-14.png)
 
-**圖5-14 圖5-13中的因果依賴關係圖。**
+**圖 5-14 圖 5-13 中的因果依賴關係圖。**
 
-請注意,伺服器可以透過檢視版本號來確定兩個操作是否是併發的——它不需要解釋該值本身(因此該值可以是任何資料結構)。該演算法的工作原理如下:
+請注意,伺服器可以透過檢視版本號來確定兩個操作是否是併發的 —— 它不需要解釋該值本身(因此該值可以是任何資料結構)。該演算法的工作原理如下:
 
 * 伺服器為每個鍵保留一個版本號,每次寫入鍵時都增加版本號,並將新版本號與寫入的值一起儲存。
 * 當客戶端讀取鍵時,伺服器將返回所有未覆蓋的值以及最新的版本號。客戶端在寫入前必須讀取。
@@ -682,31 +682,31 @@ LWW實現了最終收斂的目標,但以**永續性**為代價:如果同一
 
 #### 合併同時寫入的值
 
-這種演算法可以確保沒有資料被無聲地丟棄,但不幸的是,客戶端需要做一些額外的工作:客戶端隨後必須透過合併併發寫入的值來進行清理。 Riak稱這些併發值為**兄弟(siblings)**。
+這種演算法可以確保沒有資料被無聲地丟棄,但不幸的是,客戶端需要做一些額外的工作:客戶端隨後必須透過合併併發寫入的值來進行清理。 Riak 稱這些併發值為 **兄弟(siblings)**。
 
-合併併發值,本質上是與多領導者複製中的衝突解決問題相同,我們先前討論過(請參閱“[處理寫入衝突](#處理寫入衝突)”)。一個簡單的方法是根據版本號或時間戳(最後寫入勝利)來選擇一個值,但這意味著丟失資料。所以,你可能需要在應用程式程式碼中額外做些更聰明的事情。
+合併併發值,本質上是與多領導者複製中的衝突解決問題相同,我們先前討論過(請參閱 “[處理寫入衝突](#處理寫入衝突)”)。一個簡單的方法是根據版本號或時間戳(最後寫入勝利)來選擇一個值,但這意味著丟失資料。所以,你可能需要在應用程式程式碼中額外做些更聰明的事情。
 
-以購物車為例,一種合理的合併值的方法就是做並集。在[圖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】可以以合理的方式自動合併,包括保留刪除。
 
 #### 版本向量
 
-[圖5-13](../img/fig5-13.png)中的示例只使用一個副本。當有多個副本但沒有領導者時,演算法如何修改?
+[圖 5-13](../img/fig5-13.png) 中的示例只使用一個副本。當有多個副本但沒有領導者時,演算法如何修改?
 
-[圖5-13](../img/fig5-13.png)使用單個版本號來捕獲操作之間的依賴關係,但是當多個副本併發接受寫入時,這是不夠的。相反,還需要在**每個副本**以及**每個鍵**使用版本號。每個副本在處理寫入時增加自己的版本號,並且跟蹤從其他副本中看到的版本號。這個資訊指出了要覆蓋哪些併發值,以及保留哪些併發值。
+[圖 5-13](../img/fig5-13.png) 使用單個版本號來捕獲操作之間的依賴關係,但是當多個副本併發接受寫入時,這是不夠的。相反,還需要在 **每個副本** 以及 **每個鍵** 使用版本號。每個副本在處理寫入時增加自己的版本號,並且跟蹤從其他副本中看到的版本號。這個資訊指出了要覆蓋哪些併發值,以及保留哪些併發值。
 
-所有副本的版本號集合稱為**版本向量(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)。版本向量允許資料庫區分覆蓋寫入和併發寫入。
 
 另外,就像在單個副本中的情況一樣,應用程式可能需要合併併發值。版本向量結構能夠確保從一個副本讀取並隨後寫回到另一個副本是安全的。這樣做雖然可能會在其他副本上面建立資料,但只要能正確合併就不會丟失資料。
 
 > #### 版本向量和向量時鐘
 >
-> 版本向量有時也被稱為向量時鐘,即使它們不完全相同。 差別很微妙——細節請參閱參考資料【57,60,61】。 簡而言之,在比較副本的狀態時,版本向量是正確的資料結構。
+> 版本向量有時也被稱為向量時鐘,即使它們不完全相同。 差別很微妙 —— 細節請參閱參考資料【57,60,61】。 簡而言之,在比較副本的狀態時,版本向量是正確的資料結構。
 >
 
 ## 本章小結
diff --git a/zh-tw/ch6.md b/zh-tw/ch6.md
index c2393ea..1224975 100644
--- a/zh-tw/ch6.md
+++ b/zh-tw/ch6.md
@@ -11,43 +11,44 @@
 
 [TOC]
 
-在[第五章](ch5.md)中,我們討論了複製——即資料在不同節點上的副本,對於非常大的資料集,或非常高的吞吐量,僅僅進行復制是不夠的:我們需要將資料進行**分割槽(partitions)**,也稱為**分片(sharding)**[^i]。
+在 [第五章](ch5.md) 中,我們討論了複製 —— 即資料在不同節點上的副本,對於非常大的資料集,或非常高的吞吐量,僅僅進行復制是不夠的:我們需要將資料進行 **分割槽(partitions)**,也稱為 **分片(sharding)**[^i]。
 
-[^i]: 正如本章所討論的,分割槽是一種有意將大型資料庫分解成小型資料庫的方式。它與 **網路分割槽(network partitions, netsplits)** 無關,這是節點之間網路故障的一種。我們將在[第八章](ch8.md)討論這些錯誤。
+[^i]: 正如本章所討論的,分割槽是一種有意將大型資料庫分解成小型資料庫的方式。它與 **網路分割槽(network partitions, netsplits)** 無關,這是節點之間網路故障的一種。我們將在 [第八章](ch8.md) 討論這些錯誤。
 
 > #### 術語澄清
 >
-> 上文中的**分割槽(partition)**,在MongoDB,Elasticsearch和Solr Cloud中被稱為**分片(shard)**,在HBase中稱之為**區域(Region)**,Bigtable中則是 **表塊(tablet)**,Cassandra和Riak中是**虛節點(vnode)**,Couchbase中叫做**虛桶(vBucket)**。但是**分割槽(partitioning)** 是最約定俗成的叫法。
+> 上文中的 **分割槽 (partition)**,在 MongoDB,Elasticsearch 和 Solr Cloud 中被稱為 **分片 (shard)**,在 HBase 中稱之為 **區域 (Region)**,Bigtable 中則是 **表塊(tablet)**,Cassandra 和 Riak 中是 **虛節點(vnode)**,Couchbase 中叫做 **虛桶 (vBucket)**。但是 **分割槽 (partitioning)** 是最約定俗成的叫法。
 >
 
 通常情況下,每條資料(每條記錄,每行或每個文件)屬於且僅屬於一個分割槽。有很多方法可以實現這一點,本章將進行深入討論。實際上,每個分割槽都是自己的小型資料庫,儘管資料庫可能支援同時進行多個分割槽的操作。
 
-分割槽主要是為了**可伸縮性**。不同的分割槽可以放在不共享叢集中的不同節點上(請參閱[第二部分](part-ii.md)關於[無共享架構](part-ii.md#無共享架構)的定義)。因此,大資料集可以分佈在多個磁碟上,並且查詢負載可以分佈在多個處理器上。
+分割槽主要是為了 **可伸縮性**。不同的分割槽可以放在不共享叢集中的不同節點上(請參閱 [第二部分](part-ii.md) 關於 [無共享架構](part-ii.md#無共享架構) 的定義)。因此,大資料集可以分佈在多個磁碟上,並且查詢負載可以分佈在多個處理器上。
 
 對於在單個分割槽上執行的查詢,每個節點可以獨立執行對自己的查詢,因此可以透過新增更多的節點來擴大查詢吞吐量。大型,複雜的查詢可能會跨越多個節點並行處理,儘管這也帶來了新的困難。
 
-分割槽資料庫在20世紀80年代由Teradata和NonStop SQL【1】等產品率先推出,最近因為NoSQL資料庫和基於Hadoop的資料倉庫重新被關注。有些系統是為事務性工作設計的,有些系統則用於分析(請參閱“[事務處理還是分析](ch3.md#事務處理還是分析)”):這種差異會影響系統的運作方式,但是分割槽的基本原理均適用於這兩種工作方式。
+分割槽資料庫在 20 世紀 80 年代由 Teradata 和 NonStop SQL【1】等產品率先推出,最近因為 NoSQL 資料庫和基於 Hadoop 的資料倉庫重新被關注。有些系統是為事務性工作設計的,有些系統則用於分析(請參閱 “[事務處理還是分析](ch3.md#事務處理還是分析)”):這種差異會影響系統的運作方式,但是分割槽的基本原理均適用於這兩種工作方式。
 
-在本章中,我們將首先介紹分割大型資料集的不同方法,並觀察索引如何與分割槽配合。然後我們將討論[分割槽再平衡(rebalancing)](#分割槽再平衡),如果想要新增或刪除叢集中的節點,則必須進行再平衡。最後,我們將概述資料庫如何將請求路由到正確的分割槽並執行查詢。
+在本章中,我們將首先介紹分割大型資料集的不同方法,並觀察索引如何與分割槽配合。然後我們將討論 [分割槽再平衡(rebalancing)](#分割槽再平衡),如果想要新增或刪除叢集中的節點,則必須進行再平衡。最後,我們將概述資料庫如何將請求路由到正確的分割槽並執行查詢。
 
 ## 分割槽與複製
 
-分割槽通常與複製結合使用,使得每個分割槽的副本儲存在多個節點上。 這意味著,即使每條記錄屬於一個分割槽,它仍然可以儲存在多個不同的節點上以獲得容錯能力。
+分割槽通常與複製結合使用,使得每個分割槽的副本儲存在多個節點上。這意味著,即使每條記錄屬於一個分割槽,它仍然可以儲存在多個不同的節點上以獲得容錯能力。
 
-一個節點可能儲存多個分割槽。 如果使用主從複製模型,則分割槽和複製的組合如[圖6-1](../img/fig6-1.png)所示。 每個分割槽領導者(主)被分配給一個節點,追隨者(從)被分配給其他節點。 每個節點可能是某些分割槽的領導者,同時是其他分割槽的追隨者。
-我們在[第五章](ch5.md)討論的關於資料庫複製的所有內容同樣適用於分割槽的複製。 大多數情況下,分割槽方案的選擇與複製方案的選擇是獨立的,為簡單起見,本章中將忽略複製。
+一個節點可能儲存多個分割槽。如果使用主從複製模型,則分割槽和複製的組合如 [圖 6-1](../img/fig6-1.png) 所示。每個分割槽領導者(主)被分配給一個節點,追隨者(從)被分配給其他節點。 每個節點可能是某些分割槽的領導者,同時是其他分割槽的追隨者。
+
+我們在 [第五章](ch5.md) 討論的關於資料庫複製的所有內容同樣適用於分割槽的複製。大多數情況下,分割槽方案的選擇與複製方案的選擇是獨立的,為簡單起見,本章中將忽略複製。
 
 ![](../img/fig6-1.png)
 
-**圖6-1 組合使用複製和分割槽:每個節點充當某些分割槽的領導者,其他分割槽充當追隨者。**
+**圖 6-1 組合使用複製和分割槽:每個節點充當某些分割槽的領導者,其他分割槽充當追隨者。**
 
 ## 鍵值資料的分割槽
 
 假設你有大量資料並且想要分割槽,如何決定在哪些節點上儲存哪些記錄呢?
 
-分割槽目標是將資料和查詢負載均勻分佈在各個節點上。如果每個節點公平分享資料和負載,那麼理論上10個節點應該能夠處理10倍的資料量和10倍的單個節點的讀寫吞吐量(暫時忽略複製)。
+分割槽目標是將資料和查詢負載均勻分佈在各個節點上。如果每個節點公平分享資料和負載,那麼理論上 10 個節點應該能夠處理 10 倍的資料量和 10 倍的單個節點的讀寫吞吐量(暫時忽略複製)。
 
-如果分割槽是不公平的,一些分割槽比其他分割槽有更多的資料或查詢,我們稱之為**偏斜(skew)**。資料偏斜的存在使分割槽效率下降很多。在極端的情況下,所有的負載可能壓在一個分割槽上,其餘9個節點空閒的,瓶頸落在這一個繁忙的節點上。不均衡導致的高負載的分割槽被稱為**熱點(hot spot)**。
+如果分割槽是不公平的,一些分割槽比其他分割槽有更多的資料或查詢,我們稱之為 **偏斜(skew)**。資料偏斜的存在使分割槽效率下降很多。在極端的情況下,所有的負載可能壓在一個分割槽上,其餘 9 個節點空閒的,瓶頸落在這一個繁忙的節點上。不均衡導致的高負載的分割槽被稱為 **熱點(hot spot)**。
 
 避免熱點最簡單的方法是將記錄隨機分配給節點。這將在所有節點上平均分配資料,但是它有一個很大的缺點:當你試圖讀取一個特定的值時,你無法知道它在哪個節點上,所以你必須並行地查詢所有的節點。
 
@@ -55,19 +56,19 @@
 
 ### 根據鍵的範圍分割槽
 
-一種分割槽的方法是為每個分割槽指定一塊連續的鍵範圍(從最小值到最大值),如紙質百科全書的卷([圖6-2](../img/fig6-2.png))。如果知道範圍之間的邊界,則可以輕鬆確定哪個分割槽包含某個值。如果你還知道分割槽所在的節點,那麼可以直接向相應的節點發出請求(對於百科全書而言,就像從書架上選取正確的書籍)。
+一種分割槽的方法是為每個分割槽指定一塊連續的鍵範圍(從最小值到最大值),如紙質百科全書的卷([圖 6-2](../img/fig6-2.png))。如果知道範圍之間的邊界,則可以輕鬆確定哪個分割槽包含某個值。如果你還知道分割槽所在的節點,那麼可以直接向相應的節點發出請求(對於百科全書而言,就像從書架上選取正確的書籍)。
 
 ![](../img/fig6-2.png)
 
-**圖6-2 印刷版百科全書按照關鍵字範圍進行分割槽**
+**圖 6-2 印刷版百科全書按照關鍵字範圍進行分割槽**
 
-鍵的範圍不一定均勻分佈,因為資料也很可能不均勻分佈。例如在[圖6-2](../img/fig6-2.png)中,第1捲包含以A和B開頭的單詞,但第12卷則包含以T,U,V,X,Y和Z開頭的單詞。只是簡單的規定每個捲包含兩個字母會導致一些卷比其他卷大。為了均勻分配資料,分割槽邊界需要依據資料調整。
+鍵的範圍不一定均勻分佈,因為資料也很可能不均勻分佈。例如在 [圖 6-2](../img/fig6-2.png) 中,第 1 捲包含以 A 和 B 開頭的單詞,但第 12 卷則包含以 T、U、V、X、Y 和 Z 開頭的單詞。只是簡單的規定每個捲包含兩個字母會導致一些卷比其他卷大。為了均勻分配資料,分割槽邊界需要依據資料調整。
 
-分割槽邊界可以由管理員手動選擇,也可以由資料庫自動選擇(我們會在“[分割槽再平衡](#分割槽再平衡)”中更詳細地討論分割槽邊界的選擇)。 Bigtable使用了這種分割槽策略,以及其開源等價物HBase 【2, 3】,RethinkDB和2.4版本之前的MongoDB 【4】。
+分割槽邊界可以由管理員手動選擇,也可以由資料庫自動選擇(我們會在 “[分割槽再平衡](#分割槽再平衡)” 中更詳細地討論分割槽邊界的選擇)。 Bigtable 使用了這種分割槽策略,以及其開源等價物 HBase 【2, 3】,RethinkDB 和 2.4 版本之前的 MongoDB 【4】。
 
-在每個分割槽中,我們可以按照一定的順序儲存鍵(請參閱“[SSTables和LSM樹](ch3.md#SSTables和LSM樹)”)。好處是進行範圍掃描非常簡單,你可以將鍵作為聯合索引來處理,以便在一次查詢中獲取多個相關記錄(請參閱“[多列索引](ch3.md#多列索引)”)。例如,假設我們有一個程式來儲存感測器網路的資料,其中主鍵是測量的時間戳(年月日時分秒)。範圍掃描在這種情況下非常有用,因為我們可以輕鬆獲取某個月份的所有資料。
+在每個分割槽中,我們可以按照一定的順序儲存鍵(請參閱 “[SSTables 和 LSM 樹](ch3.md#SSTables和LSM樹)”)。好處是進行範圍掃描非常簡單,你可以將鍵作為聯合索引來處理,以便在一次查詢中獲取多個相關記錄(請參閱 “[多列索引](ch3.md#多列索引)”)。例如,假設我們有一個程式來儲存感測器網路的資料,其中主鍵是測量的時間戳(年月日時分秒)。範圍掃描在這種情況下非常有用,因為我們可以輕鬆獲取某個月份的所有資料。
 
-然而,Key Range分割槽的缺點是某些特定的訪問模式會導致熱點。 如果主鍵是時間戳,則分割槽對應於時間範圍,例如,給每天分配一個分割槽。 不幸的是,由於我們在測量發生時將資料從感測器寫入資料庫,因此所有寫入操作都會轉到同一個分割槽(即今天的分割槽),這樣分割槽可能會因寫入而過載,而其他分割槽則處於空閒狀態【5】。
+然而,Key Range 分割槽的缺點是某些特定的訪問模式會導致熱點。 如果主鍵是時間戳,則分割槽對應於時間範圍,例如,給每天分配一個分割槽。 不幸的是,由於我們在測量發生時將資料從感測器寫入資料庫,因此所有寫入操作都會轉到同一個分割槽(即今天的分割槽),這樣分割槽可能會因寫入而過載,而其他分割槽則處於空閒狀態【5】。
 
 為了避免感測器資料庫中的這個問題,需要使用除了時間戳以外的其他東西作為主鍵的第一個部分。 例如,可以在每個時間戳前新增感測器名稱,這樣會首先按感測器名稱,然後按時間進行分割槽。 假設有多個感測器同時執行,寫入負載將最終均勻分佈在不同分割槽上。 現在,當想要在一個時間範圍內獲取多個感測器的值時,你需要為每個感測器名稱執行一個單獨的範圍查詢。
 
@@ -75,39 +76,39 @@
 
 由於偏斜和熱點的風險,許多分散式資料儲存使用雜湊函式來確定給定鍵的分割槽。
 
-一個好的雜湊函式可以將偏斜的資料均勻分佈。假設你有一個32位雜湊函式,無論何時給定一個新的字串輸入,它將返回一個0到$2^{32}$ -1之間的“隨機”數。即使輸入的字串非常相似,它們的雜湊也會均勻分佈在這個數字範圍內。
+一個好的雜湊函式可以將偏斜的資料均勻分佈。假設你有一個 32 位雜湊函式,無論何時給定一個新的字串輸入,它將返回一個 0 到 $2^{32}$ -1 之間的 “隨機” 數。即使輸入的字串非常相似,它們的雜湊也會均勻分佈在這個數字範圍內。
 
-出於分割槽的目的,雜湊函式不需要多麼強壯的加密演算法:例如,Cassandra和MongoDB使用MD5,Voldemort使用Fowler-Noll-Vo函式。許多程式語言都有內建的簡單雜湊函式(它們用於散列表),但是它們可能不適合分割槽:例如,在Java的`Object.hashCode()`和Ruby的`Object#hash`,同一個鍵可能在不同的程序中有不同的雜湊值【6】。
+出於分割槽的目的,雜湊函式不需要多麼強壯的加密演算法:例如,Cassandra 和 MongoDB 使用 MD5,Voldemort 使用 Fowler-Noll-Vo 函式。許多程式語言都有內建的簡單雜湊函式(它們用於散列表),但是它們可能不適合分割槽:例如,在 Java 的 `Object.hashCode()` 和 Ruby 的 `Object#hash`,同一個鍵可能在不同的程序中有不同的雜湊值【6】。
 
-一旦你有一個合適的鍵雜湊函式,你可以為每個分割槽分配一個雜湊範圍(而不是鍵的範圍),每個透過雜湊雜湊落在分割槽範圍內的鍵將被儲存在該分割槽中。如[圖6-3](../img/fig6-3.png)所示。
+一旦你有一個合適的鍵雜湊函式,你可以為每個分割槽分配一個雜湊範圍(而不是鍵的範圍),每個透過雜湊雜湊落在分割槽範圍內的鍵將被儲存在該分割槽中。如 [圖 6-3](../img/fig6-3.png) 所示。
 
 ![](../img/fig6-3.png)
 
-**圖6-3 按雜湊鍵分割槽**
+**圖 6-3 按雜湊鍵分割槽**
 
-這種技術擅長在分割槽之間公平地分配鍵。分割槽邊界可以是均勻間隔的,也可以是偽隨機選擇的(在這種情況下,該技術有時也被稱為**一致性雜湊**,即consistent hashing)。
+這種技術擅長在分割槽之間公平地分配鍵。分割槽邊界可以是均勻間隔的,也可以是偽隨機選擇的(在這種情況下,該技術有時也被稱為 **一致性雜湊**,即 consistent hashing)。
 
 > #### 一致性雜湊
 >
-> 一致性雜湊由Karger等人定義。【7】 用於跨網際網路級別的快取系統,例如CDN中,是一種能均勻分配負載的方法。它使用隨機選擇的 **分割槽邊界(partition boundaries)** 來避免中央控制或分散式共識的需要。 請注意,這裡的一致性與複製一致性(請參閱[第五章](ch5.md))或ACID一致性(請參閱[第七章](ch7.md))無關,而只是描述了一種重新平衡(reblancing)的特定方法。
+> 一致性雜湊由 Karger 等人定義。【7】 用於跨網際網路級別的快取系統,例如 CDN 中,是一種能均勻分配負載的方法。它使用隨機選擇的 **分割槽邊界(partition boundaries)** 來避免中央控制或分散式共識的需要。 請注意,這裡的一致性與複製一致性(請參閱 [第五章](ch5.md))或 ACID 一致性(請參閱 [第七章](ch7.md))無關,而只是描述了一種重新平衡(reblancing)的特定方法。
 >
-> 正如我們將在“[分割槽再平衡](#分割槽再平衡)”中所看到的,這種特殊的方法對於資料庫實際上並不是很好,所以在實際中很少使用(某些資料庫的文件仍然會使用一致性雜湊的說法,但是它往往是不準確的)。 因為有可能產生混淆,所以最好避免使用一致性雜湊這個術語,而只是把它稱為**雜湊分割槽(hash partitioning)**。
+> 正如我們將在 “[分割槽再平衡](#分割槽再平衡)” 中所看到的,這種特殊的方法對於資料庫實際上並不是很好,所以在實際中很少使用(某些資料庫的文件仍然會使用一致性雜湊的說法,但是它往往是不準確的)。 因為有可能產生混淆,所以最好避免使用一致性雜湊這個術語,而只是把它稱為 **雜湊分割槽(hash partitioning)**。
 
-不幸的是,透過使用鍵雜湊進行分割槽,我們失去了鍵範圍分割槽的一個很好的屬性:高效執行範圍查詢的能力。曾經相鄰的鍵現在分散在所有分割槽中,所以它們之間的順序就丟失了。在MongoDB中,如果你使用了基於雜湊的分割槽模式,則任何範圍查詢都必須傳送到所有分割槽【4】。Riak【9】、Couchbase 【10】或Voldemort不支援主鍵上的範圍查詢。
+不幸的是,透過使用鍵雜湊進行分割槽,我們失去了鍵範圍分割槽的一個很好的屬性:高效執行範圍查詢的能力。曾經相鄰的鍵現在分散在所有分割槽中,所以它們之間的順序就丟失了。在 MongoDB 中,如果你使用了基於雜湊的分割槽模式,則任何範圍查詢都必須傳送到所有分割槽【4】。Riak【9】、Couchbase 【10】或 Voldemort 不支援主鍵上的範圍查詢。
 
-Cassandra採取了折衷的策略【11, 12, 13】。 Cassandra中的表可以使用由多個列組成的複合主鍵來宣告。鍵中只有第一列會作為雜湊的依據,而其他列則被用作Casssandra的SSTables中排序資料的連線索引。儘管查詢無法在複合主鍵的第一列中按範圍掃表,但如果第一列已經指定了固定值,則可以對該鍵的其他列執行有效的範圍掃描。
+Cassandra 採取了折衷的策略【11, 12, 13】。 Cassandra 中的表可以使用由多個列組成的複合主鍵來宣告。鍵中只有第一列會作為雜湊的依據,而其他列則被用作 Casssandra 的 SSTables 中排序資料的連線索引。儘管查詢無法在複合主鍵的第一列中按範圍掃表,但如果第一列已經指定了固定值,則可以對該鍵的其他列執行有效的範圍掃描。
 
-組合索引方法為一對多關係提供了一個優雅的資料模型。例如,在社交媒體網站上,一個使用者可能會發布很多更新。如果更新的主鍵被選擇為`(user_id, update_timestamp)`,那麼你可以有效地檢索特定使用者在某個時間間隔內按時間戳排序的所有更新。不同的使用者可以儲存在不同的分割槽上,對於每個使用者,更新按時間戳順序儲存在單個分割槽上。
+組合索引方法為一對多關係提供了一個優雅的資料模型。例如,在社交媒體網站上,一個使用者可能會發布很多更新。如果更新的主鍵被選擇為 `(user_id, update_timestamp)`,那麼你可以有效地檢索特定使用者在某個時間間隔內按時間戳排序的所有更新。不同的使用者可以儲存在不同的分割槽上,對於每個使用者,更新按時間戳順序儲存在單個分割槽上。
 
 ### 負載偏斜與熱點消除
 
 如前所述,雜湊分割槽可以幫助減少熱點。但是,它不能完全避免它們:在極端情況下,所有的讀寫操作都是針對同一個鍵的,所有的請求都會被路由到同一個分割槽。
 
-這種場景也許並不常見,但並非聞所未聞:例如,在社交媒體網站上,一個擁有數百萬追隨者的名人使用者在做某事時可能會引發一場風暴【14】。這個事件可能導致同一個鍵的大量寫入(鍵可能是名人的使用者ID,或者人們正在評論的動作的ID)。雜湊策略不起作用,因為兩個相同ID的雜湊值仍然是相同的。
+這種場景也許並不常見,但並非聞所未聞:例如,在社交媒體網站上,一個擁有數百萬追隨者的名人使用者在做某事時可能會引發一場風暴【14】。這個事件可能導致同一個鍵的大量寫入(鍵可能是名人的使用者 ID,或者人們正在評論的動作的 ID)。雜湊策略不起作用,因為兩個相同 ID 的雜湊值仍然是相同的。
 
-如今,大多數資料系統無法自動補償這種高度偏斜的負載,因此應用程式有責任減少偏斜。例如,如果一個主鍵被認為是非常火爆的,一個簡單的方法是在主鍵的開始或結尾新增一個隨機數。只要一個兩位數的十進位制隨機數就可以將主鍵分散為100種不同的主鍵,從而儲存在不同的分割槽中。
+如今,大多數資料系統無法自動補償這種高度偏斜的負載,因此應用程式有責任減少偏斜。例如,如果一個主鍵被認為是非常火爆的,一個簡單的方法是在主鍵的開始或結尾新增一個隨機數。只要一個兩位數的十進位制隨機數就可以將主鍵分散為 100 種不同的主鍵,從而儲存在不同的分割槽中。
 
-然而,將主鍵進行分割之後,任何讀取都必須要做額外的工作,因為他們必須從所有100個主鍵分佈中讀取資料並將其合併。此技術還需要額外的記錄:只需要對少量熱點附加隨機數;對於寫入吞吐量低的絕大多數主鍵來說是不必要的開銷。因此,你還需要一些方法來跟蹤哪些鍵需要被分割。
+然而,將主鍵進行分割之後,任何讀取都必須要做額外的工作,因為他們必須從所有 100 個主鍵分佈中讀取資料並將其合併。此技術還需要額外的記錄:只需要對少量熱點附加隨機數;對於寫入吞吐量低的絕大多數主鍵來說是不必要的開銷。因此,你還需要一些方法來跟蹤哪些鍵需要被分割。
 
 也許在將來,資料系統將能夠自動檢測和補償偏斜的工作負載;但現在,你需要自己來權衡。
 
@@ -117,132 +118,132 @@ Cassandra採取了折衷的策略【11, 12, 13】。 Cassandra中的表可以使
 
 到目前為止,我們討論的分割槽方案依賴於鍵值資料模型。如果只通過主鍵訪問記錄,我們可以從該鍵確定分割槽,並使用它來將讀寫請求路由到負責該鍵的分割槽。
 
-如果涉及次級索引,情況會變得更加複雜(參考“[其他索引結構](ch3.md#其他索引結構)”)。次級索引通常並不能唯一地標識記錄,而是一種搜尋記錄中出現特定值的方式:查詢使用者123的所有操作,查詢包含詞語`hogwash`的所有文章,查詢所有顏色為紅色的車輛等等。
+如果涉及次級索引,情況會變得更加複雜(參考 “[其他索引結構](ch3.md#其他索引結構)”)。次級索引通常並不能唯一地標識記錄,而是一種搜尋記錄中出現特定值的方式:查詢使用者 123 的所有操作,查詢包含詞語 `hogwash` 的所有文章,查詢所有顏色為紅色的車輛等等。
 
-次級索引是關係型資料庫的基礎,並且在文件資料庫中也很普遍。許多鍵值儲存(如HBase和Volde-mort)為了減少實現的複雜度而放棄了次級索引,但是一些(如Riak)已經開始新增它們,因為它們對於資料模型實在是太有用了。並且次級索引也是Solr和Elasticsearch等搜尋伺服器的基石。
+次級索引是關係型資料庫的基礎,並且在文件資料庫中也很普遍。許多鍵值儲存(如 HBase 和 Volde-mort)為了減少實現的複雜度而放棄了次級索引,但是一些(如 Riak)已經開始新增它們,因為它們對於資料模型實在是太有用了。並且次級索引也是 Solr 和 Elasticsearch 等搜尋伺服器的基石。
 
-次級索引的問題是它們不能整齊地對映到分割槽。有兩種用次級索引對資料庫進行分割槽的方法:**基於文件的分割槽(document-based)** 和**基於關鍵詞(term-based)的分割槽**。
+次級索引的問題是它們不能整齊地對映到分割槽。有兩種用次級索引對資料庫進行分割槽的方法:**基於文件的分割槽(document-based)** 和 **基於關鍵詞(term-based)的分割槽**。
 
 ### 基於文件的次級索引進行分割槽
 
-假設你正在經營一個銷售二手車的網站(如[圖6-4](../img/fig6-4.png)所示)。 每個列表都有一個唯一的ID——稱之為文件ID——並且用文件ID對資料庫進行分割槽(例如,分割槽0中的ID 0到499,分割槽1中的ID 500到999等)。
+假設你正在經營一個銷售二手車的網站(如 [圖 6-4](../img/fig6-4.png) 所示)。 每個列表都有一個唯一的 ID—— 稱之為文件 ID—— 並且用文件 ID 對資料庫進行分割槽(例如,分割槽 0 中的 ID 0 到 499,分割槽 1 中的 ID 500 到 999 等)。
 
-你想讓使用者搜尋汽車,允許他們透過顏色和廠商過濾,所以需要一個在顏色和廠商上的次級索引(文件資料庫中這些是**欄位(field)**,關係資料庫中這些是**列(column)** )。 如果你聲明瞭索引,則資料庫可以自動執行索引[^ii]。例如,無論何時將紅色汽車新增到資料庫,資料庫分割槽都會自動將其新增到索引條目`color:red`的文件ID列表中。
+你想讓使用者搜尋汽車,允許他們透過顏色和廠商過濾,所以需要一個在顏色和廠商上的次級索引(文件資料庫中這些是 **欄位(field)**,關係資料庫中這些是 **列(column)** )。 如果你聲明瞭索引,則資料庫可以自動執行索引 [^ii]。例如,無論何時將紅色汽車新增到資料庫,資料庫分割槽都會自動將其新增到索引條目 `color:red` 的文件 ID 列表中。
 
-[^ii]: 如果資料庫僅支援鍵值模型,則你可能會嘗試在應用程式程式碼中建立從值到文件ID的對映來實現次級索引。 如果沿著這條路線走下去,請萬分小心,確保你的索引與底層資料保持一致。 競爭條件和間歇性寫入失敗(其中一些更改已儲存,但其他更改未儲存)很容易導致資料不同步 - 請參閱“[多物件事務的需求](ch7.md#多物件事務的需求)”。
+[^ii]: 如果資料庫僅支援鍵值模型,則你可能會嘗試在應用程式程式碼中建立從值到文件 ID 的對映來實現次級索引。 如果沿著這條路線走下去,請萬分小心,確保你的索引與底層資料保持一致。 競爭條件和間歇性寫入失敗(其中一些更改已儲存,但其他更改未儲存)很容易導致資料不同步 - 請參閱 “[多物件事務的需求](ch7.md#多物件事務的需求)”。
 
 ![](../img/fig6-4.png)
 
-**圖6-4 基於文件的次級索引進行分割槽**
+**圖 6-4 基於文件的次級索引進行分割槽**
 
-在這種索引方法中,每個分割槽是完全獨立的:每個分割槽維護自己的次級索引,僅覆蓋該分割槽中的文件。它不關心儲存在其他分割槽的資料。無論何時你需要寫入資料庫(新增,刪除或更新文件),只需處理包含你正在編寫的文件ID的分割槽即可。出於這個原因,**文件分割槽索引**也被稱為**本地索引**(而不是將在下一節中描述的**全域性索引**)。
+在這種索引方法中,每個分割槽是完全獨立的:每個分割槽維護自己的次級索引,僅覆蓋該分割槽中的文件。它不關心儲存在其他分割槽的資料。無論何時你需要寫入資料庫(新增,刪除或更新文件),只需處理包含你正在編寫的文件 ID 的分割槽即可。出於這個原因,**文件分割槽索引** 也被稱為 **本地索引**(而不是將在下一節中描述的 **全域性索引**)。
 
-但是,從文件分割槽索引中讀取需要注意:除非你對文件ID做了特別的處理,否則沒有理由將所有具有特定顏色或特定品牌的汽車放在同一個分割槽中。在[圖6-4](../img/fig6-4.png)中,紅色汽車出現在分割槽0和分割槽1中。因此,如果要搜尋紅色汽車,則需要將查詢傳送到所有分割槽,併合並所有返回的結果。
+但是,從文件分割槽索引中讀取需要注意:除非你對文件 ID 做了特別的處理,否則沒有理由將所有具有特定顏色或特定品牌的汽車放在同一個分割槽中。在 [圖 6-4](../img/fig6-4.png) 中,紅色汽車出現在分割槽 0 和分割槽 1 中。因此,如果要搜尋紅色汽車,則需要將查詢傳送到所有分割槽,併合並所有返回的結果。
 
 
-這種查詢分割槽資料庫的方法有時被稱為**分散/聚集(scatter/gather)**,並且可能會使次級索引上的讀取查詢相當昂貴。即使並行查詢分割槽,分散/聚集也容易導致尾部延遲放大(請參閱“[實踐中的百分位點](ch1.md#實踐中的百分位點)”)。然而,它被廣泛使用:MongoDB,Riak 【15】,Cassandra 【16】,Elasticsearch 【17】,SolrCloud 【18】和VoltDB 【19】都使用文件分割槽次級索引。大多數資料庫供應商建議你構建一個能從單個分割槽提供次級索引查詢的分割槽方案,但這並不總是可行,尤其是當在單個查詢中使用多個次級索引時(例如同時需要按顏色和製造商查詢)。
+這種查詢分割槽資料庫的方法有時被稱為 **分散 / 聚集(scatter/gather)**,並且可能會使次級索引上的讀取查詢相當昂貴。即使並行查詢分割槽,分散 / 聚集也容易導致尾部延遲放大(請參閱 “[實踐中的百分位點](ch1.md#實踐中的百分位點)”)。然而,它被廣泛使用:MongoDB,Riak 【15】,Cassandra 【16】,Elasticsearch 【17】,SolrCloud 【18】和 VoltDB 【19】都使用文件分割槽次級索引。大多數資料庫供應商建議你構建一個能從單個分割槽提供次級索引查詢的分割槽方案,但這並不總是可行,尤其是當在單個查詢中使用多個次級索引時(例如同時需要按顏色和製造商查詢)。
 
 
 ### 基於關鍵詞(Term)的次級索引進行分割槽
 
-我們可以構建一個覆蓋所有分割槽資料的**全域性索引**,而不是給每個分割槽建立自己的次級索引(本地索引)。但是,我們不能只把這個索引儲存在一個節點上,因為它可能會成為瓶頸,違背了分割槽的目的。全域性索引也必須進行分割槽,但可以採用與主鍵不同的分割槽方式。
+我們可以構建一個覆蓋所有分割槽資料的 **全域性索引**,而不是給每個分割槽建立自己的次級索引(本地索引)。但是,我們不能只把這個索引儲存在一個節點上,因為它可能會成為瓶頸,違背了分割槽的目的。全域性索引也必須進行分割槽,但可以採用與主鍵不同的分割槽方式。
 
-[圖6-5](../img/fig6-5.png)描述了這可能是什麼樣子:來自所有分割槽的紅色汽車在紅色索引中,並且索引是分割槽的,首字母從`a`到`r`的顏色在分割槽0中,`s`到`z`的在分割槽1。汽車製造商的索引也與之類似(分割槽邊界在`f`和`h`之間)。
+[圖 6-5](../img/fig6-5.png) 描述了這可能是什麼樣子:來自所有分割槽的紅色汽車在紅色索引中,並且索引是分割槽的,首字母從 `a` 到 `r` 的顏色在分割槽 0 中,`s` 到 `z` 的在分割槽 1。汽車製造商的索引也與之類似(分割槽邊界在 `f` 和 `h` 之間)。
 
 ![](../img/fig6-5.png)
 
-**圖6-5 基於關鍵詞對次級索引進行分割槽**
+**圖 6-5 基於關鍵詞對次級索引進行分割槽**
 
-我們將這種索引稱為**關鍵詞分割槽(term-partitioned)**,因為我們尋找的關鍵詞決定了索引的分割槽方式。例如,一個關鍵詞可能是:`color:red`。**關鍵詞(Term)** 這個名稱來源於全文搜尋索引(一種特殊的次級索引),指文件中出現的所有單詞。
+我們將這種索引稱為 **關鍵詞分割槽(term-partitioned)**,因為我們尋找的關鍵詞決定了索引的分割槽方式。例如,一個關鍵詞可能是:`color:red`。**關鍵詞(Term)** 這個名稱來源於全文搜尋索引(一種特殊的次級索引),指文件中出現的所有單詞。
 
-和之前一樣,我們可以透過**關鍵詞**本身或者它的雜湊進行索引分割槽。根據關鍵詞本身來分割槽對於範圍掃描非常有用(例如對於數值類的屬性,像汽車的報價),而對關鍵詞的雜湊分割槽提供了負載均衡的能力。
+和之前一樣,我們可以透過 **關鍵詞** 本身或者它的雜湊進行索引分割槽。根據關鍵詞本身來分割槽對於範圍掃描非常有用(例如對於數值類的屬性,像汽車的報價),而對關鍵詞的雜湊分割槽提供了負載均衡的能力。
 
-關鍵詞分割槽的全域性索引優於文件分割槽索引的地方點是它可以使讀取更有效率:不需要**分散/收集**所有分割槽,客戶端只需要向包含關鍵詞的分割槽發出請求。全域性索引的缺點在於寫入速度較慢且較為複雜,因為寫入單個文件現在可能會影響索引的多個分割槽(文件中的每個關鍵詞可能位於不同的分割槽或者不同的節點上) 。
+關鍵詞分割槽的全域性索引優於文件分割槽索引的地方點是它可以使讀取更有效率:不需要 **分散 / 收集** 所有分割槽,客戶端只需要向包含關鍵詞的分割槽發出請求。全域性索引的缺點在於寫入速度較慢且較為複雜,因為寫入單個文件現在可能會影響索引的多個分割槽(文件中的每個關鍵詞可能位於不同的分割槽或者不同的節點上) 。
 
-理想情況下,索引總是最新的,寫入資料庫的每個文件都會立即反映在索引中。但是,在關鍵詞分割槽索引中,這需要跨分割槽的分散式事務,並不是所有資料庫都支援(請參閱[第七章](ch7.md)和[第九章](ch9.md))。
+理想情況下,索引總是最新的,寫入資料庫的每個文件都會立即反映在索引中。但是,在關鍵詞分割槽索引中,這需要跨分割槽的分散式事務,並不是所有資料庫都支援(請參閱 [第七章](ch7.md) 和 [第九章](ch9.md))。
 
-在實踐中,對全域性次級索引的更新通常是**非同步**的(也就是說,如果在寫入之後不久讀取索引,剛才所做的更改可能尚未反映在索引中)。例如,Amazon DynamoDB聲稱在正常情況下,其全域性次級索引會在不到一秒的時間內更新,但在基礎架構出現故障的情況下可能會有延遲【20】。
+在實踐中,對全域性次級索引的更新通常是 **非同步** 的(也就是說,如果在寫入之後不久讀取索引,剛才所做的更改可能尚未反映在索引中)。例如,Amazon DynamoDB 聲稱在正常情況下,其全域性次級索引會在不到一秒的時間內更新,但在基礎架構出現故障的情況下可能會有延遲【20】。
 
-全域性關鍵詞分割槽索引的其他用途包括Riak的搜尋功能【21】和Oracle資料倉庫,它允許你在本地和全域性索引之間進行選擇【22】。我們將在[第十二章](ch12.md)中繼續關鍵詞分割槽次級索引實現的話題。
+全域性關鍵詞分割槽索引的其他用途包括 Riak 的搜尋功能【21】和 Oracle 資料倉庫,它允許你在本地和全域性索引之間進行選擇【22】。我們將在 [第十二章](ch12.md) 中繼續關鍵詞分割槽次級索引實現的話題。
 
 ## 分割槽再平衡
 
 隨著時間的推移,資料庫會有各種變化:
 
-* 查詢吞吐量增加,所以你想要新增更多的CPU來處理負載。
-* 資料集大小增加,所以你想新增更多的磁碟和RAM來儲存它。
+* 查詢吞吐量增加,所以你想要新增更多的 CPU 來處理負載。
+* 資料集大小增加,所以你想新增更多的磁碟和 RAM 來儲存它。
 * 機器出現故障,其他機器需要接管故障機器的責任。
 
-所有這些更改都需要資料和請求從一個節點移動到另一個節點。 將負載從叢集中的一個節點向另一個節點移動的過程稱為**再平衡(rebalancing)**。
+所有這些更改都需要資料和請求從一個節點移動到另一個節點。 將負載從叢集中的一個節點向另一個節點移動的過程稱為 **再平衡(rebalancing)**。
 
 無論使用哪種分割槽方案,再平衡通常都要滿足一些最低要求:
 
 * 再平衡之後,負載(資料儲存,讀取和寫入請求)應該在叢集中的節點之間公平地共享。
 * 再平衡發生時,資料庫應該繼續接受讀取和寫入。
-* 節點之間只移動必須的資料,以便快速再平衡,並減少網路和磁碟I/O負載。
+* 節點之間只移動必須的資料,以便快速再平衡,並減少網路和磁碟 I/O 負載。
 
 
 ### 再平衡策略
 
-有幾種不同的分割槽分配方法【23】,讓我們依次簡要討論一下。
+有幾種不同的分割槽分配方法【23】,讓我們依次簡要討論一下。
 
 #### 反面教材:hash mod N
 
-我們在前面說過([圖6-3](../img/fig6-3.png)),最好將可能的雜湊分成不同的範圍,並將每個範圍分配給一個分割槽(例如,如果$0≤hash(key)<b_0$,則將鍵分配給分割槽0,如果$b_0 ≤ hash(key) <b_1$,則分配給分割槽1)
+我們在前面說過([圖 6-3](../img/fig6-3.png)),最好將可能的雜湊分成不同的範圍,並將每個範圍分配給一個分割槽(例如,如果 $0 ≤ hash(key)< b_0$,則將鍵分配給分割槽 0,如果 $b_0 ≤ hash(key) < b_1$,則分配給分割槽 1)
 
-也許你想知道為什麼我們不使用 ***取模(mod)***(許多程式語言中的%運算子)。例如,`hash(key) mod 10`會返回一個介於0和9之間的數字(如果我們將雜湊寫為十進位制數,雜湊模10將是最後一個數字)。如果我們有10個節點,編號為0到9,這似乎是將每個鍵分配給一個節點的簡單方法。
+也許你想知道為什麼我們不使用 ***取模(mod)***(許多程式語言中的 % 運算子)。例如,`hash(key) mod 10` 會返回一個介於 0 和 9 之間的數字(如果我們將雜湊寫為十進位制數,雜湊模 10 將是最後一個數字)。如果我們有 10 個節點,編號為 0 到 9,這似乎是將每個鍵分配給一個節點的簡單方法。
 
-模N($mod N$)方法的問題是,如果節點數量N發生變化,大多數鍵將需要從一個節點移動到另一個節點。例如,假設$hash(key)=123456$。如果最初有10個節點,那麼這個鍵一開始放在節點6上(因為$123456\ mod\  10 = 6$)。當你增長到11個節點時,鍵需要移動到節點3($123456\ mod\ 11 = 3$),當你增長到12個節點時,需要移動到節點0($123456\ mod\ 12 = 0$)。這種頻繁的舉動使得重新平衡過於昂貴。
+模 N($mod N$)方法的問題是,如果節點數量 N 發生變化,大多數鍵將需要從一個節點移動到另一個節點。例如,假設 $hash(key)=123456$。如果最初有 10 個節點,那麼這個鍵一開始放在節點 6 上(因為 $123456\ mod\  10 = 6$)。當你增長到 11 個節點時,鍵需要移動到節點 3($123456\ mod\ 11 = 3$),當你增長到 12 個節點時,需要移動到節點 0($123456\ mod\ 12 = 0$)。這種頻繁的舉動使得重新平衡過於昂貴。
 
 我們需要一種只移動必需資料的方法。
 
 #### 固定數量的分割槽
 
-幸運的是,有一個相當簡單的解決方案:建立比節點更多的分割槽,併為每個節點分配多個分割槽。例如,執行在10個節點的叢集上的資料庫可能會從一開始就被拆分為1,000個分割槽,因此大約有100個分割槽被分配給每個節點。
+幸運的是,有一個相當簡單的解決方案:建立比節點更多的分割槽,併為每個節點分配多個分割槽。例如,執行在 10 個節點的叢集上的資料庫可能會從一開始就被拆分為 1,000 個分割槽,因此大約有 100 個分割槽被分配給每個節點。
 
-現在,如果一個節點被新增到叢集中,新節點可以從當前每個節點中**竊取**一些分割槽,直到分割槽再次公平分配。這個過程如[圖6-6](../img/fig6-6.png)所示。如果從叢集中刪除一個節點,則會發生相反的情況。
+現在,如果一個節點被新增到叢集中,新節點可以從當前每個節點中 **竊取** 一些分割槽,直到分割槽再次公平分配。這個過程如 [圖 6-6](../img/fig6-6.png) 所示。如果從叢集中刪除一個節點,則會發生相反的情況。
 
 只有分割槽在節點之間的移動。分割槽的數量不會改變,鍵所指定的分割槽也不會改變。唯一改變的是分割槽所在的節點。這種變更並不是即時的 — 在網路上傳輸大量的資料需要一些時間 — 所以在傳輸過程中,原有分割槽仍然會接受讀寫操作。
 
 ![](../img/fig6-6.png)
 
-**圖6-6 將新節點新增到每個節點具有多個分割槽的資料庫叢集。**
+**圖 6-6 將新節點新增到每個節點具有多個分割槽的資料庫叢集。**
 
-原則上,你甚至可以解決叢集中的硬體不匹配問題:透過為更強大的節點分配更多的分割槽,可以強制這些節點承載更多的負載。在Riak 【15】,Elasticsearch 【24】,Couchbase 【10】和Voldemort 【25】中使用了這種再平衡的方法。
+原則上,你甚至可以解決叢集中的硬體不匹配問題:透過為更強大的節點分配更多的分割槽,可以強制這些節點承載更多的負載。在 Riak 【15】、Elasticsearch 【24】、Couchbase 【10】和 Voldemort 【25】中使用了這種再平衡的方法。
 
 在這種配置中,分割槽的數量通常在資料庫第一次建立時確定,之後不會改變。雖然原則上可以分割和合並分割槽(請參閱下一節),但固定數量的分割槽在操作上更簡單,因此許多固定分割槽資料庫選擇不實施分割槽分割。因此,一開始配置的分割槽數就是你可以擁有的最大節點數量,所以你需要選擇足夠多的分割槽以適應未來的增長。但是,每個分割槽也有管理開銷,所以選擇太大的數字會適得其反。
 
-如果資料集的總大小難以預估(例如,可能它開始很小,但隨著時間的推移會變得更大),選擇正確的分割槽數是困難的。由於每個分割槽包含了總資料量固定比率的資料,因此每個分割槽的大小與叢集中的資料總量成比例增長。如果分割槽非常大,再平衡和從節點故障恢復變得昂貴。但是,如果分割槽太小,則會產生太多的開銷。當分割槽大小“恰到好處”的時候才能獲得很好的效能,如果分割槽數量固定,但資料量變動很大,則難以達到最佳效能。
+如果資料集的總大小難以預估(例如,可能它開始很小,但隨著時間的推移會變得更大),選擇正確的分割槽數是困難的。由於每個分割槽包含了總資料量固定比率的資料,因此每個分割槽的大小與叢集中的資料總量成比例增長。如果分割槽非常大,再平衡和從節點故障恢復變得昂貴。但是,如果分割槽太小,則會產生太多的開銷。當分割槽大小 “恰到好處” 的時候才能獲得很好的效能,如果分割槽數量固定,但資料量變動很大,則難以達到最佳效能。
 
 #### 動態分割槽
 
-對於使用鍵範圍分割槽的資料庫(請參閱“[根據鍵的範圍分割槽](#根據鍵的範圍分割槽)”),具有固定邊界的固定數量的分割槽將非常不便:如果出現邊界錯誤,則可能會導致一個分割槽中的所有資料或者其他分割槽中的所有資料為空。手動重新配置分割槽邊界將非常繁瑣。
+對於使用鍵範圍分割槽的資料庫(請參閱 “[根據鍵的範圍分割槽](#根據鍵的範圍分割槽)”),具有固定邊界的固定數量的分割槽將非常不便:如果出現邊界錯誤,則可能會導致一個分割槽中的所有資料或者其他分割槽中的所有資料為空。手動重新配置分割槽邊界將非常繁瑣。
 
-出於這個原因,按鍵的範圍進行分割槽的資料庫(如HBase和RethinkDB)會動態建立分割槽。當分割槽增長到超過配置的大小時(在HBase上,預設值是10GB),會被分成兩個分割槽,每個分割槽約佔一半的資料【26】。與之相反,如果大量資料被刪除並且分割槽縮小到某個閾值以下,則可以將其與相鄰分割槽合併。此過程與B樹頂層發生的過程類似(請參閱“[B樹](ch3.md#B樹)”)。
+出於這個原因,按鍵的範圍進行分割槽的資料庫(如 HBase 和 RethinkDB)會動態建立分割槽。當分割槽增長到超過配置的大小時(在 HBase 上,預設值是 10GB),會被分成兩個分割槽,每個分割槽約佔一半的資料【26】。與之相反,如果大量資料被刪除並且分割槽縮小到某個閾值以下,則可以將其與相鄰分割槽合併。此過程與 B 樹頂層發生的過程類似(請參閱 “[B 樹](ch3.md#B樹)”)。
 
-每個分割槽分配給一個節點,每個節點可以處理多個分割槽,就像固定數量的分割槽一樣。大型分割槽拆分後,可以將其中的一半轉移到另一個節點,以平衡負載。在HBase中,分割槽檔案的傳輸透過HDFS(底層使用的分散式檔案系統)來實現【3】。
+每個分割槽分配給一個節點,每個節點可以處理多個分割槽,就像固定數量的分割槽一樣。大型分割槽拆分後,可以將其中的一半轉移到另一個節點,以平衡負載。在 HBase 中,分割槽檔案的傳輸透過 HDFS(底層使用的分散式檔案系統)來實現【3】。
 
 動態分割槽的一個優點是分割槽數量適應總資料量。如果只有少量的資料,少量的分割槽就足夠了,所以開銷很小;如果有大量的資料,每個分割槽的大小被限制在一個可配置的最大值【23】。
 
-需要注意的是,一個空的資料庫從一個分割槽開始,因為沒有關於在哪裡繪製分割槽邊界的先驗資訊。資料集開始時很小,直到達到第一個分割槽的分割點,所有寫入操作都必須由單個節點處理,而其他節點則處於空閒狀態。為了解決這個問題,HBase和MongoDB允許在一個空的資料庫上配置一組初始分割槽(這被稱為**預分割**,即pre-splitting)。在鍵範圍分割槽的情況中,預分割需要提前知道鍵是如何進行分配的【4,26】。
+需要注意的是,一個空的資料庫從一個分割槽開始,因為沒有關於在哪裡繪製分割槽邊界的先驗資訊。資料集開始時很小,直到達到第一個分割槽的分割點,所有寫入操作都必須由單個節點處理,而其他節點則處於空閒狀態。為了解決這個問題,HBase 和 MongoDB 允許在一個空的資料庫上配置一組初始分割槽(這被稱為 **預分割**,即 pre-splitting)。在鍵範圍分割槽的情況中,預分割需要提前知道鍵是如何進行分配的【4,26】。
 
-動態分割槽不僅適用於資料的範圍分割槽,而且也適用於雜湊分割槽。從版本2.4開始,MongoDB同時支援範圍和雜湊分割槽,並且都支援動態分割分割槽。
+動態分割槽不僅適用於資料的範圍分割槽,而且也適用於雜湊分割槽。從版本 2.4 開始,MongoDB 同時支援範圍和雜湊分割槽,並且都支援動態分割分割槽。
 
 #### 按節點比例分割槽
 
 透過動態分割槽,分割槽的數量與資料集的大小成正比,因為拆分和合並過程將每個分割槽的大小保持在固定的最小值和最大值之間。另一方面,對於固定數量的分割槽,每個分割槽的大小與資料集的大小成正比。在這兩種情況下,分割槽的數量都與節點的數量無關。
 
-Cassandra和Ketama使用的第三種方法是使分割槽數與節點數成正比——換句話說,每個節點具有固定數量的分割槽【23,27,28】。在這種情況下,每個分割槽的大小與資料集大小成比例地增長,而節點數量保持不變,但是當增加節點數時,分割槽將再次變小。由於較大的資料量通常需要較大數量的節點進行儲存,因此這種方法也使每個分割槽的大小較為穩定。
+Cassandra 和 Ketama 使用的第三種方法是使分割槽數與節點數成正比 —— 換句話說,每個節點具有固定數量的分割槽【23,27,28】。在這種情況下,每個分割槽的大小與資料集大小成比例地增長,而節點數量保持不變,但是當增加節點數時,分割槽將再次變小。由於較大的資料量通常需要較大數量的節點進行儲存,因此這種方法也使每個分割槽的大小較為穩定。
 
-當一個新節點加入叢集時,它隨機選擇固定數量的現有分割槽進行拆分,然後佔有這些拆分分割槽中每個分割槽的一半,同時將每個分割槽的另一半留在原地。隨機化可能會產生不公平的分割,但是平均在更大數量的分割槽上時(在Cassandra中,預設情況下,每個節點有256個分割槽),新節點最終從現有節點獲得公平的負載份額。 Cassandra 3.0引入了另一種再平衡的演算法來避免不公平的分割【29】。
+當一個新節點加入叢集時,它隨機選擇固定數量的現有分割槽進行拆分,然後佔有這些拆分分割槽中每個分割槽的一半,同時將每個分割槽的另一半留在原地。隨機化可能會產生不公平的分割,但是平均在更大數量的分割槽上時(在 Cassandra 中,預設情況下,每個節點有 256 個分割槽),新節點最終從現有節點獲得公平的負載份額。 Cassandra 3.0 引入了另一種再平衡的演算法來避免不公平的分割【29】。
 
-隨機選擇分割槽邊界要求使用基於雜湊的分割槽(可以從雜湊函式產生的數字範圍中挑選邊界)。實際上,這種方法最符合一致性雜湊的原始定義【7】(請參閱“[一致性雜湊](#一致性雜湊)”)。最新的雜湊函式可以在較低元資料開銷的情況下達到類似的效果【8】。
+隨機選擇分割槽邊界要求使用基於雜湊的分割槽(可以從雜湊函式產生的數字範圍中挑選邊界)。實際上,這種方法最符合一致性雜湊的原始定義【7】(請參閱 “[一致性雜湊](#一致性雜湊)”)。最新的雜湊函式可以在較低元資料開銷的情況下達到類似的效果【8】。
 
 ### 運維:手動還是自動再平衡
 
 關於再平衡有一個重要問題:自動還是手動進行?
 
-在全自動重新平衡(系統自動決定何時將分割槽從一個節點移動到另一個節點,無須人工干預)和完全手動(分割槽指派給節點由管理員明確配置,僅在管理員明確重新配置時才會更改)之間有一個權衡。例如,Couchbase,Riak和Voldemort會自動生成建議的分割槽分配,但需要管理員提交才能生效。
+在全自動重新平衡(系統自動決定何時將分割槽從一個節點移動到另一個節點,無須人工干預)和完全手動(分割槽指派給節點由管理員明確配置,僅在管理員明確重新配置時才會更改)之間有一個權衡。例如,Couchbase、Riak 和 Voldemort 會自動生成建議的分割槽分配,但需要管理員提交才能生效。
 
 全自動重新平衡可以很方便,因為正常維護的操作工作較少。但是,這可能是不可預測的。再平衡是一個昂貴的操作,因為它需要重新路由請求並將大量資料從一個節點移動到另一個節點。如果沒有做好,這個過程可能會使網路或節點負載過重,降低其他請求的效能。
 
@@ -252,45 +253,45 @@ Cassandra和Ketama使用的第三種方法是使分割槽數與節點數成正
 
 ## 請求路由
 
-現在我們已經將資料集分割到多個機器上執行的多個節點上。但是仍然存在一個懸而未決的問題:當客戶想要發出請求時,如何知道要連線哪個節點?隨著分割槽重新平衡,分割槽對節點的分配也發生變化。為了回答這個問題,需要有人知曉這些變化:如果我想讀或寫鍵“foo”,需要連線哪個IP地址和埠號?
+現在我們已經將資料集分割到多個機器上執行的多個節點上。但是仍然存在一個懸而未決的問題:當客戶想要發出請求時,如何知道要連線哪個節點?隨著分割槽重新平衡,分割槽對節點的分配也發生變化。為了回答這個問題,需要有人知曉這些變化:如果我想讀或寫鍵 “foo”,需要連線哪個 IP 地址和埠號?
 
-這個問題可以概括為 **服務發現(service discovery)** ,它不僅限於資料庫。任何可透過網路訪問的軟體都有這個問題,特別是如果它的目標是高可用性(在多臺機器上執行冗餘配置)。許多公司已經編寫了自己的內部服務發現工具,其中許多已經作為開源釋出【30】。
+這個問題可以概括為 **服務發現(service discovery)** ,它不僅限於資料庫。任何可透過網路訪問的軟體都有這個問題,特別是如果它的目標是高可用性(在多臺機器上執行冗餘配置)。許多公司已經編寫了自己的內部服務發現工具,其中許多已經作為開源釋出【30】。
 
-概括來說,這個問題有幾種不同的方案(如圖6-7所示):
+概括來說,這個問題有幾種不同的方案(如圖 6-7 所示):
 
-1. 允許客戶聯絡任何節點(例如,透過**迴圈策略的負載均衡**,即Round-Robin Load Balancer)。如果該節點恰巧擁有請求的分割槽,則它可以直接處理該請求;否則,它將請求轉發到適當的節點,接收回復並傳遞給客戶端。
+1. 允許客戶聯絡任何節點(例如,透過 **迴圈策略的負載均衡**,即 Round-Robin Load Balancer)。如果該節點恰巧擁有請求的分割槽,則它可以直接處理該請求;否則,它將請求轉發到適當的節點,接收回復並傳遞給客戶端。
 2. 首先將所有來自客戶端的請求傳送到路由層,它決定了應該處理請求的節點,並相應地轉發。此路由層本身不處理任何請求;它僅負責分割槽的負載均衡。
 3. 要求客戶端知道分割槽和節點的分配。在這種情況下,客戶端可以直接連線到適當的節點,而不需要任何中介。
 
-以上所有情況中的關鍵問題是:作出路由決策的元件(可能是節點之一,還是路由層或客戶端)如何瞭解分割槽-節點之間的分配關係變化?
+以上所有情況中的關鍵問題是:作出路由決策的元件(可能是節點之一,還是路由層或客戶端)如何瞭解分割槽 - 節點之間的分配關係變化?
 
 ![](../img/fig6-7.png)
 
-**圖6-7 將請求路由到正確節點的三種不同方式。**
+**圖 6-7 將請求路由到正確節點的三種不同方式。**
 
-這是一個具有挑戰性的問題,因為重要的是所有參與者都達成共識 - 否則請求將被傳送到錯誤的節點,得不到正確的處理。 在分散式系統中有達成共識的協議,但很難正確地實現(見[第九章](ch9.md))。
+這是一個具有挑戰性的問題,因為重要的是所有參與者都達成共識 - 否則請求將被傳送到錯誤的節點,得不到正確的處理。 在分散式系統中有達成共識的協議,但很難正確地實現(見 [第九章](ch9.md))。
 
-許多分散式資料系統都依賴於一個獨立的協調服務,比如ZooKeeper來跟蹤叢集元資料,如[圖6-8](../img/fig6-8.png)所示。 每個節點在ZooKeeper中註冊自己,ZooKeeper維護分割槽到節點的可靠對映。 其他參與者(如路由層或分割槽感知客戶端)可以在ZooKeeper中訂閱此資訊。 只要分割槽分配發生了改變,或者叢集中新增或刪除了一個節點,ZooKeeper就會通知路由層使路由資訊保持最新狀態。
+許多分散式資料系統都依賴於一個獨立的協調服務,比如 ZooKeeper 來跟蹤叢集元資料,如 [圖 6-8](../img/fig6-8.png) 所示。 每個節點在 ZooKeeper 中註冊自己,ZooKeeper 維護分割槽到節點的可靠對映。 其他參與者(如路由層或分割槽感知客戶端)可以在 ZooKeeper 中訂閱此資訊。 只要分割槽分配發生了改變,或者叢集中新增或刪除了一個節點,ZooKeeper 就會通知路由層使路由資訊保持最新狀態。
 
 ![](../img/fig6-8.png)
 
-**圖6-8 使用ZooKeeper跟蹤分割槽分配給節點。**
+**圖 6-8 使用 ZooKeeper 跟蹤分割槽分配給節點。**
 
-例如,LinkedIn的Espresso使用Helix 【31】進行叢集管理(依靠ZooKeeper),實現瞭如[圖6-8](../img/fig6-8.png)所示的路由層。 HBase,SolrCloud和Kafka也使用ZooKeeper來跟蹤分割槽分配。 MongoDB具有類似的體系結構,但它依賴於自己的**配置伺服器(config server)** 實現和mongos守護程序作為路由層。
+例如,LinkedIn的Espresso使用Helix 【31】進行叢集管理(依靠ZooKeeper),實現瞭如[圖6-8](../img/fig6-8.png)所示的路由層。 HBase、SolrCloud和Kafka也使用ZooKeeper來跟蹤分割槽分配。MongoDB具有類似的體系結構,但它依賴於自己的**配置伺服器(config server)** 實現和mongos守護程序作為路由層。
 
-Cassandra和Riak採取不同的方法:他們在節點之間使用**流言協議(gossip protocol)** 來傳播叢集狀態的變化。請求可以傳送到任意節點,該節點會轉發到包含所請求的分割槽的適當節點([圖6-7](../img/fig6-7.png)中的方法1)。這個模型在資料庫節點中增加了更多的複雜性,但是避免了對像ZooKeeper這樣的外部協調服務的依賴。
+Cassandra 和 Riak 採取不同的方法:他們在節點之間使用 **流言協議(gossip protocol)** 來傳播叢集狀態的變化。請求可以傳送到任意節點,該節點會轉發到包含所請求的分割槽的適當節點([圖 6-7](../img/fig6-7.png) 中的方法 1)。這個模型在資料庫節點中增加了更多的複雜性,但是避免了對像 ZooKeeper 這樣的外部協調服務的依賴。
 
-Couchbase不會自動重新平衡,這簡化了設計。通常情況下,它配置了一個名為moxi的路由層,它會從叢集節點了解路由變化【32】。
+Couchbase 不會自動重新平衡,這簡化了設計。通常情況下,它配置了一個名為 moxi 的路由層,它會從叢集節點了解路由變化【32】。
 
-當使用路由層或向隨機節點發送請求時,客戶端仍然需要找到要連線的IP地址。這些地址並不像分割槽的節點分佈變化的那麼快,所以使用DNS通常就足夠了。
+當使用路由層或向隨機節點發送請求時,客戶端仍然需要找到要連線的 IP 地址。這些地址並不像分割槽的節點分佈變化的那麼快,所以使用 DNS 通常就足夠了。
 
 ### 執行並行查詢
 
-到目前為止,我們只關注讀取或寫入單個鍵的非常簡單的查詢(加上基於文件分割槽的次級索引場景下的分散/聚集查詢)。這也是大多數NoSQL分散式資料儲存所支援的訪問層級。
+到目前為止,我們只關注讀取或寫入單個鍵的非常簡單的查詢(加上基於文件分割槽的次級索引場景下的分散 / 聚集查詢)。這也是大多數 NoSQL 分散式資料儲存所支援的訪問層級。
 
-然而,通常用於分析的**大規模並行處理(MPP, Massively parallel processing)** 關係型資料庫產品在其支援的查詢型別方面要複雜得多。一個典型的資料倉庫查詢包含多個連線,過濾,分組和聚合操作。 MPP查詢最佳化器將這個複雜的查詢分解成許多執行階段和分割槽,其中許多可以在資料庫叢集的不同節點上並行執行。涉及掃描大規模資料集的查詢特別受益於這種並行執行。
+然而,通常用於分析的 **大規模並行處理(MPP, Massively parallel processing)** 關係型資料庫產品在其支援的查詢型別方面要複雜得多。一個典型的資料倉庫查詢包含多個連線,過濾,分組和聚合操作。 MPP 查詢最佳化器將這個複雜的查詢分解成許多執行階段和分割槽,其中許多可以在資料庫叢集的不同節點上並行執行。涉及掃描大規模資料集的查詢特別受益於這種並行執行。
 
-資料倉庫查詢的快速並行執行是一個專門的話題,由於分析有很重要的商業意義,可以帶來很多利益。我們將在[第十章](ch10.md)討論並行查詢執行的一些技巧。有關並行資料庫中使用的技術的更詳細的概述,請參閱參考文獻【1,33】。
+資料倉庫查詢的快速並行執行是一個專門的話題,由於分析有很重要的商業意義,可以帶來很多利益。我們將在 [第十章](ch10.md) 討論並行查詢執行的一些技巧。有關並行資料庫中使用的技術的更詳細的概述,請參閱參考文獻【1,33】。
 
 ## 本章小結
 
@@ -314,7 +315,7 @@ Couchbase不會自動重新平衡,這簡化了設計。通常情況下,它
 
 我們還討論了分割槽和次級索引之間的相互作用。次級索引也需要分割槽,有兩種方法:
 
-* 基於文件分割槽(本地索引),其中次級索引儲存在與主鍵和值相同的分割槽中。這意味著只有一個分割槽需要在寫入時更新,但是讀取次級索引需要在所有分割槽之間進行分散/收集。
+* 基於文件分割槽(本地索引),其中次級索引儲存在與主鍵和值相同的分割槽中。這意味著只有一個分割槽需要在寫入時更新,但是讀取次級索引需要在所有分割槽之間進行分散 / 收集。
 * 基於關鍵詞分割槽(全域性索引),其中次級索引存在不同的分割槽中。次級索引中的條目可以包括來自主鍵的所有分割槽的記錄。當文件寫入時,需要更新多個分割槽中的次級索引;但是可以從單個分割槽中進行讀取。
 
 最後,我們討論了將查詢路由到適當的分割槽的技術,從簡單的分割槽負載平衡到複雜的並行查詢執行引擎。
diff --git a/zh-tw/ch7.md b/zh-tw/ch7.md
index f81f6e5..f68fe3e 100644
--- a/zh-tw/ch7.md
+++ b/zh-tw/ch7.md
@@ -4,7 +4,7 @@
 
 > 一些作者聲稱,支援通用的兩階段提交代價太大,會帶來效能與可用性的問題。讓程式設計師來處理過度使用事務導致的效能問題,總比缺少事務程式設計好得多。
 >
-> —— James Corbett等人,Spanner:Google的全球分散式資料庫(2012)
+> —— James Corbett 等人,Spanner:Google 的全球分散式資料庫(2012)
 
 ------
 
@@ -21,36 +21,36 @@
 
 為了實現可靠性,系統必須處理這些故障,確保它們不會導致整個系統的災難性故障。但是實現容錯機制工作量巨大。需要仔細考慮所有可能出錯的事情,並進行大量的測試,以確保解決方案真正管用。
 
-數十年來,**事務(transaction)** 一直是簡化這些問題的首選機制。事務是應用程式將多個讀寫操作組合成一個邏輯單元的一種方式。從概念上講,事務中的所有讀寫操作被視作單個操作來執行:整個事務要麼成功**提交**(commit),要麼失敗**中止**(abort)或**回滾**(rollback)。如果失敗,應用程式可以安全地重試。對於事務來說,應用程式的錯誤處理變得簡單多了,因為它不用再擔心部分失敗的情況了,即某些操作成功,某些失敗(無論出於何種原因)。
+數十年來,**事務(transaction)** 一直是簡化這些問題的首選機制。事務是應用程式將多個讀寫操作組合成一個邏輯單元的一種方式。從概念上講,事務中的所有讀寫操作被視作單個操作來執行:整個事務要麼成功 **提交**(commit),要麼失敗 **中止**(abort)或 **回滾**(rollback)。如果失敗,應用程式可以安全地重試。對於事務來說,應用程式的錯誤處理變得簡單多了,因為它不用再擔心部分失敗的情況了,即某些操作成功,某些失敗(無論出於何種原因)。
 
-和事務打交道時間長了,你可能會覺得它顯而易見。但我們不應將其視為理所當然。事務不是天然存在的;它們是為了**簡化應用程式設計模型**而建立的。透過使用事務,應用程式可以自由地忽略某些潛在的錯誤情況和併發問題,因為資料庫會替應用處理好這些。(我們稱之為**安全保證**,即safety guarantees)。
+和事務打交道時間長了,你可能會覺得它顯而易見。但我們不應將其視為理所當然。事務不是天然存在的;它們是為了 **簡化應用程式設計模型** 而建立的。透過使用事務,應用程式可以自由地忽略某些潛在的錯誤情況和併發問題,因為資料庫會替應用處理好這些。(我們稱之為 **安全保證**,即 safety guarantees)。
 
 並不是所有的應用都需要事務,有時候弱化事務保證、或完全放棄事務也是有好處的(例如,為了獲得更高效能或更高可用性)。一些安全屬性也可以在沒有事務的情況下實現。
 
 怎樣知道你是否需要事務?為了回答這個問題,首先需要確切理解事務可以提供的安全保障,以及它們的代價。儘管乍看事務似乎很簡單,但實際上有許多微妙但重要的細節在起作用。
 
-本章將研究許多出錯案例,並探索資料庫用於防範這些問題的演算法。尤其會深入**併發控制**的領域,討論各種可能發生的競爭條件,以及資料庫如何實現**讀已提交(read committed)**,**快照隔離(snapshot isolation)** 和**可序列化(serializability)** 等隔離級別。
+本章將研究許多出錯案例,並探索資料庫用於防範這些問題的演算法。尤其會深入 **併發控制** 的領域,討論各種可能發生的競爭條件,以及資料庫如何實現 **讀已提交(read committed)**,**快照隔離(snapshot isolation)** 和 **可序列化(serializability)** 等隔離級別。
 
-本章同時適用於單機資料庫與分散式資料庫;在[第八章](ch8.md)中將重點討論僅出現在分散式系統中的特殊挑戰。
+本章同時適用於單機資料庫與分散式資料庫;在 [第八章](ch8.md) 中將重點討論僅出現在分散式系統中的特殊挑戰。
 
 
 ## 事務的棘手概念
 
-現今,幾乎所有的關係型資料庫和一些非關係資料庫都支援**事務**。其中大多數遵循IBM System R(第一個SQL資料庫)在1975年引入的風格【1,2,3】。40年裡,儘管一些實現細節發生了變化,但總體思路大同小異:MySQL,PostgreSQL,Oracle,SQL Server等資料庫中的事務支援與System R異乎尋常地相似。
+現今,幾乎所有的關係型資料庫和一些非關係資料庫都支援 **事務**。其中大多數遵循 IBM System R(第一個 SQL 資料庫)在 1975 年引入的風格【1,2,3】。40 年裡,儘管一些實現細節發生了變化,但總體思路大同小異:MySQL、PostgreSQL、Oracle 和 SQL Server 等資料庫中的事務支援與 System R 異乎尋常地相似。
 
-2000年以後,非關係(NoSQL)資料庫開始普及。它們的目標是在關係資料庫的現狀基礎上,透過提供新的資料模型選擇(請參閱[第二章](ch2.md))並預設包含複製(第五章)和分割槽(第六章)來進一步提升。事務是這次運動的主要犧牲品:這些新一代資料庫中的許多資料庫完全放棄了事務,或者重新定義了這個詞,描述比以前所理解的更弱得多的一套保證【4】。
+2000 年以後,非關係(NoSQL)資料庫開始普及。它們的目標是在關係資料庫的現狀基礎上,透過提供新的資料模型選擇(請參閱 [第二章](ch2.md))並預設包含複製(第五章)和分割槽(第六章)來進一步提升。事務是這次運動的主要犧牲品:這些新一代資料庫中的許多資料庫完全放棄了事務,或者重新定義了這個詞,描述比以前所理解的更弱得多的一套保證【4】。
 
-隨著這種新型分散式資料庫的炒作,人們普遍認為事務是可伸縮性的對立面,任何大型系統都必須放棄事務以保持良好的效能和高可用性【5,6】。另一方面,資料庫廠商有時將事務保證作為“重要應用”和“有價值資料”的基本要求。這兩種觀點都是**純粹的誇張**。
+隨著這種新型分散式資料庫的炒作,人們普遍認為事務是可伸縮性的對立面,任何大型系統都必須放棄事務以保持良好的效能和高可用性【5,6】。另一方面,資料庫廠商有時將事務保證作為 “重要應用” 和 “有價值資料” 的基本要求。這兩種觀點都是 **純粹的誇張**。
 
-事實並非如此簡單:與其他技術設計選擇一樣,事務有其優勢和侷限性。為了理解這些權衡,讓我們瞭解事務所提供的保證的細節——無論是在正常執行中還是在各種極端(但是現實存在)的情況下。
+事實並非如此簡單:與其他技術設計選擇一樣,事務有其優勢和侷限性。為了理解這些權衡,讓我們瞭解事務所提供的保證的細節 —— 無論是在正常執行中還是在各種極端(但是現實存在)的情況下。
 
 ### ACID的含義
 
-事務所提供的安全保證,通常由眾所周知的首字母縮略詞ACID來描述,ACID代表**原子性(Atomicity)**,**一致性(Consistency)**,**隔離性(Isolation)** 和**永續性(Durability)**。它由Theo Härder和Andreas Reuter於1983年提出,旨在為資料庫中的容錯機制建立精確的術語。
+事務所提供的安全保證,通常由眾所周知的首字母縮略詞 ACID 來描述,ACID 代表 **原子性(Atomicity)**,**一致性(Consistency)**,**隔離性(Isolation)** 和 **永續性(Durability)**。它由 Theo Härder 和 Andreas Reuter 於 1983 年提出,旨在為資料庫中的容錯機制建立精確的術語。
 
-但實際上,不同資料庫的ACID實現並不相同。例如,我們將會看到,關於**隔離性**的含義就有許多含糊不清【8】。高層次上的想法很美好,但魔鬼隱藏在細節裡。今天,當一個系統聲稱自己“符合ACID”時,實際上能期待的是什麼保證並不清楚。不幸的是,ACID現在幾乎已經變成了一個營銷術語。
+但實際上,不同資料庫的 ACID 實現並不相同。例如,我們將會看到,關於 **隔離性** 的含義就有許多含糊不清【8】。高層次上的想法很美好,但魔鬼隱藏在細節裡。今天,當一個系統聲稱自己 “符合 ACID” 時,實際上能期待的是什麼保證並不清楚。不幸的是,ACID 現在幾乎已經變成了一個營銷術語。
 
-(不符合ACID標準的系統有時被稱為BASE,它代表**基本可用性(Basically Available)**,**軟狀態(Soft State)** 和**最終一致性(Eventual consistency)**【9】,這比ACID的定義更加模糊,似乎BASE的唯一合理的定義是“不是ACID”,即它幾乎可以代表任何你想要的東西。)
+(不符合 ACID 標準的系統有時被稱為 BASE,它代表 **基本可用性(Basically Available)**,**軟狀態(Soft State)** 和 **最終一致性(Eventual consistency)**【9】,這比 ACID 的定義更加模糊,似乎 BASE 的唯一合理的定義是 “不是 ACID”,即它幾乎可以代表任何你想要的東西。)
 
 讓我們深入瞭解原子性,一致性,隔離性和永續性的定義,這可以讓我們提煉出事務的思想。
 
@@ -58,85 +58,85 @@
 
 一般來說,原子是指不能分解成小部分的東西。這個詞在計算機的不同領域中意味著相似但又微妙不同的東西。例如,在多執行緒程式設計中,如果一個執行緒執行一個原子操作,這意味著另一個執行緒無法看到該操作的一半結果。系統只能處於操作之前或操作之後的狀態,而不是介於兩者之間的狀態。
 
-相比之下,ACID的原子性並**不**是關於 **併發(concurrent)** 的。它並不是在描述如果幾個程序試圖同時訪問相同的資料會發生什麼情況,這種情況包含在縮寫 ***I*** 中,即[**隔離性**](#隔離性)。
+相比之下,ACID 的原子性並 **不** 是關於 **併發(concurrent)** 的。它並不是在描述如果幾個程序試圖同時訪問相同的資料會發生什麼情況,這種情況包含在縮寫 ***I*** 中,即 [**隔離性**](#隔離性)。
 
-ACID的原子性描述了當客戶想進行多次寫入,但在一些寫操作處理完之後出現故障的情況。例如程序崩潰,網路連線中斷,磁碟變滿或者某種完整性約束被違反。如果這些寫操作被分組到一個原子事務中,並且該事務由於錯誤而不能完成(提交),則該事務將被中止,並且資料庫必須丟棄或撤消該事務中迄今為止所做的任何寫入。
+ACID 的原子性描述了當客戶想進行多次寫入,但在一些寫操作處理完之後出現故障的情況。例如程序崩潰,網路連線中斷,磁碟變滿或者某種完整性約束被違反。如果這些寫操作被分組到一個原子事務中,並且該事務由於錯誤而不能完成(提交),則該事務將被中止,並且資料庫必須丟棄或撤消該事務中迄今為止所做的任何寫入。
 
-如果沒有原子性,在多處更改進行到一半時發生錯誤,很難知道哪些更改已經生效,哪些沒有生效。該應用程式可以再試一次,但冒著進行兩次相同變更的風險,可能會導致資料重複或錯誤的資料。原子性簡化了這個問題:如果事務被**中止(abort)**,應用程式可以確定它沒有改變任何東西,所以可以安全地重試。
+如果沒有原子性,在多處更改進行到一半時發生錯誤,很難知道哪些更改已經生效,哪些沒有生效。該應用程式可以再試一次,但冒著進行兩次相同變更的風險,可能會導致資料重複或錯誤的資料。原子性簡化了這個問題:如果事務被 **中止(abort)**,應用程式可以確定它沒有改變任何東西,所以可以安全地重試。
 
-ACID原子性的定義特徵是:**能夠在錯誤時中止事務,丟棄該事務進行的所有寫入變更的能力。** 或許 **可中止性(abortability)** 是更好的術語,但本書將繼續使用原子性,因為這是慣用詞。
+ACID 原子性的定義特徵是:**能夠在錯誤時中止事務,丟棄該事務進行的所有寫入變更的能力。** 或許 **可中止性(abortability)** 是更好的術語,但本書將繼續使用原子性,因為這是慣用詞。
 
 #### 一致性
 
 一致性這個詞被賦予太多含義:
 
-* 在[第五章](ch5.md)中,我們討論了副本一致性,以及非同步複製系統中的最終一致性問題(請參閱“[複製延遲問題](ch5.md#複製延遲問題)”)。
-* [一致性雜湊(Consistency Hashing)](ch6.md#一致性雜湊))是某些系統用於重新分割槽的一種分割槽方法。
-* 在[CAP定理](ch9.md#CAP定理)中,一致性一詞用於表示[線性一致性](ch9.md#線性一致性)。
-* 在ACID的上下文中,**一致性**是指資料庫在應用程式的特定概念中處於“良好狀態”。
+* 在 [第五章](ch5.md) 中,我們討論了副本一致性,以及非同步複製系統中的最終一致性問題(請參閱 “[複製延遲問題](ch5.md#複製延遲問題)”)。
+* [一致性雜湊](ch6.md#一致性雜湊) 是某些系統用於重新分割槽的一種分割槽方法。
+* 在 [CAP 定理](ch9.md#CAP 定理) 中,一致性一詞用於表示 [線性一致性](ch9.md#線性一致性)。
+* 在 ACID 的上下文中,**一致性** 是指資料庫在應用程式的特定概念中處於 “良好狀態”。
 
 很不幸,這一個詞就至少有四種不同的含義。
 
-ACID一致性的概念是,**對資料的一組特定約束必須始終成立**。即**不變式(invariants)**。例如,在會計系統中,所有賬戶整體上必須借貸相抵。如果一個事務開始於一個滿足這些不變式的有效資料庫,且在事務處理期間的任何寫入操作都保持這種有效性,那麼可以確定,不變式總是滿足的。
+ACID 一致性的概念是,**對資料的一組特定約束必須始終成立**。即 **不變式(invariants)**。例如,在會計系統中,所有賬戶整體上必須借貸相抵。如果一個事務開始於一個滿足這些不變式的有效資料庫,且在事務處理期間的任何寫入操作都保持這種有效性,那麼可以確定,不變式總是滿足的。
 
 但是,一致性的這種概念取決於應用程式對不變式的理解,應用程式負責正確定義它的事務,並保持一致性。這並不是資料庫可以保證的事情:如果你寫入違反不變式的髒資料,資料庫也無法阻止你(一些特定型別的不變式可以由資料庫檢查,例如外來鍵約束或唯一約束,但是一般來說,是應用程式來定義什麼樣的資料是有效的,什麼樣是無效的。—— 資料庫只管儲存)。
 
-原子性,隔離性和永續性是資料庫的屬性,而一致性(在ACID意義上)是應用程式的屬性。應用可能依賴資料庫的原子性和隔離屬性來實現一致性,但這並不僅取決於資料庫。因此,字母C不屬於ACID[^i]。
+原子性,隔離性和永續性是資料庫的屬性,而一致性(在 ACID 意義上)是應用程式的屬性。應用可能依賴資料庫的原子性和隔離屬性來實現一致性,但這並不僅取決於資料庫。因此,字母 C 不屬於 ACID [^i]。
 
-[^i]: 喬·海勒斯坦(Joe Hellerstein)指出,在Härder與Reuter的論文中,“ACID中的C”是被“扔進去湊縮寫單詞的”【7】,而且那時候大家都不怎麼在乎一致性。
+[^i]: 喬・海勒斯坦(Joe Hellerstein)指出,在 Härder 與 Reuter 的論文中,“ACID 中的 C” 是被 “扔進去湊縮寫單詞的”【7】,而且那時候大家都不怎麼在乎一致性。
 
 #### 隔離性
 
-大多數資料庫都會同時被多個客戶端訪問。如果它們各自讀寫資料庫的不同部分,這是沒有問題的,但是如果它們訪問相同的資料庫記錄,則可能會遇到**併發**問題(**競爭條件**,即race conditions)。
+大多數資料庫都會同時被多個客戶端訪問。如果它們各自讀寫資料庫的不同部分,這是沒有問題的,但是如果它們訪問相同的資料庫記錄,則可能會遇到 **併發** 問題(**競爭條件**,即 race conditions)。
 
-[圖7-1](../img/fig7-1.png)是這類問題的一個簡單例子。假設你有兩個客戶端同時在資料庫中增長一個計數器。(假設資料庫沒有內建的自增操作)每個客戶端需要讀取計數器的當前值,加 1 ,再回寫新值。[圖7-1](../img/fig7-1.png) 中,因為發生了兩次增長,計數器應該從42增至44;但由於競態條件,實際上只增至 43 。
+[圖 7-1](../img/fig7-1.png) 是這類問題的一個簡單例子。假設你有兩個客戶端同時在資料庫中增長一個計數器。(假設資料庫沒有內建的自增操作)每個客戶端需要讀取計數器的當前值,加 1 ,再回寫新值。[圖 7-1](../img/fig7-1.png) 中,因為發生了兩次增長,計數器應該從 42 增至 44;但由於競態條件,實際上只增至 43 。
 
-ACID意義上的隔離性意味著,**同時執行的事務是相互隔離的**:它們不能相互冒犯。傳統的資料庫教科書將隔離性形式化為**可序列化(Serializability)**,這意味著每個事務可以假裝它是唯一在整個資料庫上執行的事務。資料庫確保當多個事務被提交時,結果與它們序列執行(一個接一個)是一樣的,儘管實際上它們可能是併發執行的【10】。
+ACID 意義上的隔離性意味著,**同時執行的事務是相互隔離的**:它們不能相互冒犯。傳統的資料庫教科書將隔離性形式化為 **可序列化(Serializability)**,這意味著每個事務可以假裝它是唯一在整個資料庫上執行的事務。資料庫確保當多個事務被提交時,結果與它們序列執行(一個接一個)是一樣的,儘管實際上它們可能是併發執行的【10】。
 
 ![](../img/fig7-1.png)
 
-**圖7-1 兩個客戶之間的競爭狀態同時遞增計數器**
+**圖 7-1 兩個客戶之間的競爭狀態同時遞增計數器**
 
-然而實踐中很少會使用可序列的隔離,因為它有效能損失。一些流行的資料庫如Oracle 11g,甚至沒有實現它。在Oracle中有一個名為“可序列的”隔離級別,但實際上它實現了一種叫做**快照隔離(snapshot isolation)** 的功能,**這是一種比可序列化更弱的保證**【8,11】。我們將在“[弱隔離級別](#弱隔離級別)”中研究快照隔離和其他形式的隔離。
+然而實踐中很少會使用可序列的隔離,因為它有效能損失。一些流行的資料庫如 Oracle 11g,甚至沒有實現它。在 Oracle 中有一個名為 “可序列的” 隔離級別,但實際上它實現了一種叫做 **快照隔離(snapshot isolation)** 的功能,**這是一種比可序列化更弱的保證**【8,11】。我們將在 “[弱隔離級別](#弱隔離級別)” 中研究快照隔離和其他形式的隔離。
 
 #### 永續性
 
 資料庫系統的目的是,提供一個安全的地方儲存資料,而不用擔心丟失。**永續性** 是一個承諾,即一旦事務成功完成,即使發生硬體故障或資料庫崩潰,寫入的任何資料也不會丟失。
 
-在單節點資料庫中,永續性通常意味著資料已被寫入非易失性儲存裝置,如硬碟或SSD。它通常還包括預寫日誌或類似的檔案(請參閱“[讓B樹更可靠](ch3.md#讓B樹更可靠)”),以便在磁碟上的資料結構損壞時進行恢復。在帶複製的資料庫中,永續性可能意味著資料已成功複製到一些節點。為了提供永續性保證,資料庫必須等到這些寫入或複製完成後,才能報告事務成功提交。
+在單節點資料庫中,永續性通常意味著資料已被寫入非易失性儲存裝置,如硬碟或 SSD。它通常還包括預寫日誌或類似的檔案(請參閱 “[讓 B 樹更可靠](ch3.md#讓B樹更可靠)”),以便在磁碟上的資料結構損壞時進行恢復。在帶複製的資料庫中,永續性可能意味著資料已成功複製到一些節點。為了提供永續性保證,資料庫必須等到這些寫入或複製完成後,才能報告事務成功提交。
 
-如“[可靠性](ch1.md#可靠性)”一節所述,**完美的永續性是不存在的** :如果所有硬碟和所有備份同時被銷燬,那顯然沒有任何資料庫能救得了你。
+如 “[可靠性](ch1.md#可靠性)” 一節所述,**完美的永續性是不存在的** :如果所有硬碟和所有備份同時被銷燬,那顯然沒有任何資料庫能救得了你。
 
 > #### 複製與永續性
 >
-> 在歷史上,永續性意味著寫入歸檔磁帶。後來它被理解為寫入磁碟或SSD。再後來它又有了新的內涵即“複製(replication)”。哪種實現更好一些?
+> 在歷史上,永續性意味著寫入歸檔磁帶。後來它被理解為寫入磁碟或 SSD。再後來它又有了新的內涵即 “複製(replication)”。哪種實現更好一些?
 >
 > 真相是,沒有什麼是完美的:
 >
 > * 如果你寫入磁碟然後機器宕機,即使資料沒有丟失,在修復機器或將磁碟轉移到其他機器之前,也是無法訪問的。這種情況下,複製系統可以保持可用性。
-> * 一個相關性故障(停電,或一個特定輸入導致所有節點崩潰的Bug)可能會一次性摧毀所有副本(請參閱「[可靠性](ch1.md#可靠性)」),任何僅儲存在記憶體中的資料都會丟失,故記憶體資料庫仍然要和磁碟寫入打交道。
+> * 一個相關性故障(停電,或一個特定輸入導致所有節點崩潰的 Bug)可能會一次性摧毀所有副本(請參閱「[可靠性](ch1.md#可靠性)」),任何僅儲存在記憶體中的資料都會丟失,故記憶體資料庫仍然要和磁碟寫入打交道。
 > * 在非同步複製系統中,當主庫不可用時,最近的寫入操作可能會丟失(請參閱「[處理節點宕機](ch5.md#處理節點宕機)」)。
-> * 當電源突然斷電時,特別是固態硬碟,有證據顯示有時會違反應有的保證:甚至fsync也不能保證正常工作【12】。硬碟韌體可能有錯誤,就像任何其他型別的軟體一樣【13,14】。
+> * 當電源突然斷電時,特別是固態硬碟,有證據顯示有時會違反應有的保證:甚至 fsync 也不能保證正常工作【12】。硬碟韌體可能有錯誤,就像任何其他型別的軟體一樣【13,14】。
 > * 儲存引擎和檔案系統之間的微妙互動可能會導致難以追蹤的錯誤,並可能導致磁碟上的檔案在崩潰後被損壞【15,16】。
 > * 磁碟上的資料可能會在沒有檢測到的情況下逐漸損壞【17】。如果資料已損壞一段時間,副本和最近的備份也可能損壞。這種情況下,需要嘗試從歷史備份中恢復資料。
-> * 一項關於固態硬碟的研究發現,在執行的前四年中,30%到80%的硬碟會產生至少一個壞塊【18】。相比固態硬碟,磁碟的壞道率較低,但完全失效的概率更高。
-> * 如果SSD斷電,可能會在幾周內開始丟失資料,具體取決於溫度【19】。
+> * 一項關於固態硬碟的研究發現,在執行的前四年中,30% 到 80% 的硬碟會產生至少一個壞塊【18】。相比固態硬碟,磁碟的壞道率較低,但完全失效的概率更高。
+> * 如果 SSD 斷電,可能會在幾周內開始丟失資料,具體取決於溫度【19】。
 >
-> 在實踐中,沒有一種技術可以提供絕對保證。只有各種降低風險的技術,包括寫入磁碟,複製到遠端機器和備份——它們可以且應該一起使用。與往常一樣,最好抱著懷疑的態度接受任何理論上的“保證”。
+> 在實踐中,沒有一種技術可以提供絕對保證。只有各種降低風險的技術,包括寫入磁碟,複製到遠端機器和備份 —— 它們可以且應該一起使用。與往常一樣,最好抱著懷疑的態度接受任何理論上的 “保證”。
 
 ### 單物件和多物件操作
 
-回顧一下,在ACID中,原子性和隔離性描述了客戶端在同一事務中執行多次寫入時,資料庫應該做的事情:
+回顧一下,在 ACID 中,原子性和隔離性描述了客戶端在同一事務中執行多次寫入時,資料庫應該做的事情:
 
 * 原子性
 
-  如果在一系列寫操作的中途發生錯誤,則應中止事務處理,並丟棄當前事務的所有寫入。換句話說,資料庫免去了使用者對部分失敗的擔憂——透過提供“**寧為玉碎,不為瓦全(all-or-nothing)**”的保證。
+  如果在一系列寫操作的中途發生錯誤,則應中止事務處理,並丟棄當前事務的所有寫入。換句話說,資料庫免去了使用者對部分失敗的擔憂 —— 透過提供 “**寧為玉碎,不為瓦全(all-or-nothing)**” 的保證。
 
 * 隔離性
 
   同時執行的事務不應該互相干擾。例如,如果一個事務進行多次寫入,則另一個事務要麼看到全部寫入結果,要麼什麼都看不到,但不應該是一些子集。
 
-這些定義假設你想同時修改多個物件(行,文件,記錄)。通常需要**多物件事務(multi-object transaction)** 來保持多塊資料同步。[圖7-2](../img/fig7-2.png)展示了一個來自電郵應用的例子。執行以下查詢來顯示使用者未讀郵件數量:
+這些定義假設你想同時修改多個物件(行,文件,記錄)。通常需要 **多物件事務(multi-object transaction)** 來保持多塊資料同步。[圖 7-2](../img/fig7-2.png) 展示了一個來自電郵應用的例子。執行以下查詢來顯示使用者未讀郵件數量:
 
 ```sql
 SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
@@ -144,41 +144,41 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
 
 但如果郵件太多,你可能會覺得這個查詢太慢,並決定用單獨的欄位儲存未讀郵件的數量(一種反規範化)。現在每當一個新訊息寫入時,必須也增長未讀計數器,每當一個訊息被標記為已讀時,也必須減少未讀計數器。
 
-在[圖7-2](../img/fig7-2.png)中,使用者2 遇到異常情況:郵件列表裡顯示有未讀訊息,但計數器顯示為零未讀訊息,因為計數器增長還沒有發生[^ii]。隔離性可以避免這個問題:透過確保使用者2 要麼同時看到新郵件和增長後的計數器,要麼都看不到,而不是一個前後矛盾的中間結果。
+在 [圖 7-2](../img/fig7-2.png) 中,使用者 2 遇到異常情況:郵件列表裡顯示有未讀訊息,但計數器顯示為零未讀訊息,因為計數器增長還沒有發生 [^ii]。隔離性可以避免這個問題:透過確保使用者 2 要麼同時看到新郵件和增長後的計數器,要麼都看不到,而不是一個前後矛盾的中間結果。
 
 [^ii]: 可以說郵件應用中的錯誤計數器並不是什麼特別重要的問題。但換種方式來看,你可以把未讀計數器換成客戶賬戶餘額,把郵件收發看成支付交易。
 
 ![](../img/fig7-2.png)
 
-**圖7-2 違反隔離性:一個事務讀取另一個事務的未被執行的寫入(“髒讀”)。**
+**圖 7-2 違反隔離性:一個事務讀取另一個事務的未被執行的寫入(“髒讀”)。**
 
-[圖7-3](../img/fig7-3.png)說明了對原子性的需求:如果在事務過程中發生錯誤,郵箱和未讀計數器的內容可能會失去同步。在原子事務中,如果對計數器的更新失敗,事務將被中止,並且插入的電子郵件將被回滾。
+[圖 7-3](../img/fig7-3.png) 說明了對原子性的需求:如果在事務過程中發生錯誤,郵箱和未讀計數器的內容可能會失去同步。在原子事務中,如果對計數器的更新失敗,事務將被中止,並且插入的電子郵件將被回滾。
 
 ![](../img/fig7-3.png)
 
-**圖7-3 原子性確保發生錯誤時,事務先前的任何寫入都會被撤消,以避免狀態不一致**
+**圖 7-3 原子性確保發生錯誤時,事務先前的任何寫入都會被撤消,以避免狀態不一致**
 
-多物件事務需要某種方式來確定哪些讀寫操作屬於同一個事務。在關係型資料庫中,通常基於客戶端與資料庫伺服器的TCP連線:在任何特定連線上,`BEGIN TRANSACTION` 和 `COMMIT` 語句之間的所有內容,被認為是同一事務的一部分.[^iii]
+多物件事務需要某種方式來確定哪些讀寫操作屬於同一個事務。在關係型資料庫中,通常基於客戶端與資料庫伺服器的 TCP 連線:在任何特定連線上,`BEGIN TRANSACTION` 和 `COMMIT` 語句之間的所有內容,被認為是同一事務的一部分.[^iii]
 
-[^iii]: 這並不完美。如果TCP連線中斷,則事務必須中止。如果中斷發生在客戶端請求提交之後,但在伺服器確認提交發生之前,客戶端並不知道事務是否已提交。為了解決這個問題,事務管理器可以透過一個唯一事務識別符號來對操作進行分組,這個識別符號並未繫結到特定TCP連線。後續再“[資料庫的端到端原則](ch12.md#資料庫的端到端原則)”一節將回到這個主題。
+[^iii]: 這並不完美。如果 TCP 連線中斷,則事務必須中止。如果中斷發生在客戶端請求提交之後,但在伺服器確認提交發生之前,客戶端並不知道事務是否已提交。為了解決這個問題,事務管理器可以透過一個唯一事務識別符號來對操作進行分組,這個識別符號並未繫結到特定 TCP 連線。後續再 “[資料庫的端到端原則](ch12.md#資料庫的端到端原則)” 一節將回到這個主題。
 
-另一方面,許多非關係資料庫並沒有將這些操作組合在一起的方法。即使存在多物件API(例如,某鍵值儲存可能具有在一個操作中更新幾個鍵的multi-put操作),但這並不一定意味著它具有事務語義:該命令可能在一些鍵上成功,在其他的鍵上失敗,使資料庫處於部分更新的狀態。
+另一方面,許多非關係資料庫並沒有將這些操作組合在一起的方法。即使存在多物件 API(例如,某鍵值儲存可能具有在一個操作中更新幾個鍵的 multi-put 操作),但這並不一定意味著它具有事務語義:該命令可能在一些鍵上成功,在其他的鍵上失敗,使資料庫處於部分更新的狀態。
 
 #### 單物件寫入
 
-當單個物件發生改變時,原子性和隔離性也是適用的。例如,假設你正在向資料庫寫入一個 20 KB的 JSON文件:
+當單個物件發生改變時,原子性和隔離性也是適用的。例如,假設你正在向資料庫寫入一個 20 KB 的 JSON 文件:
 
-- 如果在傳送第一個10 KB之後網路連線中斷,資料庫是否儲存了不可解析的10KB JSON片段?
+- 如果在傳送第一個 10 KB 之後網路連線中斷,資料庫是否儲存了不可解析的 10KB JSON 片段?
 - 如果在資料庫正在覆蓋磁碟上的前一個值的過程中電源發生故障,是否最終將新舊值拼接在一起?
 - 如果另一個客戶端在寫入過程中讀取該文件,是否會看到部分更新的值?
 
-這些問題非常讓人頭大,故儲存引擎一個幾乎普遍的目標是:對單節點上的單個物件(例如鍵值對)上提供原子性和隔離性。原子性可以透過使用日誌來實現崩潰恢復(請參閱“[讓B樹更可靠](ch3.md#讓B樹更可靠)”),並且可以使用每個物件上的鎖來實現隔離(每次只允許一個執行緒訪問物件) 。
+這些問題非常讓人頭大,故儲存引擎一個幾乎普遍的目標是:對單節點上的單個物件(例如鍵值對)上提供原子性和隔離性。原子性可以透過使用日誌來實現崩潰恢復(請參閱 “[讓 B 樹更可靠](ch3.md#讓B樹更可靠)”),並且可以使用每個物件上的鎖來實現隔離(每次只允許一個執行緒訪問物件) 。
 
-一些資料庫也提供更復雜的原子操作[^iv],例如自增操作,這樣就不再需要像 [圖7-1](../img/fig7-1.png) 那樣的讀取-修改-寫入序列了。同樣流行的是 **[比較和設定(CAS, compare-and-set)](#比較並設定(CAS))** 操作,僅當值沒有被其他併發修改過時,才允許執行寫操作。
+一些資料庫也提供更復雜的原子操作 [^iv],例如自增操作,這樣就不再需要像 [圖 7-1](../img/fig7-1.png) 那樣的讀取 - 修改 - 寫入序列了。同樣流行的是 **[比較和設定(CAS, compare-and-set)](#比較並設定(CAS))** 操作,僅當值沒有被其他併發修改過時,才允許執行寫操作。
 
-[^iv]: 嚴格地說,**原子自增(atomic increment)** 這個術語在多執行緒程式設計的意義上使用了原子這個詞。 在ACID的情況下,它實際上應該被稱為 **隔離的(isolated)** 的或**可序列的(serializable)** 的增量。 但這就太吹毛求疵了。
+[^iv]: 嚴格地說,**原子自增(atomic increment)** 這個術語在多執行緒程式設計的意義上使用了原子這個詞。 在 ACID 的情況下,它實際上應該被稱為 **隔離的(isolated)** 的或 **可序列的(serializable)** 的增量。 但這就太吹毛求疵了。
 
-這些單物件操作很有用,因為它們可以防止在多個客戶端嘗試同時寫入同一個物件時丟失更新(請參閱“[防止丟失更新](#防止丟失更新)”)。但它們不是通常意義上的事務。CAS以及其他單一物件操作被稱為“輕量級事務”,甚至出於營銷目的被稱為“ACID”【20,21,22】,但是這個術語是誤導性的。事務通常被理解為,**將多個物件上的多個操作合併為一個執行單元的機制**。
+這些單物件操作很有用,因為它們可以防止在多個客戶端嘗試同時寫入同一個物件時丟失更新(請參閱 “[防止丟失更新](#防止丟失更新)”)。但它們不是通常意義上的事務。CAS 以及其他單一物件操作被稱為 “輕量級事務”,甚至出於營銷目的被稱為 “ACID”【20,21,22】,但是這個術語是誤導性的。事務通常被理解為,**將多個物件上的多個操作合併為一個執行單元的機制**。
 
 #### 多物件事務的需求
 
@@ -189,117 +189,117 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
 有一些場景中,單物件插入,更新和刪除是足夠的。但是許多其他場景需要協調寫入幾個不同的物件:
 
 * 在關係資料模型中,一個表中的行通常具有對另一個表中的行的外來鍵引用。(類似的是,在一個圖資料模型中,一個頂點有著到其他頂點的邊)。多物件事務使你確保這些引用始終有效:當插入幾個相互引用的記錄時,外來鍵必須是正確的和最新的,不然資料就沒有意義。
-* 在文件資料模型中,需要一起更新的欄位通常在同一個文件中,這被視為單個物件——更新單個文件時不需要多物件事務。但是,缺乏連線功能的文件資料庫會鼓勵非規範化(請參閱“[關係型資料庫與文件資料庫在今日的對比](ch2.md#關係型資料庫與文件資料庫在今日的對比)”)。當需要更新非規範化的資訊時,如 [圖7-2](../img/fig7-2.png) 所示,需要一次更新多個文件。事務在這種情況下非常有用,可以防止非規範化的資料不同步。
+* 在文件資料模型中,需要一起更新的欄位通常在同一個文件中,這被視為單個物件 —— 更新單個文件時不需要多物件事務。但是,缺乏連線功能的文件資料庫會鼓勵非規範化(請參閱 “[關係型資料庫與文件資料庫在今日的對比](ch2.md#關係型資料庫與文件資料庫在今日的對比)”)。當需要更新非規範化的資訊時,如 [圖 7-2](../img/fig7-2.png) 所示,需要一次更新多個文件。事務在這種情況下非常有用,可以防止非規範化的資料不同步。
 * 在具有次級索引的資料庫中(除了純粹的鍵值儲存以外幾乎都有),每次更改值時都需要更新索引。從事務角度來看,這些索引是不同的資料庫物件:例如,如果沒有事務隔離性,記錄可能出現在一個索引中,但沒有出現在另一個索引中,因為第二個索引的更新還沒有發生。
 
-這些應用仍然可以在沒有事務的情況下實現。然而,**沒有原子性,錯誤處理就要複雜得多,缺乏隔離性,就會導致併發問題**。我們將在“[弱隔離級別](#弱隔離級別)”中討論這些問題,並在[第十二章](ch12.md)中探討其他方法。
+這些應用仍然可以在沒有事務的情況下實現。然而,**沒有原子性,錯誤處理就要複雜得多,缺乏隔離性,就會導致併發問題**。我們將在 “[弱隔離級別](#弱隔離級別)” 中討論這些問題,並在 [第十二章](ch12.md) 中探討其他方法。
 
 #### 處理錯誤和中止
 
-事務的一個關鍵特性是,如果發生錯誤,它可以中止並安全地重試。 ACID資料庫基於這樣的哲學:如果資料庫有違反其原子性,隔離性或永續性的危險,則寧願完全放棄事務,而不是留下半成品。
+事務的一個關鍵特性是,如果發生錯誤,它可以中止並安全地重試。 ACID 資料庫基於這樣的哲學:如果資料庫有違反其原子性,隔離性或永續性的危險,則寧願完全放棄事務,而不是留下半成品。
 
-然而並不是所有的系統都遵循這個哲學。特別是具有[無主複製](ch5.md#無主複製)的資料儲存,主要是在“盡力而為”的基礎上進行工作。可以概括為“資料庫將做盡可能多的事,執行遇到錯誤時,它不會撤消它已經完成的事情“ ——所以,從錯誤中恢復是應用程式的責任。
+然而並不是所有的系統都遵循這個哲學。特別是具有 [無主複製](ch5.md#無主複製) 的資料儲存,主要是在 “盡力而為” 的基礎上進行工作。可以概括為 “資料庫將做盡可能多的事,執行遇到錯誤時,它不會撤消它已經完成的事情 “ —— 所以,從錯誤中恢復是應用程式的責任。
 
-錯誤發生不可避免,但許多軟體開發人員傾向於只考慮樂觀情況,而不是錯誤處理的複雜性。例如,像Rails的ActiveRecord和Django這樣的**物件關係對映(ORM, object-relation Mapping)** 框架不會重試中斷的事務—— 這個錯誤通常會導致一個從堆疊向上傳播的異常,所以任何使用者輸入都會被丟棄,使用者拿到一個錯誤資訊。這實在是太恥辱了,因為中止的重點就是允許安全的重試。
+錯誤發生不可避免,但許多軟體開發人員傾向於只考慮樂觀情況,而不是錯誤處理的複雜性。例如,像 Rails 的 ActiveRecord 和 Django 這樣的 **物件關係對映(ORM, object-relation Mapping)** 框架不會重試中斷的事務 —— 這個錯誤通常會導致一個從堆疊向上傳播的異常,所以任何使用者輸入都會被丟棄,使用者拿到一個錯誤資訊。這實在是太恥辱了,因為中止的重點就是允許安全的重試。
 
 儘管重試一箇中止的事務是一個簡單而有效的錯誤處理機制,但它並不完美:
 
-- 如果事務實際上成功了,但是在伺服器試圖向客戶端確認提交成功時網路發生故障(所以客戶端認為提交失敗了),那麼重試事務會導致事務被執行兩次——除非你有一個額外的應用級去重機制。
+- 如果事務實際上成功了,但是在伺服器試圖向客戶端確認提交成功時網路發生故障(所以客戶端認為提交失敗了),那麼重試事務會導致事務被執行兩次 —— 除非你有一個額外的應用級去重機制。
 - 如果錯誤是由於負載過大造成的,則重試事務將使問題變得更糟,而不是更好。為了避免這種正反饋迴圈,可以限制重試次數,使用指數退避演算法,並單獨處理與過載相關的錯誤(如果允許)。
 - 僅在臨時性錯誤(例如,由於死鎖,異常情況,臨時性網路中斷和故障切換)後才值得重試。在發生永久性錯誤(例如,違反約束)之後重試是毫無意義的。
-- 如果事務在資料庫之外也有副作用,即使事務被中止,也可能發生這些副作用。例如,如果你正在傳送電子郵件,那你肯定不希望每次重試事務時都重新發送電子郵件。如果你想確保幾個不同的系統一起提交或放棄,**兩階段提交(2PC, two-phase commit)** 可以提供幫助(“[原子提交與兩階段提交](ch9.md#原子提交與兩階段提交)”中將討論這個問題)。
+- 如果事務在資料庫之外也有副作用,即使事務被中止,也可能發生這些副作用。例如,如果你正在傳送電子郵件,那你肯定不希望每次重試事務時都重新發送電子郵件。如果你想確保幾個不同的系統一起提交或放棄,**兩階段提交(2PC, two-phase commit)** 可以提供幫助(“[原子提交與兩階段提交](ch9.md#原子提交與兩階段提交)” 中將討論這個問題)。
 - 如果客戶端程序在重試中失效,任何試圖寫入資料庫的資料都將丟失。
 
 ## 弱隔離級別
 
-如果兩個事務不觸及相同的資料,它們可以安全地**並行(parallel)** 執行,因為兩者都不依賴於另一個。當一個事務讀取由另一個事務同時修改的資料時,或者當兩個事務試圖同時修改相同的資料時,併發問題(競爭條件)才會出現。
+如果兩個事務不觸及相同的資料,它們可以安全地 **並行(parallel)** 執行,因為兩者都不依賴於另一個。當一個事務讀取由另一個事務同時修改的資料時,或者當兩個事務試圖同時修改相同的資料時,併發問題(競爭條件)才會出現。
 
-併發BUG很難透過測試找到,因為這樣的錯誤只有在特殊時序下才會觸發。這樣的時序問題可能非常少發生,通常很難重現[^譯註i]。併發性也很難推理,特別是在大型應用中,你不一定知道哪些其他程式碼正在訪問資料庫。在一次只有一個使用者時,應用開發已經很麻煩了,有許多併發使用者使得它更加困難,因為任何一個數據都可能隨時改變。
+併發 BUG 很難透過測試找到,因為這樣的錯誤只有在特殊時序下才會觸發。這樣的時序問題可能非常少發生,通常很難重現 [^譯註i]。併發性也很難推理,特別是在大型應用中,你不一定知道哪些其他程式碼正在訪問資料庫。在一次只有一個使用者時,應用開發已經很麻煩了,有許多併發使用者使得它更加困難,因為任何一個數據都可能隨時改變。
 
-[^譯註i]: 軼事:偶然出現的瞬時錯誤有時稱為***Heisenbug***,而確定性的問題對應地稱為***Bohrbugs***
+[^譯註i]: 軼事:偶然出現的瞬時錯誤有時稱為 ***Heisenbug***,而確定性的問題對應地稱為 ***Bohrbugs***
 
-出於這個原因,資料庫一直試圖透過提供**事務隔離(transaction isolation)** 來隱藏應用程式開發者的併發問題。從理論上講,隔離可以透過假裝沒有併發發生,讓你的生活更加輕鬆:**可序列的(serializable)** 隔離等級意味著資料庫保證事務的效果如同序列執行(即一次一個,沒有任何併發)。
+出於這個原因,資料庫一直試圖透過提供 **事務隔離(transaction isolation)** 來隱藏應用程式開發者的併發問題。從理論上講,隔離可以透過假裝沒有併發發生,讓你的生活更加輕鬆:**可序列的(serializable)** 隔離等級意味著資料庫保證事務的效果如同序列執行(即一次一個,沒有任何併發)。
 
-實際上不幸的是:隔離並沒有那麼簡單。**可序列的隔離**會有效能損失,許多資料庫不願意支付這個代價【8】。因此,系統通常使用較弱的隔離級別來防止一部分,而不是全部的併發問題。這些隔離級別難以理解,並且會導致微妙的錯誤,但是它們仍然在實踐中被使用【23】。
+實際上不幸的是:隔離並沒有那麼簡單。**可序列的隔離** 會有效能損失,許多資料庫不願意支付這個代價【8】。因此,系統通常使用較弱的隔離級別來防止一部分,而不是全部的併發問題。這些隔離級別難以理解,並且會導致微妙的錯誤,但是它們仍然在實踐中被使用【23】。
 
-弱事務隔離級別導致的併發性錯誤不僅僅是一個理論問題。它們造成了很多的資金損失【24,25】,耗費了財務審計人員的調查【26】,並導致客戶資料被破壞【27】。關於這類問題的一個流行的評論是“如果你正在處理財務資料,請使用ACID資料庫!” —— 但是這一點沒有提到。即使是很多流行的關係型資料庫系統(通常被認為是“ACID”)也使用弱隔離級別,所以它們也不一定能防止這些錯誤的發生。
+弱事務隔離級別導致的併發性錯誤不僅僅是一個理論問題。它們造成了很多的資金損失【24,25】,耗費了財務審計人員的調查【26】,並導致客戶資料被破壞【27】。關於這類問題的一個流行的評論是 “如果你正在處理財務資料,請使用 ACID 資料庫!” —— 但是這一點沒有提到。即使是很多流行的關係型資料庫系統(通常被認為是 “ACID”)也使用弱隔離級別,所以它們也不一定能防止這些錯誤的發生。
 
 比起盲目地依賴工具,我們應該對存在的併發問題的種類,以及如何防止這些問題有深入的理解。然後就可以使用我們所掌握的工具來構建可靠和正確的應用程式。
 
-在本節中,我們將看幾個在實踐中使用的弱(**非序列的**,即nonserializable)隔離級別,並詳細討論哪種競爭條件可能發生也可能不發生,以便你可以決定什麼級別適合你的應用程式。一旦我們完成了這個工作,我們將詳細討論可序列化(請參閱“[可序列化](#可序列化)”)。我們討論的隔離級別將是非正式的,透過示例來進行。如果你需要嚴格的定義和分析它們的屬性,你可以在學術文獻中找到它們【28,29,30】。
+在本節中,我們將看幾個在實踐中使用的弱(**非序列的**,即 nonserializable)隔離級別,並詳細討論哪種競爭條件可能發生也可能不發生,以便你可以決定什麼級別適合你的應用程式。一旦我們完成了這個工作,我們將詳細討論可序列化(請參閱 “[可序列化](#可序列化)”)。我們討論的隔離級別將是非正式的,透過示例來進行。如果你需要嚴格的定義和分析它們的屬性,你可以在學術文獻中找到它們【28,29,30】。
 
 ### 讀已提交
 
-最基本的事務隔離級別是**讀已提交(Read Committed)**[^v],它提供了兩個保證:
+最基本的事務隔離級別是 **讀已提交(Read Committed)**[^v],它提供了兩個保證:
 
-1. 從資料庫讀時,只能看到已提交的資料(沒有**髒讀**,即dirty reads)。
-2. 寫入資料庫時,只會覆蓋已經寫入的資料(沒有**髒寫**,即dirty writes)。
+1. 從資料庫讀時,只能看到已提交的資料(沒有 **髒讀**,即 dirty reads)。
+2. 寫入資料庫時,只會覆蓋已經寫入的資料(沒有 **髒寫**,即 dirty writes)。
 
 我們來更詳細地討論這兩個保證。
 
-[^v]: 某些資料庫支援甚至更弱的隔離級別,稱為**讀未提交(Read uncommitted)**。它可以防止髒寫,但不防止髒讀。
+[^v]: 某些資料庫支援甚至更弱的隔離級別,稱為 **讀未提交(Read uncommitted)**。它可以防止髒寫,但不防止髒讀。
 
 #### 沒有髒讀
 
-設想一個事務已經將一些資料寫入資料庫,但事務還沒有提交或中止。另一個事務可以看到未提交的資料嗎?如果是的話,那就叫做**髒讀(dirty reads)**【2】。
+設想一個事務已經將一些資料寫入資料庫,但事務還沒有提交或中止。另一個事務可以看到未提交的資料嗎?如果是的話,那就叫做 **髒讀(dirty reads)**【2】。
 
-在**讀已提交**隔離級別執行的事務必須防止髒讀。這意味著事務的任何寫入操作只有在該事務提交時才能被其他人看到(然後所有的寫入操作都會立即變得可見)。如[圖7-4](../img/fig7-4.png)所示,使用者1 設定了`x = 3`,但使用者2 的 `get x `仍舊返回舊值2 (當用戶1 尚未提交時)。
+在 **讀已提交** 隔離級別執行的事務必須防止髒讀。這意味著事務的任何寫入操作只有在該事務提交時才能被其他人看到(然後所有的寫入操作都會立即變得可見)。如 [圖 7-4](../img/fig7-4.png) 所示,使用者 1 設定了 `x = 3`,但使用者 2 的 `get x` 仍舊返回舊值 2 (當用戶 1 尚未提交時)。
 
 ![](../img/fig7-4.png)
 
-**圖7-4 沒有髒讀:使用者2只有在使用者1的事務已經提交後才能看到x的新值。**
+**圖 7-4 沒有髒讀:使用者 2 只有在使用者 1 的事務已經提交後才能看到 x 的新值。**
 
 為什麼要防止髒讀,有幾個原因:
 
-- 如果事務需要更新多個物件,髒讀取意味著另一個事務可能會只看到一部分更新。例如,在[圖7-2](../img/fig7-2.png)中,使用者看到新的未讀電子郵件,但看不到更新的計數器。這就是電子郵件的髒讀。看到處於部分更新狀態的資料庫會讓使用者感到困惑,並可能導致其他事務做出錯誤的決定。
-- 如果事務中止,則所有寫入操作都需要回滾(如[圖7-3](../img/fig7-3.png)所示)。如果資料庫允許髒讀,那就意味著一個事務可能會看到稍後需要回滾的資料,即從未實際提交給資料庫的資料。想想後果就讓人頭大。
+- 如果事務需要更新多個物件,髒讀取意味著另一個事務可能會只看到一部分更新。例如,在 [圖 7-2](../img/fig7-2.png) 中,使用者看到新的未讀電子郵件,但看不到更新的計數器。這就是電子郵件的髒讀。看到處於部分更新狀態的資料庫會讓使用者感到困惑,並可能導致其他事務做出錯誤的決定。
+- 如果事務中止,則所有寫入操作都需要回滾(如 [圖 7-3](../img/fig7-3.png) 所示)。如果資料庫允許髒讀,那就意味著一個事務可能會看到稍後需要回滾的資料,即從未實際提交給資料庫的資料。想想後果就讓人頭大。
 
 #### 沒有髒寫
 
 如果兩個事務同時嘗試更新資料庫中的相同物件,會發生什麼情況?我們不知道寫入的順序是怎樣的,但是我們通常認為後面的寫入會覆蓋前面的寫入。
 
-但是,如果先前的寫入是尚未提交事務的一部分,又會發生什麼情況,後面的寫入會覆蓋一個尚未提交的值?這被稱作**髒寫(dirty write)**【28】。在**讀已提交**的隔離級別上執行的事務必須防止髒寫,通常是延遲第二次寫入,直到第一次寫入事務提交或中止為止。
+但是,如果先前的寫入是尚未提交事務的一部分,又會發生什麼情況,後面的寫入會覆蓋一個尚未提交的值?這被稱作 **髒寫(dirty write)**【28】。在 **讀已提交** 的隔離級別上執行的事務必須防止髒寫,通常是延遲第二次寫入,直到第一次寫入事務提交或中止為止。
 
 透過防止髒寫,這個隔離級別避免了一些併發問題:
 
-- 如果事務更新多個物件,髒寫會導致不好的結果。例如,考慮 [圖7-5](../img/fig7-5.png),以一個二手車銷售網站為例,Alice和Bob兩個人同時試圖購買同一輛車。購買汽車需要兩次資料庫寫入:網站上的商品列表需要更新,以反映買家的購買,銷售發票需要傳送給買家。在[圖7-5](../img/fig7-5.png)的情況下,銷售是屬於Bob的(因為他成功更新了商品列表),但發票卻寄送給了愛麗絲(因為她成功更新了發票表)。讀已提交會阻止這樣的事故。
-- 但是,讀已提交併不能防止[圖7-1](../img/fig7-1.png)中兩個計數器增量之間的競爭狀態。在這種情況下,第二次寫入發生在第一個事務提交後,所以它不是一個髒寫。這仍然是不正確的,但是出於不同的原因,在“[防止更新丟失](#防止丟失更新)”中將討論如何使這種計數器增量安全。
+- 如果事務更新多個物件,髒寫會導致不好的結果。例如,考慮 [圖 7-5](../img/fig7-5.png),以一個二手車銷售網站為例,Alice 和 Bob 兩個人同時試圖購買同一輛車。購買汽車需要兩次資料庫寫入:網站上的商品列表需要更新,以反映買家的購買,銷售發票需要傳送給買家。在 [圖 7-5](../img/fig7-5.png) 的情況下,銷售是屬於 Bob 的(因為他成功更新了商品列表),但發票卻寄送給了愛麗絲(因為她成功更新了發票表)。讀已提交會阻止這樣的事故。
+- 但是,讀已提交併不能防止 [圖 7-1](../img/fig7-1.png) 中兩個計數器增量之間的競爭狀態。在這種情況下,第二次寫入發生在第一個事務提交後,所以它不是一個髒寫。這仍然是不正確的,但是出於不同的原因,在 “[防止更新丟失](#防止丟失更新)” 中將討論如何使這種計數器增量安全。
 
 ![](../img/fig7-5.png)
 
-**圖7-5 如果存在髒寫,來自不同事務的衝突寫入可能會混淆在一起**
+**圖 7-5 如果存在髒寫,來自不同事務的衝突寫入可能會混淆在一起**
 
 #### 實現讀已提交
 
-**讀已提交**是一個非常流行的隔離級別。這是Oracle 11g,PostgreSQL,SQL Server 2012,MemSQL和其他許多資料庫的預設設定【8】。
+**讀已提交** 是一個非常流行的隔離級別。這是 Oracle 11g、PostgreSQL、SQL Server 2012、MemSQL 和其他許多資料庫的預設設定【8】。
 
-最常見的情況是,資料庫透過使用**行鎖(row-level lock)** 來防止髒寫:當事務想要修改特定物件(行或文件)時,它必須首先獲得該物件的鎖。然後必須持有該鎖直到事務被提交或中止。一次只有一個事務可持有任何給定物件的鎖;如果另一個事務要寫入同一個物件,則必須等到第一個事務提交或中止後,才能獲取該鎖並繼續。這種鎖定是讀已提交模式(或更強的隔離級別)的資料庫自動完成的。
+最常見的情況是,資料庫透過使用 **行鎖(row-level lock)** 來防止髒寫:當事務想要修改特定物件(行或文件)時,它必須首先獲得該物件的鎖。然後必須持有該鎖直到事務被提交或中止。一次只有一個事務可持有任何給定物件的鎖;如果另一個事務要寫入同一個物件,則必須等到第一個事務提交或中止後,才能獲取該鎖並繼續。這種鎖定是讀已提交模式(或更強的隔離級別)的資料庫自動完成的。
 
 如何防止髒讀?一種選擇是使用相同的鎖,並要求任何想要讀取物件的事務來簡單地獲取該鎖,然後在讀取之後立即再次釋放該鎖。這能確保在讀取進行時,物件不會在髒的、有未提交的值的狀態(因為在那段時間鎖會被寫入該物件的事務持有)。
 
 但是要求讀鎖的辦法在實踐中效果並不好。因為一個長時間執行的寫入事務會迫使許多隻讀事務等到這個慢寫入事務完成。這會損失只讀事務的響應時間,並且不利於可操作性:因為等待鎖,應用某個部分的遲緩可能由於連鎖效應,導致其他部分出現問題。
 
-出於這個原因,大多數資料庫[^vi]使用[圖7-4](../img/fig7-4.png)的方式防止髒讀:對於寫入的每個物件,資料庫都會記住舊的已提交值,和由當前持有寫入鎖的事務設定的新值。當事務正在進行時,任何其他讀取物件的事務都會拿到舊值。 只有當新值提交後,事務才會切換到讀取新值。
+出於這個原因,大多數資料庫 [^vi] 使用 [圖 7-4](../img/fig7-4.png) 的方式防止髒讀:對於寫入的每個物件,資料庫都會記住舊的已提交值,和由當前持有寫入鎖的事務設定的新值。當事務正在進行時,任何其他讀取物件的事務都會拿到舊值。 只有當新值提交後,事務才會切換到讀取新值。
 
-[^vi]: 在撰寫本文時,唯一在讀已提交隔離級別使用讀鎖的主流資料庫是使用`read_committed_snapshot = off`配置的IBM DB2和Microsoft SQL Server 【23,36】。
+[^vi]: 在撰寫本文時,唯一在讀已提交隔離級別使用讀鎖的主流資料庫是使用 `read_committed_snapshot = off` 配置的 IBM DB2 和 Microsoft SQL Server 【23,36】。
 
 ### 快照隔離和可重複讀
 
-如果只從表面上看讀已提交隔離級別你就認為它完成了事務所需的一切,那是可以原諒的。它允許**中止**(原子性的要求);它防止讀取不完整的事務結果,並且防止併發寫入造成的混亂。事實上這些功能非常有用,比起沒有事務的系統來,可以提供更多的保證。
+如果只從表面上看讀已提交隔離級別你就認為它完成了事務所需的一切,那是可以原諒的。它允許 **中止**(原子性的要求);它防止讀取不完整的事務結果,並且防止併發寫入造成的混亂。事實上這些功能非常有用,比起沒有事務的系統來,可以提供更多的保證。
 
-但是在使用此隔離級別時,仍然有很多地方可能會產生併發錯誤。例如[圖7-6](../img/fig7-6.png)說明了讀已提交時可能發生的問題。
+但是在使用此隔離級別時,仍然有很多地方可能會產生併發錯誤。例如 [圖 7-6](../img/fig7-6.png) 說明了讀已提交時可能發生的問題。
 
 ![](../img/fig7-6.png)
 
-**圖7-6 讀取偏差:Alice觀察資料庫處於不一致的狀態**
+**圖 7-6 讀取偏差:Alice 觀察資料庫處於不一致的狀態**
 
-愛麗絲在銀行有1000美元的儲蓄,分為兩個賬戶,每個500美元。現在有一筆事務從她的一個賬戶轉移了100美元到另一個賬戶。如果她非常不幸地在事務處理的過程中檢視其賬戶餘額列表,她可能會在收到付款之前先看到一個賬戶的餘額(收款賬戶,餘額仍為500美元),在發出轉賬之後再看到另一個賬戶的餘額(付款賬戶,新餘額為400美元)。對愛麗絲來說,現在她的賬戶似乎總共只有900美元——看起來有100美元已經憑空消失了。
+愛麗絲在銀行有 1000 美元的儲蓄,分為兩個賬戶,每個 500 美元。現在有一筆事務從她的一個賬戶轉移了 100 美元到另一個賬戶。如果她非常不幸地在事務處理的過程中檢視其賬戶餘額列表,她可能會在收到付款之前先看到一個賬戶的餘額(收款賬戶,餘額仍為 500 美元),在發出轉賬之後再看到另一個賬戶的餘額(付款賬戶,新餘額為 400 美元)。對愛麗絲來說,現在她的賬戶似乎總共只有 900 美元 —— 看起來有 100 美元已經憑空消失了。
 
-這種異常被稱為**不可重複讀(nonrepeatable read)** 或**讀取偏差(read skew)**:如果Alice在事務結束時再次讀取賬戶1的餘額,她將看到與她之前的查詢中看到的不同的值(600美元)。在讀已提交的隔離條件下,**不可重複讀**被認為是可接受的:Alice看到的帳戶餘額時確實在閱讀時已經提交了。
+這種異常被稱為 **不可重複讀(nonrepeatable read)** 或 **讀取偏差(read skew)**:如果 Alice 在事務結束時再次讀取賬戶 1 的餘額,她將看到與她之前的查詢中看到的不同的值(600 美元)。在讀已提交的隔離條件下,**不可重複讀** 被認為是可接受的:Alice 看到的帳戶餘額時確實在閱讀時已經提交了。
 
-> 不幸的是,術語**偏差(skew)** 這個詞是過載的:以前使用它是因為熱點的不平衡工作量(請參閱“[負載偏斜與熱點消除](ch6.md#負載偏斜與熱點消除)”),而這裡偏差意味著異常的時序。
+> 不幸的是,術語 **偏差(skew)** 這個詞是過載的:以前使用它是因為熱點的不平衡工作量(請參閱 “[負載偏斜與熱點消除](ch6.md#負載偏斜與熱點消除)”),而這裡偏差意味著異常的時序。
 
-對於Alice的情況,這不是一個長期持續的問題。因為如果她幾秒鐘後重新整理銀行網站的頁面,她很可能會看到一致的帳戶餘額。但是有些情況下,不能容忍這種暫時的不一致:
+對於 Alice 的情況,這不是一個長期持續的問題。因為如果她幾秒鐘後重新整理銀行網站的頁面,她很可能會看到一致的帳戶餘額。但是有些情況下,不能容忍這種暫時的不一致:
 
 * 備份
 
@@ -307,46 +307,46 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
 
 * 分析查詢和完整性檢查
 
-  有時,你可能需要執行一個查詢,掃描大部分的資料庫。這樣的查詢在分析中很常見(請參閱“[事務處理還是分析?](ch3.md#事務處理還是分析?)”),也可能是定期完整性檢查(即監視資料損壞)的一部分。如果這些查詢在不同時間點觀察資料庫的不同部分,則可能會返回毫無意義的結果。
+  有時,你可能需要執行一個查詢,掃描大部分的資料庫。這樣的查詢在分析中很常見(請參閱 “[事務處理還是分析?](ch3.md#事務處理還是分析?)”),也可能是定期完整性檢查(即監視資料損壞)的一部分。如果這些查詢在不同時間點觀察資料庫的不同部分,則可能會返回毫無意義的結果。
 
-**快照隔離(snapshot isolation)**【28】是這個問題最常見的解決方案。想法是,每個事務都從資料庫的**一致快照(consistent snapshot)** 中讀取——也就是說,事務可以看到事務開始時在資料庫中提交的所有資料。即使這些資料隨後被另一個事務更改,每個事務也只能看到該特定時間點的舊資料。
+**快照隔離(snapshot isolation)**【28】是這個問題最常見的解決方案。想法是,每個事務都從資料庫的 **一致快照(consistent snapshot)** 中讀取 —— 也就是說,事務可以看到事務開始時在資料庫中提交的所有資料。即使這些資料隨後被另一個事務更改,每個事務也只能看到該特定時間點的舊資料。
 
 快照隔離對長時間執行的只讀查詢(如備份和分析)非常有用。如果查詢的資料在查詢執行的同時發生變化,則很難理解查詢的含義。當一個事務可以看到資料庫在某個特定時間點凍結時的一致快照,理解起來就很容易了。
 
-快照隔離是一個流行的功能:PostgreSQL,使用InnoDB引擎的MySQL,Oracle,SQL Server等都支援【23,31,32】。
+快照隔離是一個流行的功能:PostgreSQL、使用 InnoDB 引擎的 MySQL、Oracle、SQL Server 等都支援【23,31,32】。
 
 #### 實現快照隔離
 
-與讀取提交的隔離類似,快照隔離的實現通常使用寫鎖來防止髒寫(請參閱“[讀已提交](#讀已提交)”),這意味著進行寫入的事務會阻止另一個事務修改同一個物件。但是讀取不需要任何鎖定。從效能的角度來看,快照隔離的一個關鍵原則是:**讀不阻塞寫,寫不阻塞讀**。這允許資料庫在處理一致性快照上的長時間查詢時,可以正常地同時處理寫入操作。且兩者間沒有任何鎖定爭用。
+與讀取提交的隔離類似,快照隔離的實現通常使用寫鎖來防止髒寫(請參閱 “[讀已提交](#讀已提交)”),這意味著進行寫入的事務會阻止另一個事務修改同一個物件。但是讀取不需要任何鎖定。從效能的角度來看,快照隔離的一個關鍵原則是:**讀不阻塞寫,寫不阻塞讀**。這允許資料庫在處理一致性快照上的長時間查詢時,可以正常地同時處理寫入操作。且兩者間沒有任何鎖定爭用。
 
-為了實現快照隔離,資料庫使用了我們看到的用於防止[圖7-4](../img/fig7-4.png)中的髒讀的機制的一般化。資料庫必須可能保留一個物件的幾個不同的提交版本,因為各種正在進行的事務可能需要看到資料庫在不同的時間點的狀態。因為它同時維護著單個物件的多個版本,所以這種技術被稱為**多版本併發控制(MVCC, multi-version concurrency control)**。
+為了實現快照隔離,資料庫使用了我們看到的用於防止 [圖 7-4](../img/fig7-4.png) 中的髒讀的機制的一般化。資料庫必須可能保留一個物件的幾個不同的提交版本,因為各種正在進行的事務可能需要看到資料庫在不同的時間點的狀態。因為它同時維護著單個物件的多個版本,所以這種技術被稱為 **多版本併發控制(MVCC, multi-version concurrency control)**。
 
-如果一個數據庫只需要提供**讀已提交**的隔離級別,而不提供**快照隔離**,那麼保留一個物件的兩個版本就足夠了:提交的版本和被覆蓋但尚未提交的版本。支援快照隔離的儲存引擎通常也使用MVCC來實現**讀已提交**隔離級別。一種典型的方法是**讀已提交**為每個查詢使用單獨的快照,而**快照隔離**對整個事務使用相同的快照。
+如果一個數據庫只需要提供 **讀已提交** 的隔離級別,而不提供 **快照隔離**,那麼保留一個物件的兩個版本就足夠了:提交的版本和被覆蓋但尚未提交的版本。支援快照隔離的儲存引擎通常也使用 MVCC 來實現 **讀已提交** 隔離級別。一種典型的方法是 **讀已提交** 為每個查詢使用單獨的快照,而 **快照隔離** 對整個事務使用相同的快照。
 
-[圖7-7](../img/fig7-7.png)說明了如何在PostgreSQL中實現基於MVCC的快照隔離【31】(其他實現類似)。當一個事務開始時,它被賦予一個唯一的,永遠增長[^vii]的事務ID(`txid`)。每當事務向資料庫寫入任何內容時,它所寫入的資料都會被標記上寫入者的事務ID。
+[圖 7-7](../img/fig7-7.png) 說明了如何在 PostgreSQL 中實現基於 MVCC 的快照隔離【31】(其他實現類似)。當一個事務開始時,它被賦予一個唯一的,永遠增長 [^vii] 的事務 ID(`txid`)。每當事務向資料庫寫入任何內容時,它所寫入的資料都會被標記上寫入者的事務 ID。
 
-[^vii]: 事實上,事務ID是32位整數,所以大約會在40億次事務之後溢位。 PostgreSQL的Vacuum過程會清理老舊的事務ID,確保事務ID溢位(回捲)不會影響到資料。
+[^vii]: 事實上,事務 ID 是 32 位整數,所以大約會在 40 億次事務之後溢位。 PostgreSQL 的 Vacuum 過程會清理老舊的事務 ID,確保事務 ID 溢位(回捲)不會影響到資料。
 
 ![](../img/fig7-7.png)
 
-**圖7-7 使用多版本物件實現快照隔離**
+**圖 7-7 使用多版本物件實現快照隔離**
 
-表中的每一行都有一個 `created_by` 欄位,其中包含將該行插入到表中的的事務ID。此外,每行都有一個 `deleted_by` 欄位,最初是空的。如果某個事務刪除了一行,那麼該行實際上並未從資料庫中刪除,而是透過將 `deleted_by` 欄位設定為請求刪除的事務的ID來標記為刪除。在稍後的時間,當確定沒有事務可以再訪問已刪除的資料時,資料庫中的垃圾收集過程會將所有帶有刪除標記的行移除,並釋放其空間。[^譯註ii]
+表中的每一行都有一個 `created_by` 欄位,其中包含將該行插入到表中的的事務 ID。此外,每行都有一個 `deleted_by` 欄位,最初是空的。如果某個事務刪除了一行,那麼該行實際上並未從資料庫中刪除,而是透過將 `deleted_by` 欄位設定為請求刪除的事務的 ID 來標記為刪除。在稍後的時間,當確定沒有事務可以再訪問已刪除的資料時,資料庫中的垃圾收集過程會將所有帶有刪除標記的行移除,並釋放其空間。[^譯註ii]
 
-[^譯註ii]: 在PostgreSQL中,`created_by` 的實際名稱為`xmin`,`deleted_by` 的實際名稱為`xmax`
+[^譯註ii]: 在 PostgreSQL 中,`created_by` 的實際名稱為 `xmin`,`deleted_by` 的實際名稱為 `xmax`
 
-`UPDATE` 操作在內部翻譯為 `DELETE` 和 `INSERT` 。例如,在[圖7-7](../img/fig7-7.png)中,事務13 從賬戶2 中扣除100美元,將餘額從500美元改為400美元。實際上包含兩條賬戶2 的記錄:餘額為 \$500 的行被標記為**被事務13刪除**,餘額為 \$400 的行**由事務13建立**。
+`UPDATE` 操作在內部翻譯為 `DELETE` 和 `INSERT` 。例如,在 [圖 7-7](../img/fig7-7.png) 中,事務 13 從賬戶 2 中扣除 100 美元,將餘額從 500 美元改為 400 美元。實際上包含兩條賬戶 2 的記錄:餘額為 \$500 的行被標記為 **被事務 13 刪除**,餘額為 \$400 的行 **由事務 13 建立**。
 
 #### 觀察一致性快照的可見性規則
 
-當一個事務從資料庫中讀取時,事務ID用於決定它可以看見哪些物件,看不見哪些物件。透過仔細定義可見性規則,資料庫可以嚮應用程式呈現一致的資料庫快照。工作如下:
+當一個事務從資料庫中讀取時,事務 ID 用於決定它可以看見哪些物件,看不見哪些物件。透過仔細定義可見性規則,資料庫可以嚮應用程式呈現一致的資料庫快照。工作如下:
 
 1. 在每次事務開始時,資料庫列出當時所有其他(尚未提交或尚未中止)的事務清單,即使之後提交了,這些事務已執行的任何寫入也都會被忽略。
 2. 被中止事務所執行的任何寫入都將被忽略。
-3. 由具有較晚事務ID(即,在當前事務開始之後開始的)的事務所做的任何寫入都被忽略,而不管這些事務是否已經提交。
+3. 由具有較晚事務 ID(即,在當前事務開始之後開始的)的事務所做的任何寫入都被忽略,而不管這些事務是否已經提交。
 4. 所有其他寫入,對應用都是可見的。
 
-這些規則適用於建立和刪除物件。在[圖7-7](../img/fig7-7.png)中,當事務12 從賬戶2 讀取時,它會看到 \$500 的餘額,因為 \$500 餘額的刪除是由事務13 完成的(根據規則3,事務12 看不到事務13 執行的刪除),且400美元記錄的建立也是不可見的(按照相同的規則)。
+這些規則適用於建立和刪除物件。在 [圖 7-7](../img/fig7-7.png) 中,當事務 12 從賬戶 2 讀取時,它會看到 \$500 的餘額,因為 \$500 餘額的刪除是由事務 13 完成的(根據規則 3,事務 12 看不到事務 13 執行的刪除),且 400 美元記錄的建立也是不可見的(按照相同的規則)。
 
 換句話說,如果以下兩個條件都成立,則可見一個物件:
 
@@ -359,59 +359,59 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
 
 索引如何在多版本資料庫中工作?一種選擇是使索引簡單地指向物件的所有版本,並且需要索引查詢來過濾掉當前事務不可見的任何物件版本。當垃圾收集刪除任何事務不再可見的舊物件版本時,相應的索引條目也可以被刪除。
 
-在實踐中,許多實現細節決定了多版本併發控制的效能。例如,如果同一物件的不同版本可以放入同一個頁面中,PostgreSQL的最佳化可以避免更新索引【31】。
+在實踐中,許多實現細節決定了多版本併發控制的效能。例如,如果同一物件的不同版本可以放入同一個頁面中,PostgreSQL 的最佳化可以避免更新索引【31】。
 
-在CouchDB,Datomic和LMDB中使用另一種方法。雖然它們也使用[B樹](ch3.md#B樹),但它們使用的是一種**僅追加/寫時複製(append-only/copy-on-write)** 的變體,它們在更新時不覆蓋樹的頁面,而為每個修改頁面建立一份副本。從父頁面直到樹根都會級聯更新,以指向它們子頁面的新版本。任何不受寫入影響的頁面都不需要被複制,並且保持不變【33,34,35】。
+在 CouchDB、Datomic 和 LMDB 中使用另一種方法。雖然它們也使用 [B 樹](ch3.md#B樹),但它們使用的是一種 **僅追加 / 寫時複製(append-only/copy-on-write)** 的變體,它們在更新時不覆蓋樹的頁面,而為每個修改頁面建立一份副本。從父頁面直到樹根都會級聯更新,以指向它們子頁面的新版本。任何不受寫入影響的頁面都不需要被複制,並且保持不變【33,34,35】。
 
-使用僅追加的B樹,每個寫入事務(或一批事務)都會建立一顆新的B樹,當建立時,從該特定樹根生長的樹就是資料庫的一個一致性快照。沒必要根據事務ID過濾掉物件,因為後續寫入不能修改現有的B樹;它們只能建立新的樹根。但這種方法也需要一個負責壓縮和垃圾收集的後臺程序。
+使用僅追加的 B 樹,每個寫入事務(或一批事務)都會建立一顆新的 B 樹,當建立時,從該特定樹根生長的樹就是資料庫的一個一致性快照。沒必要根據事務 ID 過濾掉物件,因為後續寫入不能修改現有的 B 樹;它們只能建立新的樹根。但這種方法也需要一個負責壓縮和垃圾收集的後臺程序。
 
 #### 可重複讀與命名混淆
 
-快照隔離是一個有用的隔離級別,特別對於只讀事務而言。但是,許多資料庫實現了它,卻用不同的名字來稱呼。在Oracle中稱為**可序列化(Serializable)** 的,在PostgreSQL和MySQL中稱為**可重複讀(repeatable read)**【23】。
+快照隔離是一個有用的隔離級別,特別對於只讀事務而言。但是,許多資料庫實現了它,卻用不同的名字來稱呼。在 Oracle 中稱為 **可序列化(Serializable)** 的,在 PostgreSQL 和 MySQL 中稱為 **可重複讀(repeatable read)**【23】。
 
-這種命名混淆的原因是SQL標準沒有**快照隔離**的概念,因為標準是基於System R 1975年定義的隔離級別【2】,那時候**快照隔離**尚未發明。相反,它定義了**可重複讀**,表面上看起來與快照隔離很相似。 PostgreSQL和MySQL稱其**快照隔離**級別為**可重複讀(repeatable read)**,因為這樣符合標準要求,所以它們可以聲稱自己“標準相容”。
+這種命名混淆的原因是 SQL 標準沒有 **快照隔離** 的概念,因為標準是基於 System R 1975 年定義的隔離級別【2】,那時候 **快照隔離** 尚未發明。相反,它定義了 **可重複讀**,表面上看起來與快照隔離很相似。 PostgreSQL 和 MySQL 稱其 **快照隔離** 級別為 **可重複讀(repeatable read)**,因為這樣符合標準要求,所以它們可以聲稱自己 “標準相容”。
 
-不幸的是,SQL標準對隔離級別的定義是有缺陷的——模糊,不精確,並不像標準應有的樣子獨立於實現【28】。有幾個資料庫實現了可重複讀,但它們實際提供的保證存在很大的差異,儘管表面上是標準化的【23】。在研究文獻【29,30】中已經有了可重複讀的正式定義,但大多數的實現並不能滿足這個正式定義。最後,IBM DB2使用“可重複讀”來引用可序列化【8】。
+不幸的是,SQL 標準對隔離級別的定義是有缺陷的 —— 模糊,不精確,並不像標準應有的樣子獨立於實現【28】。有幾個資料庫實現了可重複讀,但它們實際提供的保證存在很大的差異,儘管表面上是標準化的【23】。在研究文獻【29,30】中已經有了可重複讀的正式定義,但大多數的實現並不能滿足這個正式定義。最後,IBM DB2 使用 “可重複讀” 來引用可序列化【8】。
 
-結果,沒有人真正知道**可重複讀**的意思。
+結果,沒有人真正知道 **可重複讀** 的意思。
 
 ### 防止丟失更新
 
-到目前為止已經討論的**讀已提交**和**快照隔離**級別,主要保證了**只讀事務在併發寫入時**可以看到什麼。卻忽略了兩個事務併發寫入的問題——我們只討論了髒寫(請參閱“[沒有髒寫](#沒有髒寫)”),一種特定型別的寫-寫衝突是可能出現的。
+到目前為止已經討論的 **讀已提交** 和 **快照隔離** 級別,主要保證了 **只讀事務在併發寫入時** 可以看到什麼。卻忽略了兩個事務併發寫入的問題 —— 我們只討論了髒寫(請參閱 “[沒有髒寫](#沒有髒寫)”),一種特定型別的寫 - 寫衝突是可能出現的。
 
-併發的寫入事務之間還有其他幾種有趣的衝突。其中最著名的是**丟失更新(lost update)** 問題,如[圖7-1](../img/fig7-1.png)所示,以兩個併發計數器增量為例。
+併發的寫入事務之間還有其他幾種有趣的衝突。其中最著名的是 **丟失更新(lost update)** 問題,如 [圖 7-1](../img/fig7-1.png) 所示,以兩個併發計數器增量為例。
 
-如果應用從資料庫中讀取一些值,修改它並寫回修改的值(讀取-修改-寫入序列),則可能會發生丟失更新的問題。如果兩個事務同時執行,則其中一個的修改可能會丟失,因為第二個寫入的內容並沒有包括第一個事務的修改(有時會說後面寫入**狠揍(clobber)** 了前面的寫入)這種模式發生在各種不同的情況下:
+如果應用從資料庫中讀取一些值,修改它並寫回修改的值(讀取 - 修改 - 寫入序列),則可能會發生丟失更新的問題。如果兩個事務同時執行,則其中一個的修改可能會丟失,因為第二個寫入的內容並沒有包括第一個事務的修改(有時會說後面寫入 **狠揍(clobber)** 了前面的寫入)這種模式發生在各種不同的情況下:
 
 - 增加計數器或更新賬戶餘額(需要讀取當前值,計算新值並寫回更新後的值)
-- 在複雜值中進行本地修改:例如,將元素新增到JSON文件中的一個列表(需要解析文件,進行更改並寫回修改的文件)
-- 兩個使用者同時編輯wiki頁面,每個使用者透過將整個頁面內容傳送到伺服器來儲存其更改,覆寫資料庫中當前的任何內容。
+- 在複雜值中進行本地修改:例如,將元素新增到 JSON 文件中的一個列表(需要解析文件,進行更改並寫回修改的文件)
+- 兩個使用者同時編輯 wiki 頁面,每個使用者透過將整個頁面內容傳送到伺服器來儲存其更改,覆寫資料庫中當前的任何內容。
 
 這是一個普遍的問題,所以已經開發了各種解決方案。
 
 #### 原子寫
 
-許多資料庫提供了原子更新操作,從而消除了在應用程式程式碼中執行讀取-修改-寫入序列的需要。如果你的程式碼可以用這些操作來表達,那這通常是最好的解決方案。例如,下面的指令在大多數關係資料庫中是併發安全的:
+許多資料庫提供了原子更新操作,從而消除了在應用程式程式碼中執行讀取 - 修改 - 寫入序列的需要。如果你的程式碼可以用這些操作來表達,那這通常是最好的解決方案。例如,下面的指令在大多數關係資料庫中是併發安全的:
 
 ```sql
 UPDATE counters SET value = value + 1 WHERE key = 'foo';
 ```
 
-類似地,像MongoDB這樣的文件資料庫提供了對JSON文件的一部分進行本地修改的原子操作,Redis提供了修改資料結構(如優先順序佇列)的原子操作。並不是所有的寫操作都可以用原子操作的方式來表達,例如維基頁面的更新涉及到任意文字編輯[^viii],但是在可以使用原子操作的情況下,它們通常是最好的選擇。
+類似地,像 MongoDB 這樣的文件資料庫提供了對 JSON 文件的一部分進行本地修改的原子操作,Redis 提供了修改資料結構(如優先順序佇列)的原子操作。並不是所有的寫操作都可以用原子操作的方式來表達,例如維基頁面的更新涉及到任意文字編輯 [^viii],但是在可以使用原子操作的情況下,它們通常是最好的選擇。
 
-[^viii]: 將文字文件的編輯表示為原子的變化流是可能的,儘管相當複雜。請參閱“[自動衝突解決](ch5.md#自動衝突解決)”。
+[^viii]: 將文字文件的編輯表示為原子的變化流是可能的,儘管相當複雜。請參閱 “[自動衝突解決](ch5.md#自動衝突解決)”。
 
-原子操作通常透過在讀取物件時,獲取其上的排它鎖來實現。以便更新完成之前沒有其他事務可以讀取它。這種技術有時被稱為**遊標穩定性(cursor stability)**【36,37】。另一個選擇是簡單地強制所有的原子操作在單一執行緒上執行。
+原子操作通常透過在讀取物件時,獲取其上的排它鎖來實現。以便更新完成之前沒有其他事務可以讀取它。這種技術有時被稱為 **遊標穩定性(cursor stability)**【36,37】。另一個選擇是簡單地強制所有的原子操作在單一執行緒上執行。
 
-不幸的是,ORM框架很容易意外地執行不安全的讀取-修改-寫入序列,而不是使用資料庫提供的原子操作【38】。如果你知道自己在做什麼那當然不是問題,但它經常產生那種很難測出來的微妙Bug。
+不幸的是,ORM 框架很容易意外地執行不安全的讀取 - 修改 - 寫入序列,而不是使用資料庫提供的原子操作【38】。如果你知道自己在做什麼那當然不是問題,但它經常產生那種很難測出來的微妙 Bug。
 
 #### 顯式鎖定
 
-如果資料庫的內建原子操作沒有提供必要的功能,防止丟失更新的另一個選擇是讓應用程式顯式地鎖定將要更新的物件。然後應用程式可以執行讀取-修改-寫入序列,如果任何其他事務嘗試同時讀取同一個物件,則強制等待,直到第一個**讀取-修改-寫入序列**完成。
+如果資料庫的內建原子操作沒有提供必要的功能,防止丟失更新的另一個選擇是讓應用程式顯式地鎖定將要更新的物件。然後應用程式可以執行讀取 - 修改 - 寫入序列,如果任何其他事務嘗試同時讀取同一個物件,則強制等待,直到第一個 **讀取 - 修改 - 寫入序列** 完成。
 
-例如,考慮一個多人遊戲,其中幾個玩家可以同時移動相同的棋子。在這種情況下,一個原子操作可能是不夠的,因為應用程式還需要確保玩家的移動符合遊戲規則,這可能涉及到一些不能合理地用資料庫查詢實現的邏輯。但你可以使用鎖來防止兩名玩家同時移動相同的棋子,如例7-1所示。
+例如,考慮一個多人遊戲,其中幾個玩家可以同時移動相同的棋子。在這種情況下,一個原子操作可能是不夠的,因為應用程式還需要確保玩家的移動符合遊戲規則,這可能涉及到一些不能合理地用資料庫查詢實現的邏輯。但你可以使用鎖來防止兩名玩家同時移動相同的棋子,如例 7-1 所示。
 
-**例7-1 顯式鎖定行以防止丟失更新**
+**例 7-1 顯式鎖定行以防止丟失更新**
 
 ```plsql
 BEGIN TRANSACTION;
@@ -419,28 +419,28 @@ SELECT * FROM figures
   WHERE name = 'robot' AND game_id = 222
 FOR UPDATE;
 
--- 檢查玩家的操作是否有效,然後更新先前SELECT返回棋子的位置。
+-- 檢查玩家的操作是否有效,然後更新先前 SELECT 返回棋子的位置。
 UPDATE figures SET position = 'c4' WHERE id = 1234;
 COMMIT;
 ```
 
-- `FOR UPDATE`子句告訴資料庫應該對該查詢返回的所有行加鎖。
+- `FOR UPDATE` 子句告訴資料庫應該對該查詢返回的所有行加鎖。
 
 這是有效的,但要做對,你需要仔細考慮應用邏輯。忘記在程式碼某處加鎖很容易引入競爭條件。
 
 #### 自動檢測丟失的更新
 
-原子操作和鎖是透過強制**讀取-修改-寫入序列**按順序發生,來防止丟失更新的方法。另一種方法是允許它們並行執行,如果事務管理器檢測到丟失更新,則中止事務並強制它們重試其**讀取-修改-寫入序列**。
+原子操作和鎖是透過強制 **讀取 - 修改 - 寫入序列** 按順序發生,來防止丟失更新的方法。另一種方法是允許它們並行執行,如果事務管理器檢測到丟失更新,則中止事務並強制它們重試其 **讀取 - 修改 - 寫入序列**。
 
-這種方法的一個優點是,資料庫可以結合快照隔離高效地執行此檢查。事實上,PostgreSQL的可重複讀,Oracle的可序列化和SQL Server的快照隔離級別,都會自動檢測到丟失更新,並中止惹麻煩的事務。但是,MySQL/InnoDB的可重複讀並不會檢測**丟失更新**【23】。一些作者【28,30】認為,資料庫必須能防止丟失更新才稱得上是提供了**快照隔離**,所以在這個定義下,MySQL下不提供快照隔離。
+這種方法的一個優點是,資料庫可以結合快照隔離高效地執行此檢查。事實上,PostgreSQL 的可重複讀,Oracle 的可序列化和 SQL Server 的快照隔離級別,都會自動檢測到丟失更新,並中止惹麻煩的事務。但是,MySQL/InnoDB 的可重複讀並不會檢測 **丟失更新**【23】。一些作者【28,30】認為,資料庫必須能防止丟失更新才稱得上是提供了 **快照隔離**,所以在這個定義下,MySQL 下不提供快照隔離。
 
 丟失更新檢測是一個很好的功能,因為它不需要應用程式碼使用任何特殊的資料庫功能,你可能會忘記使用鎖或原子操作,從而引入錯誤;但丟失更新的檢測是自動發生的,因此不太容易出錯。
 
 #### 比較並設定(CAS)
 
-在不提供事務的資料庫中,有時會發現一種原子操作:**比較並設定(CAS, 即Compare And Set,先前在“[單物件寫入](#單物件寫入)”中提到)。此操作的目的是為了避免丟失更新:只有當前值從上次讀取時一直未改變,才允許更新發生。如果當前值與先前讀取的值不匹配,則更新不起作用,且必須重試讀取-修改-寫入序列。
+在不提供事務的資料庫中,有時會發現一種原子操作:** 比較並設定(CAS, 即 Compare And Set,先前在 “[單物件寫入](#單物件寫入)” 中提到)。此操作的目的是為了避免丟失更新:只有當前值從上次讀取時一直未改變,才允許更新發生。如果當前值與先前讀取的值不匹配,則更新不起作用,且必須重試讀取 - 修改 - 寫入序列。
 
-例如,為了防止兩個使用者同時更新同一個wiki頁面,可以嘗試類似這樣的方式,只有當用戶開始編輯頁面內容時,才會發生更新:
+例如,為了防止兩個使用者同時更新同一個 wiki 頁面,可以嘗試類似這樣的方式,只有當用戶開始編輯頁面內容時,才會發生更新:
 
 ```sql
 -- 根據資料庫的實現情況,這可能安全也可能不安全
@@ -448,46 +448,46 @@ UPDATE wiki_pages SET content = '新內容'
   WHERE id = 1234 AND content = '舊內容';
 ```
 
-如果內容已經更改並且不再與“舊內容”相匹配,則此更新將不起作用,因此你需要檢查更新是否生效,必要時重試。但是,如果資料庫允許`WHERE`子句從舊快照中讀取,則此語句可能無法防止丟失更新,因為即使發生了另一個併發寫入,`WHERE`條件也可能為真。在依賴資料庫的CAS操作前要檢查其是否安全。
+如果內容已經更改並且不再與 “舊內容” 相匹配,則此更新將不起作用,因此你需要檢查更新是否生效,必要時重試。但是,如果資料庫允許 `WHERE` 子句從舊快照中讀取,則此語句可能無法防止丟失更新,因為即使發生了另一個併發寫入,`WHERE` 條件也可能為真。在依賴資料庫的 CAS 操作前要檢查其是否安全。
 
 #### 衝突解決和複製
 
-在複製資料庫中(請參閱[第五章](ch5.md)),防止丟失的更新需要考慮另一個維度:由於在多個節點上存在資料副本,並且在不同節點上的資料可能被併發地修改,因此需要採取一些額外的步驟來防止丟失更新。
+在複製資料庫中(請參閱 [第五章](ch5.md)),防止丟失的更新需要考慮另一個維度:由於在多個節點上存在資料副本,並且在不同節點上的資料可能被併發地修改,因此需要採取一些額外的步驟來防止丟失更新。
 
-鎖和CAS操作假定只有一個最新的資料副本。但是多主或無主複製的資料庫通常允許多個寫入併發執行,並非同步複製到副本上,因此無法保證只有一個最新資料的副本。所以基於鎖或CAS操作的技術不適用於這種情況(我們將在“[線性一致性](ch9.md#線性一致性)”中更詳細地討論這個問題)。
+鎖和 CAS 操作假定只有一個最新的資料副本。但是多主或無主複製的資料庫通常允許多個寫入併發執行,並非同步複製到副本上,因此無法保證只有一個最新資料的副本。所以基於鎖或 CAS 操作的技術不適用於這種情況(我們將在 “[線性一致性](ch9.md#線性一致性)” 中更詳細地討論這個問題)。
 
-相反,如“[檢測併發寫入](ch5.md#檢測併發寫入)”一節所述,這種複製資料庫中的一種常見方法是允許併發寫入建立多個衝突版本的值(也稱為兄弟),並使用應用程式碼或特殊資料結構在事實發生之後解決和合並這些版本。
+相反,如 “[檢測併發寫入](ch5.md#檢測併發寫入)” 一節所述,這種複製資料庫中的一種常見方法是允許併發寫入建立多個衝突版本的值(也稱為兄弟),並使用應用程式碼或特殊資料結構在事實發生之後解決和合並這些版本。
 
-原子操作可以在複製的上下文中很好地工作,尤其當它們具有可交換性時(即,可以在不同的副本上以不同的順序應用它們,且仍然可以得到相同的結果)。例如,遞增計數器或向集合新增元素是可交換的操作。這是Riak 2.0資料型別背後的思想,它可以防止複製副本丟失更新。當不同的客戶端同時更新一個值時,Riak自動將更新合併在一起,以免丟失更新【39】。
+原子操作可以在複製的上下文中很好地工作,尤其當它們具有可交換性時(即,可以在不同的副本上以不同的順序應用它們,且仍然可以得到相同的結果)。例如,遞增計數器或向集合新增元素是可交換的操作。這是 Riak 2.0 資料型別背後的思想,它可以防止複製副本丟失更新。當不同的客戶端同時更新一個值時,Riak 自動將更新合併在一起,以免丟失更新【39】。
 
-另一方面,最後寫入勝利(LWW)的衝突解決方法很容易丟失更新,如“[最後寫入勝利(丟棄併發寫入)](ch5.md#最後寫入勝利(丟棄併發寫入))”中所述。不幸的是,LWW是許多複製資料庫中的預設方案。
+另一方面,最後寫入勝利(LWW)的衝突解決方法很容易丟失更新,如 “[最後寫入勝利(丟棄併發寫入)](ch5.md#最後寫入勝利(丟棄併發寫入))” 中所述。不幸的是,LWW 是許多複製資料庫中的預設方案。
 
 ### 寫入偏斜與幻讀
 
-前面的章節中,我們看到了**髒寫**和**丟失更新**,當不同的事務併發地嘗試寫入相同的物件時,會出現這兩種競爭條件。為了避免資料損壞,這些競爭條件需要被阻止——既可以由資料庫自動執行,也可以透過鎖和原子寫操作這類手動安全措施來防止。
+前面的章節中,我們看到了 **髒寫** 和 **丟失更新**,當不同的事務併發地嘗試寫入相同的物件時,會出現這兩種競爭條件。為了避免資料損壞,這些競爭條件需要被阻止 —— 既可以由資料庫自動執行,也可以透過鎖和原子寫操作這類手動安全措施來防止。
 
 但是,併發寫入間可能發生的競爭條件還沒有完。在本節中,我們將看到一些更微妙的衝突例子。
 
 首先,想象一下這個例子:你正在為醫院寫一個醫生輪班管理程式。醫院通常會同時要求幾位醫生待命,但底線是至少有一位醫生在待命。醫生可以放棄他們的班次(例如,如果他們自己生病了),只要至少有一個同事在這一班中繼續工作【40,41】。
 
-現在想象一下,Alice和Bob是兩位值班醫生。兩人都感到不適,所以他們都決定請假。不幸的是,他們恰好在同一時間點選按鈕下班。[圖7-8](../img/fig7-8.png)說明了接下來的事情。
+現在想象一下,Alice 和 Bob 是兩位值班醫生。兩人都感到不適,所以他們都決定請假。不幸的是,他們恰好在同一時間點選按鈕下班。[圖 7-8](../img/fig7-8.png) 說明了接下來的事情。
 
 ![](../img/fig7-8.png)
 
-**圖7-8 寫入偏差導致應用程式錯誤的示例**
+**圖 7-8 寫入偏差導致應用程式錯誤的示例**
 
-在兩個事務中,應用首先檢查是否有兩個或以上的醫生正在值班;如果是的話,它就假定一名醫生可以安全地休班。由於資料庫使用快照隔離,兩次檢查都返回 2 ,所以兩個事務都進入下一個階段。Alice更新自己的記錄休班了,而Bob也做了一樣的事情。兩個事務都成功提交了,現在沒有醫生值班了。違反了至少有一名醫生在值班的要求。
+在兩個事務中,應用首先檢查是否有兩個或以上的醫生正在值班;如果是的話,它就假定一名醫生可以安全地休班。由於資料庫使用快照隔離,兩次檢查都返回 2 ,所以兩個事務都進入下一個階段。Alice 更新自己的記錄休班了,而 Bob 也做了一樣的事情。兩個事務都成功提交了,現在沒有醫生值班了。違反了至少有一名醫生在值班的要求。
 
 #### 寫偏差的特徵
 
-這種異常稱為**寫偏差**【28】。它既不是**髒寫**,也不是**丟失更新**,因為這兩個事務正在更新兩個不同的物件(Alice和Bob各自的待命記錄)。在這裡發生的衝突並不是那麼明顯,但是這顯然是一個競爭條件:如果兩個事務一個接一個地執行,那麼第二個醫生就不能歇班了。異常行為只有在事務併發進行時才有可能。
+這種異常稱為 **寫偏差**【28】。它既不是 **髒寫**,也不是 **丟失更新**,因為這兩個事務正在更新兩個不同的物件(Alice 和 Bob 各自的待命記錄)。在這裡發生的衝突並不是那麼明顯,但是這顯然是一個競爭條件:如果兩個事務一個接一個地執行,那麼第二個醫生就不能歇班了。異常行為只有在事務併發進行時才有可能。
 
 可以將寫入偏差視為丟失更新問題的一般化。如果兩個事務讀取相同的物件,然後更新其中一些物件(不同的事務可能更新不同的物件),則可能發生寫入偏差。在多個事務更新同一個物件的特殊情況下,就會發生髒寫或丟失更新(取決於時序)。
 
 我們已經看到,有各種不同的方法來防止丟失的更新。但對於寫偏差,我們的選擇更受限制:
 
 * 由於涉及多個物件,單物件的原子操作不起作用。
-* 不幸的是,在一些快照隔離的實現中,自動檢測丟失更新對此並沒有幫助。在PostgreSQL的可重複讀,MySQL/InnoDB的可重複讀,Oracle可序列化或SQL Server的快照隔離級別中,都不會自動檢測寫入偏差【23】。自動防止寫入偏差需要真正的可序列化隔離(請參閱“[可序列化](#可序列化)”)。
+* 不幸的是,在一些快照隔離的實現中,自動檢測丟失更新對此並沒有幫助。在 PostgreSQL 的可重複讀,MySQL/InnoDB 的可重複讀,Oracle 可序列化或 SQL Server 的快照隔離級別中,都不會自動檢測寫入偏差【23】。自動防止寫入偏差需要真正的可序列化隔離(請參閱 “[可序列化](#可序列化)”)。
 * 某些資料庫允許配置約束,然後由資料庫強制執行(例如,唯一性,外來鍵約束或特定值限制)。但是為了指定至少有一名醫生必須線上,需要一個涉及多個物件的約束。大多數資料庫沒有內建對這種約束的支援,但是你可以使用觸發器,或者物化檢視來實現它們,這取決於不同的資料庫【42】。
 * 如果無法使用可序列化的隔離級別,則此情況下的次優選項可能是顯式鎖定事務所依賴的行。在例子中,你可以寫下如下的程式碼:
 
@@ -505,7 +505,7 @@ UPDATE doctors
 COMMIT;
 ```
 
-* 和以前一樣,`FOR UPDATE`告訴資料庫鎖定返回的所有行以用於更新。
+* 和以前一樣,`FOR UPDATE` 告訴資料庫鎖定返回的所有行以用於更新。
 
 #### 寫偏差的更多例子
 
@@ -513,21 +513,21 @@ COMMIT;
 
 * 會議室預訂系統
 
-  比如你想要規定不能在同一時間對同一個會議室進行多次的預訂【43】。當有人想要預訂時,首先檢查是否存在相互衝突的預訂(即預訂時間範圍重疊的同一房間),如果沒有找到,則建立會議(請參閱示例7-2)[^ix]。
+  比如你想要規定不能在同一時間對同一個會議室進行多次的預訂【43】。當有人想要預訂時,首先檢查是否存在相互衝突的預訂(即預訂時間範圍重疊的同一房間),如果沒有找到,則建立會議(請參閱示例 7-2)[^ix]。
 
-  [^ix]: 在PostgreSQL中,你可以使用範圍型別優雅地執行此操作,但在其他資料庫中並未得到廣泛支援。
+  [^ix]: 在 PostgreSQL 中,你可以使用範圍型別優雅地執行此操作,但在其他資料庫中並未得到廣泛支援。
 
-  **例7-2 會議室預訂系統試圖避免重複預訂(在快照隔離下不安全)**
+  **例 7-2 會議室預訂系統試圖避免重複預訂(在快照隔離下不安全)**
 
   ```sql
   BEGIN TRANSACTION;
 
-  -- 檢查所有現存的與12:00~13:00重疊的預定
+  -- 檢查所有現存的與 12:00~13:00 重疊的預定
   SELECT COUNT(*) FROM bookings
   WHERE room_id = 123 AND
     end_time > '2015-01-01 12:00' AND start_time < '2015-01-01 13:00';
 
-  -- 如果之前的查詢返回0
+  -- 如果之前的查詢返回 0
   INSERT INTO bookings(room_id, start_time, end_time, user_id)
     VALUES (123, '2015-01-01 12:00', '2015-01-01 13:00', 666);
 
@@ -538,7 +538,7 @@ COMMIT;
 
 * 多人遊戲
 
-  在[例7-1]()中,我們使用一個鎖來防止丟失更新(也就是確保兩個玩家不能同時移動同一個棋子)。但是鎖定並不妨礙玩家將兩個不同的棋子移動到棋盤上的相同位置,或者採取其他違反遊戲規則的行為。按照你正在執行的規則型別,也許可以使用唯一約束(unique constraint),否則你很容易發生寫入偏差。
+  在 [例 7-1]() 中,我們使用一個鎖來防止丟失更新(也就是確保兩個玩家不能同時移動同一個棋子)。但是鎖定並不妨礙玩家將兩個不同的棋子移動到棋盤上的相同位置,或者採取其他違反遊戲規則的行為。按照你正在執行的規則型別,也許可以使用唯一約束(unique constraint),否則你很容易發生寫入偏差。
 
 * 搶注使用者名稱
 
@@ -552,168 +552,168 @@ COMMIT;
 
 所有這些例子都遵循類似的模式:
 
-1. 一個`SELECT`查詢找出符合條件的行,並檢查是否符合一些要求。(例如:至少有兩名醫生在值班;不存在對該會議室同一時段的預定;棋盤上的位置沒有被其他棋子佔據;使用者名稱還沒有被搶注;賬戶裡還有足夠餘額)
+1. 一個 `SELECT` 查詢找出符合條件的行,並檢查是否符合一些要求。(例如:至少有兩名醫生在值班;不存在對該會議室同一時段的預定;棋盤上的位置沒有被其他棋子佔據;使用者名稱還沒有被搶注;賬戶裡還有足夠餘額)
 
 2. 按照第一個查詢的結果,應用程式碼決定是否繼續。(可能會繼續操作,也可能中止並報錯)
 
 3. 如果應用決定繼續操作,就執行寫入(插入、更新或刪除),並提交事務。
 
-   這個寫入的效果改變了步驟2 中的先決條件。換句話說,如果在提交寫入後,重複執行一次步驟1 的SELECT查詢,將會得到不同的結果。因為寫入改變了符合搜尋條件的行集(現在少了一個醫生值班,那時候的會議室現在已經被預訂了,棋盤上的這個位置已經被佔據了,使用者名稱已經被搶注,賬戶餘額不夠了)。
+   這個寫入的效果改變了步驟 2 中的先決條件。換句話說,如果在提交寫入後,重複執行一次步驟 1 的 SELECT 查詢,將會得到不同的結果。因為寫入改變了符合搜尋條件的行集(現在少了一個醫生值班,那時候的會議室現在已經被預訂了,棋盤上的這個位置已經被佔據了,使用者名稱已經被搶注,賬戶餘額不夠了)。
 
-這些步驟可能以不同的順序發生。例如可以首先進行寫入,然後進行SELECT查詢,最後根據查詢結果決定是放棄還是提交。
+這些步驟可能以不同的順序發生。例如可以首先進行寫入,然後進行 SELECT 查詢,最後根據查詢結果決定是放棄還是提交。
 
-在醫生值班的例子中,在步驟3中修改的行,是步驟1中返回的行之一,所以我們可以透過鎖定步驟1 中的行(`SELECT FOR UPDATE`)來使事務安全並避免寫入偏差。但是其他四個例子是不同的:它們檢查是否**不存在**某些滿足條件的行,寫入會**新增**一個匹配相同條件的行。如果步驟1中的查詢沒有返回任何行,則`SELECT FOR UPDATE`鎖不了任何東西。
+在醫生值班的例子中,在步驟 3 中修改的行,是步驟 1 中返回的行之一,所以我們可以透過鎖定步驟 1 中的行(`SELECT FOR UPDATE`)來使事務安全並避免寫入偏差。但是其他四個例子是不同的:它們檢查是否 **不存在** 某些滿足條件的行,寫入會 **新增** 一個匹配相同條件的行。如果步驟 1 中的查詢沒有返回任何行,則 `SELECT FOR UPDATE` 鎖不了任何東西。
 
-這種效應:一個事務中的寫入改變另一個事務的搜尋查詢的結果,被稱為**幻讀**【3】。快照隔離避免了只讀查詢中幻讀,但是在像我們討論的例子那樣的讀寫事務中,幻讀會導致特別棘手的寫入偏差情況。
+這種效應:一個事務中的寫入改變另一個事務的搜尋查詢的結果,被稱為 **幻讀**【3】。快照隔離避免了只讀查詢中幻讀,但是在像我們討論的例子那樣的讀寫事務中,幻讀會導致特別棘手的寫入偏差情況。
 
 #### 物化衝突
 
 如果幻讀的問題是沒有物件可以加鎖,也許可以人為地在資料庫中引入一個鎖物件?
 
-例如,在會議室預訂的場景中,可以想象建立一個關於時間槽和房間的表。此表中的每一行對應於特定時間段(例如15分鐘)的特定房間。可以提前插入房間和時間的所有可能組合行(例如接下來的六個月)。
+例如,在會議室預訂的場景中,可以想象建立一個關於時間槽和房間的表。此表中的每一行對應於特定時間段(例如 15 分鐘)的特定房間。可以提前插入房間和時間的所有可能組合行(例如接下來的六個月)。
 
-現在,要建立預訂的事務可以鎖定(`SELECT FOR UPDATE`)表中與所需房間和時間段對應的行。在獲得鎖定之後,它可以檢查重疊的預訂並像以前一樣插入新的預訂。請注意,這個表並不是用來儲存預訂相關的資訊——它完全就是一組鎖,用於防止同時修改同一房間和時間範圍內的預訂。
+現在,要建立預訂的事務可以鎖定(`SELECT FOR UPDATE`)表中與所需房間和時間段對應的行。在獲得鎖定之後,它可以檢查重疊的預訂並像以前一樣插入新的預訂。請注意,這個表並不是用來儲存預訂相關的資訊 —— 它完全就是一組鎖,用於防止同時修改同一房間和時間範圍內的預訂。
 
-這種方法被稱為**物化衝突(materializing conflicts)**,因為它將幻讀變為資料庫中一組具體行上的鎖衝突【11】。不幸的是,弄清楚如何物化衝突可能很難,也很容易出錯,而讓併發控制機制洩漏到應用資料模型是很醜陋的做法。出於這些原因,如果沒有其他辦法可以實現,物化衝突應被視為最後的手段。在大多數情況下。**可序列化(Serializable)** 的隔離級別是更可取的。
+這種方法被稱為 **物化衝突(materializing conflicts)**,因為它將幻讀變為資料庫中一組具體行上的鎖衝突【11】。不幸的是,弄清楚如何物化衝突可能很難,也很容易出錯,而讓併發控制機制洩漏到應用資料模型是很醜陋的做法。出於這些原因,如果沒有其他辦法可以實現,物化衝突應被視為最後的手段。在大多數情況下。**可序列化(Serializable)** 的隔離級別是更可取的。
 
 
 ## 可序列化
 
-在本章中,已經看到了幾個易於出現競爭條件的事務例子。**讀已提交**和**快照隔離**級別會阻止某些競爭條件,但不會阻止另一些。我們遇到了一些特別棘手的例子,**寫入偏差**和**幻讀**。這是一個可悲的情況:
+在本章中,已經看到了幾個易於出現競爭條件的事務例子。**讀已提交** 和 **快照隔離** 級別會阻止某些競爭條件,但不會阻止另一些。我們遇到了一些特別棘手的例子,**寫入偏差** 和 **幻讀**。這是一個可悲的情況:
 
-- 隔離級別難以理解,並且在不同的資料庫中實現的不一致(例如,“可重複讀”的含義天差地別)。
+- 隔離級別難以理解,並且在不同的資料庫中實現的不一致(例如,“可重複讀” 的含義天差地別)。
 - 光檢查應用程式碼很難判斷在特定的隔離級別執行是否安全。 特別是在大型應用程式中,你可能並不知道併發發生的所有事情。
 - 沒有檢測競爭條件的好工具。原則上來說,靜態分析可能會有幫助【26】,但研究中的技術還沒法實際應用。併發問題的測試是很難的,因為它們通常是非確定性的 —— 只有在倒黴的時序下才會出現問題。
 
-這不是一個新問題,從20世紀70年代以來就一直是這樣了,當時首先引入了較弱的隔離級別【2】。一直以來,研究人員的答案都很簡單:使用**可序列化(serializable)** 的隔離級別!
+這不是一個新問題,從 20 世紀 70 年代以來就一直是這樣了,當時首先引入了較弱的隔離級別【2】。一直以來,研究人員的答案都很簡單:使用 **可序列化(serializable)** 的隔離級別!
 
-**可序列化(Serializability)** 隔離通常被認為是最強的隔離級別。它保證即使事務可以並行執行,最終的結果也是一樣的,就好像它們沒有任何併發性,連續挨個執行一樣。因此資料庫保證,如果事務在單獨執行時正常執行,則它們在併發執行時繼續保持正確 —— 換句話說,資料庫可以防止**所有**可能的競爭條件。
+**可序列化(Serializability)** 隔離通常被認為是最強的隔離級別。它保證即使事務可以並行執行,最終的結果也是一樣的,就好像它們沒有任何併發性,連續挨個執行一樣。因此資料庫保證,如果事務在單獨執行時正常執行,則它們在併發執行時繼續保持正確 —— 換句話說,資料庫可以防止 **所有** 可能的競爭條件。
 
 但如果可序列化隔離級別比弱隔離級別的爛攤子要好得多,那為什麼沒有人見人愛?為了回答這個問題,我們需要看看實現可序列化的選項,以及它們如何執行。目前大多數提供可序列化的資料庫都使用了三種技術之一,本章的剩餘部分將會介紹這些技術:
 
-- 字面意義上地序列順序執行事務(請參閱“[真的序列執行](#真的序列執行)”)
-- **兩階段鎖定(2PL, two-phase locking)**,幾十年來唯一可行的選擇(請參閱“[兩階段鎖定](#兩階段鎖定)”)
-- 樂觀併發控制技術,例如**可序列化快照隔離**(serializable snapshot isolation,請參閱“[可序列化快照隔離](#可序列化快照隔離)”)
+- 字面意義上地序列順序執行事務(請參閱 “[真的序列執行](#真的序列執行)”)
+- **兩階段鎖定(2PL, two-phase locking)**,幾十年來唯一可行的選擇(請參閱 “[兩階段鎖定](#兩階段鎖定)”)
+- 樂觀併發控制技術,例如 **可序列化快照隔離**(serializable snapshot isolation,請參閱 “[可序列化快照隔離](#可序列化快照隔離)”)
 
-現在將主要在單節點資料庫的背景下討論這些技術;在[第九章](ch9.md)中,我們將研究如何將它們推廣到涉及分散式系統中多個節點的事務。
+現在將主要在單節點資料庫的背景下討論這些技術;在 [第九章](ch9.md) 中,我們將研究如何將它們推廣到涉及分散式系統中多個節點的事務。
 
 ### 真的序列執行
 
-避免併發問題的最簡單方法就是完全不要併發:在單個執行緒上按順序一次只執行一個事務。這樣做就完全繞開了檢測/防止事務間衝突的問題,由此產生的隔離,正是可序列化的定義。
+避免併發問題的最簡單方法就是完全不要併發:在單個執行緒上按順序一次只執行一個事務。這樣做就完全繞開了檢測 / 防止事務間衝突的問題,由此產生的隔離,正是可序列化的定義。
 
-儘管這似乎是一個明顯的主意,但資料庫設計人員只是在2007年左右才決定,單執行緒迴圈執行事務是可行的【45】。如果多執行緒併發在過去的30年中被認為是獲得良好效能的關鍵所在,那麼究竟是什麼改變致使單執行緒執行變為可能呢?
+儘管這似乎是一個明顯的主意,但資料庫設計人員只是在 2007 年左右才決定,單執行緒迴圈執行事務是可行的【45】。如果多執行緒併發在過去的 30 年中被認為是獲得良好效能的關鍵所在,那麼究竟是什麼改變致使單執行緒執行變為可能呢?
 
 兩個進展引發了這個反思:
 
-- RAM足夠便宜了,許多場景現在都可以將完整的活躍資料集儲存在記憶體中。(請參閱“[在記憶體中儲存一切](ch3.md#在記憶體中儲存一切)”)。當事務需要訪問的所有資料都在記憶體中時,事務處理的執行速度要比等待資料從磁碟載入時快得多。
-- 資料庫設計人員意識到OLTP事務通常很短,而且只進行少量的讀寫操作(請參閱“[事務處理還是分析?](ch3.md#事務處理還是分析?)”)。相比之下,長時間執行的分析查詢通常是隻讀的,因此它們可以在序列執行迴圈之外的一致快照(使用快照隔離)上執行。
+- RAM 足夠便宜了,許多場景現在都可以將完整的活躍資料集儲存在記憶體中。(請參閱 “[在記憶體中儲存一切](ch3.md#在記憶體中儲存一切)”)。當事務需要訪問的所有資料都在記憶體中時,事務處理的執行速度要比等待資料從磁碟載入時快得多。
+- 資料庫設計人員意識到 OLTP 事務通常很短,而且只進行少量的讀寫操作(請參閱 “[事務處理還是分析?](ch3.md#事務處理還是分析?)”)。相比之下,長時間執行的分析查詢通常是隻讀的,因此它們可以在序列執行迴圈之外的一致快照(使用快照隔離)上執行。
 
-序列執行事務的方法在VoltDB/H-Store,Redis和Datomic中實現【46,47,48】。設計用於單執行緒執行的系統有時可以比支援併發的系統更好,因為它可以避免鎖的協調開銷。但是其吞吐量僅限於單個CPU核的吞吐量。為了充分利用單一執行緒,需要與傳統形式的事務不同的結構。
+序列執行事務的方法在 VoltDB/H-Store,Redis 和 Datomic 中實現【46,47,48】。設計用於單執行緒執行的系統有時可以比支援併發的系統更好,因為它可以避免鎖的協調開銷。但是其吞吐量僅限於單個 CPU 核的吞吐量。為了充分利用單一執行緒,需要與傳統形式的事務不同的結構。
 
 #### 在儲存過程中封裝事務
 
 在資料庫的早期階段,意圖是資料庫事務可以包含整個使用者活動流程。例如,預訂機票是一個多階段的過程(搜尋路線,票價和可用座位,決定行程,在每段行程的航班上訂座,輸入乘客資訊,付款)。資料庫設計者認為,如果整個過程是一個事務,那麼它就可以被原子化地執行。
 
-不幸的是,人類做出決定和迴應的速度非常緩慢。如果資料庫事務需要等待來自使用者的輸入,則資料庫需要支援潛在的大量併發事務,其中大部分是空閒的。大多數資料庫不能高效完成這項工作,因此幾乎所有的OLTP應用程式都避免在事務中等待互動式的使用者輸入,以此來保持事務的簡短。在Web上,這意味著事務在同一個HTTP請求中被提交——一個事務不會跨越多個請求。一個新的HTTP請求開始一個新的事務。
+不幸的是,人類做出決定和迴應的速度非常緩慢。如果資料庫事務需要等待來自使用者的輸入,則資料庫需要支援潛在的大量併發事務,其中大部分是空閒的。大多數資料庫不能高效完成這項工作,因此幾乎所有的 OLTP 應用程式都避免在事務中等待互動式的使用者輸入,以此來保持事務的簡短。在 Web 上,這意味著事務在同一個 HTTP 請求中被提交 —— 一個事務不會跨越多個請求。一個新的 HTTP 請求開始一個新的事務。
 
-即使已經將人類從關鍵路徑中排除,事務仍然以互動式的客戶端/伺服器風格執行,一次一個語句。應用程式進行查詢,讀取結果,可能根據第一個查詢的結果進行另一個查詢,依此類推。查詢和結果在應用程式程式碼(在一臺機器上執行)和資料庫伺服器(在另一臺機器上)之間來回傳送。
+即使已經將人類從關鍵路徑中排除,事務仍然以互動式的客戶端 / 伺服器風格執行,一次一個語句。應用程式進行查詢,讀取結果,可能根據第一個查詢的結果進行另一個查詢,依此類推。查詢和結果在應用程式程式碼(在一臺機器上執行)和資料庫伺服器(在另一臺機器上)之間來回傳送。
 
 在這種互動式的事務方式中,應用程式和資料庫之間的網路通訊耗費了大量的時間。如果不允許在資料庫中進行併發處理,且一次只處理一個事務,則吞吐量將會非常糟糕,因為資料庫大部分的時間都花費在等待應用程式發出當前事務的下一個查詢。在這種資料庫中,為了獲得合理的效能,需要同時處理多個事務。
 
-出於這個原因,具有單執行緒序列事務處理的系統不允許互動式的多語句事務。取而代之,應用程式必須提前將整個事務程式碼作為儲存過程提交給資料庫。這些方法之間的差異如[圖7-9](../img/fig7-9.png) 所示。如果事務所需的所有資料都在記憶體中,則儲存過程可以非常快地執行,而不用等待任何網路或磁碟I/O。
+出於這個原因,具有單執行緒序列事務處理的系統不允許互動式的多語句事務。取而代之,應用程式必須提前將整個事務程式碼作為儲存過程提交給資料庫。這些方法之間的差異如 [圖 7-9](../img/fig7-9.png) 所示。如果事務所需的所有資料都在記憶體中,則儲存過程可以非常快地執行,而不用等待任何網路或磁碟 I/O。
 
 ![](../img/fig7-9.png)
 
-**圖7-9 互動式事務和儲存過程之間的區別(使用圖7-8的示例事務)**
+**圖 7-9 互動式事務和儲存過程之間的區別(使用圖 7-8 的示例事務)**
 
 #### 儲存過程的優點和缺點
 
-儲存過程在關係型資料庫中已經存在了一段時間了,自1999年以來它們一直是SQL標準(SQL/PSM)的一部分。出於各種原因,它們的名聲有點不太好:
+儲存過程在關係型資料庫中已經存在了一段時間了,自 1999 年以來它們一直是 SQL 標準(SQL/PSM)的一部分。出於各種原因,它們的名聲有點不太好:
 
-- 每個資料庫廠商都有自己的儲存過程語言(Oracle有PL/SQL,SQL Server有T-SQL,PostgreSQL有PL/pgSQL等)。這些語言並沒有跟上通用程式語言的發展,所以從今天的角度來看,它們看起來相當醜陋和陳舊,而且缺乏大多數程式語言中能找到的庫的生態系統。
+- 每個資料庫廠商都有自己的儲存過程語言(Oracle 有 PL/SQL,SQL Server 有 T-SQL,PostgreSQL 有 PL/pgSQL 等)。這些語言並沒有跟上通用程式語言的發展,所以從今天的角度來看,它們看起來相當醜陋和陳舊,而且缺乏大多數程式語言中能找到的庫的生態系統。
 - 在資料庫中執行的程式碼難以管理:與應用伺服器相比,它更難除錯,更難以保持版本控制和部署,更難測試,並且難以整合到指標收集系統來進行監控。
-- 資料庫通常比應用伺服器對效能敏感的多,因為單個數據庫例項通常由許多應用伺服器共享。資料庫中一個寫得不好的儲存過程(例如,佔用大量記憶體或CPU時間)會比在應用伺服器中相同的程式碼造成更多的麻煩。
+- 資料庫通常比應用伺服器對效能敏感的多,因為單個數據庫例項通常由許多應用伺服器共享。資料庫中一個寫得不好的儲存過程(例如,佔用大量記憶體或 CPU 時間)會比在應用伺服器中相同的程式碼造成更多的麻煩。
 
-但是這些問題都是可以克服的。現代的儲存過程實現放棄了PL/SQL,而是使用現有的通用程式語言:VoltDB使用Java或Groovy,Datomic使用Java或Clojure,而Redis使用Lua。
+但是這些問題都是可以克服的。現代的儲存過程實現放棄了 PL/SQL,而是使用現有的通用程式語言:VoltDB 使用 Java 或 Groovy,Datomic 使用 Java 或 Clojure,而 Redis 使用 Lua。
 
-**儲存過程與記憶體儲存**,使得在單個執行緒上執行所有事務變得可行。由於不需要等待I/O,且避免了併發控制機制的開銷,它們可以在單個執行緒上實現相當好的吞吐量。
+**儲存過程與記憶體儲存**,使得在單個執行緒上執行所有事務變得可行。由於不需要等待 I/O,且避免了併發控制機制的開銷,它們可以在單個執行緒上實現相當好的吞吐量。
 
-VoltDB還使用儲存過程進行復制:但不是將事務的寫入結果從一個節點複製到另一個節點,而是在每個節點上執行相同的儲存過程。因此VoltDB要求儲存過程是**確定性的**(在不同的節點上執行時,它們必須產生相同的結果)。舉個例子,如果事務需要使用當前的日期和時間,則必須透過特殊的確定性API來實現。
+VoltDB 還使用儲存過程進行復制:但不是將事務的寫入結果從一個節點複製到另一個節點,而是在每個節點上執行相同的儲存過程。因此 VoltDB 要求儲存過程是 **確定性的**(在不同的節點上執行時,它們必須產生相同的結果)。舉個例子,如果事務需要使用當前的日期和時間,則必須透過特殊的確定性 API 來實現。
 
 #### 分割槽
 
 順序執行所有事務使併發控制簡單多了,但資料庫的事務吞吐量被限制為單機單核的速度。只讀事務可以使用快照隔離在其它地方執行,但對於寫入吞吐量較高的應用,單執行緒事務處理器可能成為一個嚴重的瓶頸。
 
-為了伸縮至多個CPU核心和多個節點,可以對資料進行分割槽(請參閱[第六章](ch6.md)),在VoltDB中支援這樣做。如果你可以找到一種對資料集進行分割槽的方法,以便每個事務只需要在單個分割槽中讀寫資料,那麼每個分割槽就可以擁有自己獨立執行的事務處理執行緒。在這種情況下可以為每個分割槽指派一個獨立的CPU核,事務吞吐量就可以與CPU核數保持線性伸縮【47】。
+為了伸縮至多個 CPU 核心和多個節點,可以對資料進行分割槽(請參閱 [第六章](ch6.md)),在 VoltDB 中支援這樣做。如果你可以找到一種對資料集進行分割槽的方法,以便每個事務只需要在單個分割槽中讀寫資料,那麼每個分割槽就可以擁有自己獨立執行的事務處理執行緒。在這種情況下可以為每個分割槽指派一個獨立的 CPU 核,事務吞吐量就可以與 CPU 核數保持線性伸縮【47】。
 
 但是,對於需要訪問多個分割槽的任何事務,資料庫必須在觸及的所有分割槽之間協調事務。儲存過程需要跨越所有分割槽鎖定執行,以確保整個系統的可序列性。
 
-由於跨分割槽事務具有額外的協調開銷,所以它們比單分割槽事務慢得多。 VoltDB報告的吞吐量大約是每秒1000個跨分割槽寫入,比單分割槽吞吐量低幾個數量級,並且不能透過增加更多的機器來增加【49】。
+由於跨分割槽事務具有額外的協調開銷,所以它們比單分割槽事務慢得多。 VoltDB 報告的吞吐量大約是每秒 1000 個跨分割槽寫入,比單分割槽吞吐量低幾個數量級,並且不能透過增加更多的機器來增加【49】。
 
-事務是否可以是劃分至單個分割槽很大程度上取決於應用資料的結構。簡單的鍵值資料通常可以非常容易地進行分割槽,但是具有多個次級索引的資料可能需要大量的跨分割槽協調(請參閱“[分割槽與次級索引](ch6.md#分割槽與次級索引)”)。
+事務是否可以是劃分至單個分割槽很大程度上取決於應用資料的結構。簡單的鍵值資料通常可以非常容易地進行分割槽,但是具有多個次級索引的資料可能需要大量的跨分割槽協調(請參閱 “[分割槽與次級索引](ch6.md#分割槽與次級索引)”)。
 
 #### 序列執行小結
 
 在特定約束條件下,真的序列執行事務,已經成為一種實現可序列化隔離等級的可行辦法。
 
 - 每個事務都必須小而快,只要有一個緩慢的事務,就會拖慢所有事務處理。
-- 僅限於活躍資料集可以放入記憶體的情況。很少訪問的資料可能會被移動到磁碟,但如果需要在單執行緒執行的事務中訪問,系統就會變得非常慢[^x]。
-- 寫入吞吐量必須低到能在單個CPU核上處理,如若不然,事務需要能劃分至單個分割槽,且不需要跨分割槽協調。
+- 僅限於活躍資料集可以放入記憶體的情況。很少訪問的資料可能會被移動到磁碟,但如果需要在單執行緒執行的事務中訪問,系統就會變得非常慢 [^x]。
+- 寫入吞吐量必須低到能在單個 CPU 核上處理,如若不然,事務需要能劃分至單個分割槽,且不需要跨分割槽協調。
 - 跨分割槽事務是可能的,但是它們能被使用的程度有很大的限制。
 
-[^x]: 如果事務需要訪問不在記憶體中的資料,最好的解決方案可能是中止事務,非同步地將資料提取到記憶體中,同時繼續處理其他事務,然後在資料載入完畢時重新啟動事務。這種方法被稱為**反快取(anti-caching)**,正如前面在“[在記憶體中儲存一切](ch3.md#在記憶體中儲存一切)”中所述。
+[^x]: 如果事務需要訪問不在記憶體中的資料,最好的解決方案可能是中止事務,非同步地將資料提取到記憶體中,同時繼續處理其他事務,然後在資料載入完畢時重新啟動事務。這種方法被稱為 **反快取(anti-caching)**,正如前面在 “[在記憶體中儲存一切](ch3.md#在記憶體中儲存一切)” 中所述。
 
 ### 兩階段鎖定
 
-大約30年來,在資料庫中只有一種廣泛使用的序列化演算法:**兩階段鎖定(2PL,two-phase locking)** [^xi]
+大約 30 年來,在資料庫中只有一種廣泛使用的序列化演算法:**兩階段鎖定(2PL,two-phase locking)** [^xi]
 
-[^xi]: 有時也稱為**嚴格兩階段鎖定(SS2PL, strong strict two-phase locking)**,以便和其他2PL變體區分。
+[^xi]: 有時也稱為 **嚴格兩階段鎖定(SS2PL, strong strict two-phase locking)**,以便和其他 2PL 變體區分。
 
 > #### 2PL不是2PC
 >
-> 請注意,雖然兩階段鎖定(2PL)聽起來非常類似於兩階段提交(2PC),但它們是完全不同的東西。我們將在[第九章](ch9.md)討論2PC。
+> 請注意,雖然兩階段鎖定(2PL)聽起來非常類似於兩階段提交(2PC),但它們是完全不同的東西。我們將在 [第九章](ch9.md) 討論 2PC。
 
-之前我們看到鎖通常用於防止髒寫(請參閱“[沒有髒寫](#沒有髒寫)”一節):如果兩個事務同時嘗試寫入同一個物件,則鎖可確保第二個寫入必須等到第一個寫入完成事務(中止或提交),然後才能繼續。
+之前我們看到鎖通常用於防止髒寫(請參閱 “[沒有髒寫](#沒有髒寫)” 一節):如果兩個事務同時嘗試寫入同一個物件,則鎖可確保第二個寫入必須等到第一個寫入完成事務(中止或提交),然後才能繼續。
 
-兩階段鎖定類似,但是鎖的要求更強得多。只要沒有寫入,就允許多個事務同時讀取同一個物件。但物件只要有寫入(修改或刪除),就需要**獨佔訪問(exclusive access)** 許可權:
+兩階段鎖定類似,但是鎖的要求更強得多。只要沒有寫入,就允許多個事務同時讀取同一個物件。但物件只要有寫入(修改或刪除),就需要 **獨佔訪問(exclusive access)** 許可權:
 
-- 如果事務A讀取了一個物件,並且事務B想要寫入該物件,那麼B必須等到A提交或中止才能繼續(這確保B不能在A底下意外地改變物件)。
-- 如果事務A寫入了一個物件,並且事務B想要讀取該物件,則B必須等到A提交或中止才能繼續(像[圖7-1](../img/fig7-1.png)那樣讀取舊版本的物件在2PL下是不可接受的)。
+- 如果事務 A 讀取了一個物件,並且事務 B 想要寫入該物件,那麼 B 必須等到 A 提交或中止才能繼續(這確保 B 不能在 A 底下意外地改變物件)。
+- 如果事務 A 寫入了一個物件,並且事務 B 想要讀取該物件,則 B 必須等到 A 提交或中止才能繼續(像 [圖 7-1](../img/fig7-1.png) 那樣讀取舊版本的物件在 2PL 下是不可接受的)。
 
-在2PL中,寫入不僅會阻塞其他寫入,也會阻塞讀,反之亦然。快照隔離使得**讀不阻塞寫,寫也不阻塞讀**(請參閱“[實現快照隔離](#實現快照隔離)”),這是2PL和快照隔離之間的關鍵區別。另一方面,因為2PL提供了可序列化的性質,它可以防止早先討論的所有競爭條件,包括丟失更新和寫入偏差。
+在 2PL 中,寫入不僅會阻塞其他寫入,也會阻塞讀,反之亦然。快照隔離使得 **讀不阻塞寫,寫也不阻塞讀**(請參閱 “[實現快照隔離](#實現快照隔離)”),這是 2PL 和快照隔離之間的關鍵區別。另一方面,因為 2PL 提供了可序列化的性質,它可以防止早先討論的所有競爭條件,包括丟失更新和寫入偏差。
 
 #### 實現兩階段鎖
 
-2PL用於MySQL(InnoDB)和SQL Server中的可序列化隔離級別,以及DB2中的可重複讀隔離級別【23,36】。
+2PL 用於 MySQL(InnoDB)和 SQL Server 中的可序列化隔離級別,以及 DB2 中的可重複讀隔離級別【23,36】。
 
-讀與寫的阻塞是透過為資料庫中每個物件新增鎖來實現的。鎖可以處於**共享模式(shared mode)** 或**獨佔模式(exclusive mode)**。鎖使用如下:
+讀與寫的阻塞是透過為資料庫中每個物件新增鎖來實現的。鎖可以處於 **共享模式(shared mode)** 或 **獨佔模式(exclusive mode)**。鎖使用如下:
 
 - 若事務要讀取物件,則須先以共享模式獲取鎖。允許多個事務同時持有共享鎖。但如果另一個事務已經在物件上持有排它鎖,則這些事務必須等待。
 - 若事務要寫入一個物件,它必須首先以獨佔模式獲取該鎖。沒有其他事務可以同時持有鎖(無論是共享模式還是獨佔模式),所以如果物件上存在任何鎖,該事務必須等待。
 - 如果事務先讀取再寫入物件,則它可能會將其共享鎖升級為獨佔鎖。升級鎖的工作與直接獲得排他鎖相同。
-- 事務獲得鎖之後,必須繼續持有鎖直到事務結束(提交或中止)。這就是“兩階段”這個名字的來源:第一階段(當事務正在執行時)獲取鎖,第二階段(在事務結束時)釋放所有的鎖。
+- 事務獲得鎖之後,必須繼續持有鎖直到事務結束(提交或中止)。這就是 “兩階段” 這個名字的來源:第一階段(當事務正在執行時)獲取鎖,第二階段(在事務結束時)釋放所有的鎖。
 
-由於使用了這麼多的鎖,因此很可能會發生:事務A等待事務B釋放它的鎖,反之亦然。這種情況叫做**死鎖(Deadlock)**。資料庫會自動檢測事務之間的死鎖,並中止其中一個,以便另一個繼續執行。被中止的事務需要由應用程式重試。
+由於使用了這麼多的鎖,因此很可能會發生:事務 A 等待事務 B 釋放它的鎖,反之亦然。這種情況叫做 **死鎖(Deadlock)**。資料庫會自動檢測事務之間的死鎖,並中止其中一個,以便另一個繼續執行。被中止的事務需要由應用程式重試。
 
 #### 兩階段鎖定的效能
 
-兩階段鎖定的巨大缺點,以及70年代以來沒有被所有人使用的原因,是其效能問題。兩階段鎖定下的事務吞吐量與查詢響應時間要比弱隔離級別下要差得多。
+兩階段鎖定的巨大缺點,以及 70 年代以來沒有被所有人使用的原因,是其效能問題。兩階段鎖定下的事務吞吐量與查詢響應時間要比弱隔離級別下要差得多。
 
 這一部分是由於獲取和釋放所有這些鎖的開銷,但更重要的是由於併發性的降低。按照設計,如果兩個併發事務試圖做任何可能導致競爭條件的事情,那麼必須等待另一個完成。
 
 傳統的關係資料庫不限制事務的持續時間,因為它們是為等待人類輸入的互動式應用而設計的。因此,當一個事務需要等待另一個事務時,等待的時長並沒有限制。即使你保證所有的事務都很短,如果有多個事務想要訪問同一個物件,那麼可能會形成一個佇列,所以事務可能需要等待幾個其他事務才能完成。
 
-因此,執行2PL的資料庫可能具有相當不穩定的延遲,如果在工作負載中存在爭用,那麼可能高百分位點處的響應會非常的慢(請參閱“[描述效能](ch1.md#描述效能)”)。可能只需要一個緩慢的事務,或者一個訪問大量資料並獲取許多鎖的事務,就能把系統的其他部分拖慢,甚至迫使系統停機。當需要穩健的操作時,這種不穩定性是有問題的。
+因此,執行 2PL 的資料庫可能具有相當不穩定的延遲,如果在工作負載中存在爭用,那麼可能高百分位點處的響應會非常的慢(請參閱 “[描述效能](ch1.md#描述效能)”)。可能只需要一個緩慢的事務,或者一個訪問大量資料並獲取許多鎖的事務,就能把系統的其他部分拖慢,甚至迫使系統停機。當需要穩健的操作時,這種不穩定性是有問題的。
 
-基於鎖實現的讀已提交隔離級別可能發生死鎖,但在基於2PL實現的可序列化隔離級別中,它們會出現的頻繁的多(取決於事務的訪問模式)。這可能是一個額外的效能問題:當事務由於死鎖而被中止並被重試時,它需要從頭重做它的工作。如果死鎖很頻繁,這可能意味著巨大的浪費。
+基於鎖實現的讀已提交隔離級別可能發生死鎖,但在基於 2PL 實現的可序列化隔離級別中,它們會出現的頻繁的多(取決於事務的訪問模式)。這可能是一個額外的效能問題:當事務由於死鎖而被中止並被重試時,它需要從頭重做它的工作。如果死鎖很頻繁,這可能意味著巨大的浪費。
 
 #### 謂詞鎖
 
-在前面關於鎖的描述中,我們掩蓋了一個微妙而重要的細節。在“[導致寫入偏差的幻讀](#導致寫入偏差的幻讀)”中,我們討論了**幻讀(phantoms)** 的問題。即一個事務改變另一個事務的搜尋查詢的結果。具有可序列化隔離級別的資料庫必須防止**幻讀**。
+在前面關於鎖的描述中,我們掩蓋了一個微妙而重要的細節。在 “[導致寫入偏差的幻讀](#導致寫入偏差的幻讀)” 中,我們討論了 **幻讀(phantoms)** 的問題。即一個事務改變另一個事務的搜尋查詢的結果。具有可序列化隔離級別的資料庫必須防止 **幻讀**。
 
-在會議室預訂的例子中,這意味著如果一個事務在某個時間視窗內搜尋了一個房間的現有預訂(見[例7-2]()),則另一個事務不能同時插入或更新同一時間視窗與同一房間的另一個預訂 (可以同時插入其他房間的預訂,或在不影響另一個預定的條件下預定同一房間的其他時間段)。
+在會議室預訂的例子中,這意味著如果一個事務在某個時間視窗內搜尋了一個房間的現有預訂(見 [例 7-2]()),則另一個事務不能同時插入或更新同一時間視窗與同一房間的另一個預訂 (可以同時插入其他房間的預訂,或在不影響另一個預定的條件下預定同一房間的其他時間段)。
 
-如何實現這一點?從概念上講,我們需要一個**謂詞鎖(predicate lock)**【3】。它類似於前面描述的共享/排它鎖,但不屬於特定的物件(例如,表中的一行),它屬於所有符合某些搜尋條件的物件,如:
+如何實現這一點?從概念上講,我們需要一個 **謂詞鎖(predicate lock)**【3】。它類似於前面描述的共享 / 排它鎖,但不屬於特定的物件(例如,表中的一行),它屬於所有符合某些搜尋條件的物件,如:
 
 ```sql
 SELECT * FROM bookings
@@ -724,23 +724,23 @@ WHERE room_id = 123 AND
 
 謂詞鎖限制訪問,如下所示:
 
-- 如果事務A想要讀取匹配某些條件的物件,就像在這個 `SELECT` 查詢中那樣,它必須獲取查詢條件上的**共享謂詞鎖(shared-mode predicate lock)**。如果另一個事務B持有任何滿足這一查詢條件物件的排它鎖,那麼A必須等到B釋放它的鎖之後才允許進行查詢。
-- 如果事務A想要插入,更新或刪除任何物件,則必須首先檢查舊值或新值是否與任何現有的謂詞鎖匹配。如果事務B持有匹配的謂詞鎖,那麼A必須等到B已經提交或中止後才能繼續。
+- 如果事務 A 想要讀取匹配某些條件的物件,就像在這個 `SELECT` 查詢中那樣,它必須獲取查詢條件上的 **共享謂詞鎖(shared-mode predicate lock)**。如果另一個事務 B 持有任何滿足這一查詢條件物件的排它鎖,那麼 A 必須等到 B 釋放它的鎖之後才允許進行查詢。
+- 如果事務 A 想要插入,更新或刪除任何物件,則必須首先檢查舊值或新值是否與任何現有的謂詞鎖匹配。如果事務 B 持有匹配的謂詞鎖,那麼 A 必須等到 B 已經提交或中止後才能繼續。
 
 這裡的關鍵思想是,謂詞鎖甚至適用於資料庫中尚不存在,但將來可能會新增的物件(幻象)。如果兩階段鎖定包含謂詞鎖,則資料庫將阻止所有形式的寫入偏差和其他競爭條件,因此其隔離實現了可序列化。
 
 #### 索引範圍鎖
 
-不幸的是謂詞鎖效能不佳:**如果活躍事務持有很多鎖,檢查匹配的鎖會非常耗時。**因此,大多數使用2PL的資料庫實際上實現了索引範圍鎖(index-range locking,也稱為**next-key locking**),這是一個簡化的近似版謂詞鎖【41,50】。
+不幸的是謂詞鎖效能不佳:**如果活躍事務持有很多鎖,檢查匹配的鎖會非常耗時。** 因此,大多數使用 2PL 的資料庫實際上實現了索引範圍鎖(index-range locking,也稱為 **next-key locking**),這是一個簡化的近似版謂詞鎖【41,50】。
 
-透過使謂詞匹配到一個更大的集合來簡化謂詞鎖是安全的。例如,如果你有在中午和下午1點之間預訂123號房間的謂詞鎖,則鎖定123號房間的所有時間段,或者鎖定12:00~13:00時間段的所有房間(不只是123號房間)是一個安全的近似,因為任何滿足原始謂詞的寫入也一定會滿足這種更鬆散的近似。
+透過使謂詞匹配到一個更大的集合來簡化謂詞鎖是安全的。例如,如果你有在中午和下午 1 點之間預訂 123 號房間的謂詞鎖,則鎖定 123 號房間的所有時間段,或者鎖定 12:00~13:00 時間段的所有房間(不只是 123 號房間)是一個安全的近似,因為任何滿足原始謂詞的寫入也一定會滿足這種更鬆散的近似。
 
-在房間預訂資料庫中,你可能會在`room_id`列上有一個索引,並且/或者在`start_time` 和 `end_time`上有索引(否則前面的查詢在大型資料庫上的速度會非常慢):
+在房間預訂資料庫中,你可能會在 `room_id` 列上有一個索引,並且 / 或者在 `start_time` 和 `end_time` 上有索引(否則前面的查詢在大型資料庫上的速度會非常慢):
 
-- 假設你的索引位於`room_id`上,並且資料庫使用此索引查詢123號房間的現有預訂。現在資料庫可以簡單地將共享鎖附加到這個索引項上,指示事務已搜尋123號房間用於預訂。
-- 或者,如果資料庫使用基於時間的索引來查詢現有預訂,那麼它可以將共享鎖附加到該索引中的一系列值,指示事務已經將12:00~13:00時間段標記為用於預定。
+- 假設你的索引位於 `room_id` 上,並且資料庫使用此索引查詢 123 號房間的現有預訂。現在資料庫可以簡單地將共享鎖附加到這個索引項上,指示事務已搜尋 123 號房間用於預訂。
+- 或者,如果資料庫使用基於時間的索引來查詢現有預訂,那麼它可以將共享鎖附加到該索引中的一系列值,指示事務已經將 12:00~13:00 時間段標記為用於預定。
 
-無論哪種方式,搜尋條件的近似值都附加到其中一個索引上。現在,如果另一個事務想要插入,更新或刪除同一個房間和/或重疊時間段的預訂,則它將不得不更新索引的相同部分。在這樣做的過程中,它會遇到共享鎖,它將被迫等到鎖被釋放。
+無論哪種方式,搜尋條件的近似值都附加到其中一個索引上。現在,如果另一個事務想要插入,更新或刪除同一個房間和 / 或重疊時間段的預訂,則它將不得不更新索引的相同部分。在這樣做的過程中,它會遇到共享鎖,它將被迫等到鎖被釋放。
 
 這種方法能夠有效防止幻讀和寫入偏差。索引範圍鎖並不像謂詞鎖那樣精確(它們可能會鎖定更大範圍的物件,而不是維持可序列化所必需的範圍),但是由於它們的開銷較低,所以是一個很好的折衷。
 
@@ -751,76 +751,76 @@ WHERE room_id = 123 AND
 
 本章描繪了資料庫中併發控制的黯淡畫面。一方面,我們實現了效能不好(2PL)或者伸縮性不好(序列執行)的可序列化隔離級別。另一方面,我們有效能良好的弱隔離級別,但容易出現各種競爭條件(丟失更新,寫入偏差,幻讀等)。序列化的隔離級別和高效能是從根本上相互矛盾的嗎?
 
-也許不是:一個稱為**可序列化快照隔離(SSI, serializable snapshot isolation)** 的演算法是非常有前途的。它提供了完整的可序列化隔離級別,但與快照隔離相比只有很小的效能損失。 SSI是相當新的:它在2008年首次被描述【40】,並且是Michael Cahill的博士論文【51】的主題。
+也許不是:一個稱為 **可序列化快照隔離(SSI, serializable snapshot isolation)** 的演算法是非常有前途的。它提供了完整的可序列化隔離級別,但與快照隔離相比只有很小的效能損失。 SSI 是相當新的:它在 2008 年首次被描述【40】,並且是 Michael Cahill 的博士論文【51】的主題。
 
-今天,SSI既用於單節點資料庫(PostgreSQL9.1 以後的可序列化隔離級別)和分散式資料庫(FoundationDB使用類似的演算法)。由於SSI與其他併發控制機制相比還很年輕,還處於在實踐中證明自己表現的階段。但它有可能因為足夠快而在未來成為新的預設選項。
+今天,SSI 既用於單節點資料庫(PostgreSQL9.1 以後的可序列化隔離級別)和分散式資料庫(FoundationDB 使用類似的演算法)。由於 SSI 與其他併發控制機制相比還很年輕,還處於在實踐中證明自己表現的階段。但它有可能因為足夠快而在未來成為新的預設選項。
 
 #### 悲觀與樂觀的併發控制
 
-兩階段鎖是一種所謂的**悲觀併發控制機制(pessimistic)** :它是基於這樣的原則:如果有事情可能出錯(如另一個事務所持有的鎖所表示的),最好等到情況安全後再做任何事情。這就像互斥,用於保護多執行緒程式設計中的資料結構。
+兩階段鎖是一種所謂的 **悲觀併發控制機制(pessimistic)** :它是基於這樣的原則:如果有事情可能出錯(如另一個事務所持有的鎖所表示的),最好等到情況安全後再做任何事情。這就像互斥,用於保護多執行緒程式設計中的資料結構。
 
-從某種意義上說,序列執行可以稱為悲觀到了極致:在事務持續期間,每個事務對整個資料庫(或資料庫的一個分割槽)具有排它鎖,作為對悲觀的補償,我們讓每筆事務執行得非常快,所以只需要短時間持有“鎖”。
+從某種意義上說,序列執行可以稱為悲觀到了極致:在事務持續期間,每個事務對整個資料庫(或資料庫的一個分割槽)具有排它鎖,作為對悲觀的補償,我們讓每筆事務執行得非常快,所以只需要短時間持有 “鎖”。
 
-相比之下,**序列化快照隔離**是一種**樂觀(optimistic)** 的併發控制技術。在這種情況下,樂觀意味著,如果存在潛在的危險也不阻止事務,而是繼續執行事務,希望一切都會好起來。當一個事務想要提交時,資料庫檢查是否有什麼不好的事情發生(即隔離是否被違反);如果是的話,事務將被中止,並且必須重試。只有可序列化的事務才被允許提交。
+相比之下,**序列化快照隔離** 是一種 **樂觀(optimistic)** 的併發控制技術。在這種情況下,樂觀意味著,如果存在潛在的危險也不阻止事務,而是繼續執行事務,希望一切都會好起來。當一個事務想要提交時,資料庫檢查是否有什麼不好的事情發生(即隔離是否被違反);如果是的話,事務將被中止,並且必須重試。只有可序列化的事務才被允許提交。
 
-樂觀併發控制是一個古老的想法【52】,其優點和缺點已經爭論了很長時間【53】。如果存在很多**爭用**(contention,即很多事務試圖訪問相同的物件),則表現不佳,因為這會導致很大一部分事務需要中止。如果系統已經接近最大吞吐量,來自重試事務的額外負載可能會使效能變差。
+樂觀併發控制是一個古老的想法【52】,其優點和缺點已經爭論了很長時間【53】。如果存在很多 **爭用**(contention,即很多事務試圖訪問相同的物件),則表現不佳,因為這會導致很大一部分事務需要中止。如果系統已經接近最大吞吐量,來自重試事務的額外負載可能會使效能變差。
 
 但是,如果有足夠的備用容量,並且事務之間的爭用不是太高,樂觀的併發控制技術往往比悲觀的要好。可交換的原子操作可以減少爭用:例如,如果多個事務同時要增加一個計數器,那麼應用增量的順序(只要計數器不在同一個事務中讀取)就無關緊要了,所以併發增量可以全部應用且無需衝突。
 
-顧名思義,SSI基於快照隔離——也就是說,事務中的所有讀取都是來自資料庫的一致性快照(請參閱“[快照隔離和可重複讀取](#快照隔離和可重複讀)”)。與早期的樂觀併發控制技術相比這是主要的區別。在快照隔離的基礎上,SSI添加了一種演算法來檢測寫入之間的序列化衝突,並確定要中止哪些事務。
+顧名思義,SSI 基於快照隔離 —— 也就是說,事務中的所有讀取都是來自資料庫的一致性快照(請參閱 “[快照隔離和可重複讀取](#快照隔離和可重複讀)”)。與早期的樂觀併發控制技術相比這是主要的區別。在快照隔離的基礎上,SSI 添加了一種演算法來檢測寫入之間的序列化衝突,並確定要中止哪些事務。
 
 #### 基於過時前提的決策
 
-先前討論了快照隔離中的寫入偏差(請參閱“[寫入偏斜與幻讀](#寫入偏斜與幻讀)”)時,我們觀察到一個迴圈模式:事務從資料庫讀取一些資料,檢查查詢的結果,並根據它看到的結果決定採取一些操作(寫入資料庫)。但是,在快照隔離的情況下,原始查詢的結果在事務提交時可能不再是最新的,因為資料可能在同一時間被修改。
+先前討論了快照隔離中的寫入偏差(請參閱 “[寫入偏斜與幻讀](#寫入偏斜與幻讀)”)時,我們觀察到一個迴圈模式:事務從資料庫讀取一些資料,檢查查詢的結果,並根據它看到的結果決定採取一些操作(寫入資料庫)。但是,在快照隔離的情況下,原始查詢的結果在事務提交時可能不再是最新的,因為資料可能在同一時間被修改。
 
-換句話說,事務基於一個**前提(premise)** 採取行動(事務開始時候的事實,例如:“目前有兩名醫生正在值班”)。之後當事務要提交時,原始資料可能已經改變——前提可能不再成立。
+換句話說,事務基於一個 **前提(premise)** 採取行動(事務開始時候的事實,例如:“目前有兩名醫生正在值班”)。之後當事務要提交時,原始資料可能已經改變 —— 前提可能不再成立。
 
 當應用程式進行查詢時(例如,“當前有多少醫生正在值班?”),資料庫不知道應用邏輯如何使用該查詢結果。在這種情況下為了安全,資料庫需要假設任何對該結果集的變更都可能會使該事務中的寫入變得無效。 換而言之,事務中的查詢與寫入可能存在因果依賴。為了提供可序列化的隔離級別,如果事務在過時的前提下執行操作,資料庫必須能檢測到這種情況,並中止事務。
 
 資料庫如何知道查詢結果是否可能已經改變?有兩種情況需要考慮:
 
-- 檢測對舊MVCC物件版本的讀取(讀之前存在未提交的寫入)
+- 檢測對舊 MVCC 物件版本的讀取(讀之前存在未提交的寫入)
 - 檢測影響先前讀取的寫入(讀之後發生寫入)
 
 #### 檢測舊MVCC讀取
 
-回想一下,快照隔離通常是透過多版本併發控制(MVCC;見[圖7-10](../img/fig7-10.png))來實現的。當一個事務從MVCC資料庫中的一致快照讀時,它將忽略取快照時尚未提交的任何其他事務所做的寫入。在[圖7-10](../img/fig7-10.png)中,事務43 認為Alice的 `on_call = true` ,因為事務42(修改Alice的待命狀態)未被提交。然而,在事務43想要提交時,事務42 已經提交。這意味著在讀一致性快照時被忽略的寫入已經生效,事務43 的前提不再為真。
+回想一下,快照隔離通常是透過多版本併發控制(MVCC;見 [圖 7-10](../img/fig7-10.png))來實現的。當一個事務從 MVCC 資料庫中的一致快照讀時,它將忽略取快照時尚未提交的任何其他事務所做的寫入。在 [圖 7-10](../img/fig7-10.png) 中,事務 43 認為 Alice 的 `on_call = true` ,因為事務 42(修改 Alice 的待命狀態)未被提交。然而,在事務 43 想要提交時,事務 42 已經提交。這意味著在讀一致性快照時被忽略的寫入已經生效,事務 43 的前提不再為真。
 
 ![](../img/fig7-10.png)
 
-**圖7-10 檢測事務何時從MVCC快照讀取過時的值**
+**圖 7-10 檢測事務何時從 MVCC 快照讀取過時的值**
 
-為了防止這種異常,資料庫需要跟蹤一個事務由於MVCC可見性規則而忽略另一個事務的寫入。當事務想要提交時,資料庫檢查是否有任何被忽略的寫入現在已經被提交。如果是這樣,事務必須中止。
+為了防止這種異常,資料庫需要跟蹤一個事務由於 MVCC 可見性規則而忽略另一個事務的寫入。當事務想要提交時,資料庫檢查是否有任何被忽略的寫入現在已經被提交。如果是這樣,事務必須中止。
 
-為什麼要等到提交?當檢測到陳舊的讀取時,為什麼不立即中止事務43 ?因為如果事務43 是隻讀事務,則不需要中止,因為沒有寫入偏差的風險。當事務43 進行讀取時,資料庫還不知道事務是否要稍後執行寫操作。此外,事務42 可能在事務43 被提交的時候中止或者可能仍然未被提交,因此讀取可能終究不是陳舊的。透過避免不必要的中止,SSI 保留了快照隔離從一致快照中長時間讀取的能力。
+為什麼要等到提交?當檢測到陳舊的讀取時,為什麼不立即中止事務 43 ?因為如果事務 43 是隻讀事務,則不需要中止,因為沒有寫入偏差的風險。當事務 43 進行讀取時,資料庫還不知道事務是否要稍後執行寫操作。此外,事務 42 可能在事務 43 被提交的時候中止或者可能仍然未被提交,因此讀取可能終究不是陳舊的。透過避免不必要的中止,SSI 保留了快照隔離從一致快照中長時間讀取的能力。
 
 #### 檢測影響之前讀取的寫入
 
-第二種情況要考慮的是另一個事務在讀取資料之後修改資料。這種情況如[圖7-11](../img/fig7-11.png)所示。
+第二種情況要考慮的是另一個事務在讀取資料之後修改資料。這種情況如 [圖 7-11](../img/fig7-11.png) 所示。
 
 ![](../img/fig7-11.png)
 
-**圖7-11 在可序列化快照隔離中,檢測一個事務何時修改另一個事務的讀取。**
+**圖 7-11 在可序列化快照隔離中,檢測一個事務何時修改另一個事務的讀取。**
 
-在兩階段鎖定的上下文中,我們討論了索引範圍鎖(請參閱“[索引範圍鎖](#索引範圍鎖)”),它允許資料庫鎖定與某個搜尋查詢匹配的所有行的訪問權,例如 `WHERE shift_id = 1234`。可以在這裡使用類似的技術,除了SSI鎖不會阻塞其他事務。
+在兩階段鎖定的上下文中,我們討論了索引範圍鎖(請參閱 “[索引範圍鎖](#索引範圍鎖)”),它允許資料庫鎖定與某個搜尋查詢匹配的所有行的訪問權,例如 `WHERE shift_id = 1234`。可以在這裡使用類似的技術,除了 SSI 鎖不會阻塞其他事務。
 
-在[圖7-11](../img/fig7-11.png)中,事務42 和43 都在班次1234 查詢值班醫生。如果在`shift_id`上有索引,則資料庫可以使用索引項1234 來記錄事務42 和43 讀取這個資料的事實。 (如果沒有索引,這個資訊可以在表級別進行跟蹤)。這個資訊只需要保留一段時間:在一個事務完成(提交或中止),並且所有的併發事務完成之後,資料庫就可以忘記它讀取的資料了。
+在 [圖 7-11](../img/fig7-11.png) 中,事務 42 和 43 都在班次 1234 查詢值班醫生。如果在 `shift_id` 上有索引,則資料庫可以使用索引項 1234 來記錄事務 42 和 43 讀取這個資料的事實。 (如果沒有索引,這個資訊可以在表級別進行跟蹤)。這個資訊只需要保留一段時間:在一個事務完成(提交或中止),並且所有的併發事務完成之後,資料庫就可以忘記它讀取的資料了。
 
 當事務寫入資料庫時,它必須在索引中查詢最近曾讀取受影響資料的其他事務。這個過程類似於在受影響的鍵範圍上獲取寫鎖,但鎖並不會阻塞事務指導其他讀事務完成,而是像警戒線一樣只是簡單通知其他事務:你們讀過的資料可能不是最新的啦。
 
-在[圖7-11](../img/fig7-11.png)中,事務43 通知事務42 其先前讀已過時,反之亦然。事務42首先提交併成功,儘管事務43 的寫影響了42 ,但因為事務43 尚未提交,所以寫入尚未生效。然而當事務43 想要提交時,來自事務42 的衝突寫入已經被提交,所以事務43 必須中止。
+在 [圖 7-11](../img/fig7-11.png) 中,事務 43 通知事務 42 其先前讀已過時,反之亦然。事務 42 首先提交併成功,儘管事務 43 的寫影響了 42 ,但因為事務 43 尚未提交,所以寫入尚未生效。然而當事務 43 想要提交時,來自事務 42 的衝突寫入已經被提交,所以事務 43 必須中止。
 
 #### 可序列化快照隔離的效能
 
-與往常一樣,許多工程細節會影響演算法的實際表現。例如一個權衡是跟蹤事務的讀取和寫入的**粒度(granularity)**。如果資料庫詳細地跟蹤每個事務的活動(細粒度),那麼可以準確地確定哪些事務需要中止,但是簿記開銷可能變得很顯著。簡略的跟蹤速度更快(粗粒度),但可能會導致更多不必要的事務中止。
+與往常一樣,許多工程細節會影響演算法的實際表現。例如一個權衡是跟蹤事務的讀取和寫入的 **粒度(granularity)**。如果資料庫詳細地跟蹤每個事務的活動(細粒度),那麼可以準確地確定哪些事務需要中止,但是簿記開銷可能變得很顯著。簡略的跟蹤速度更快(粗粒度),但可能會導致更多不必要的事務中止。
 
-在某些情況下,事務可以讀取被另一個事務覆蓋的資訊:這取決於發生了什麼,有時可以證明執行結果無論如何都是可序列化的。 PostgreSQL使用這個理論來減少不必要的中止次數【11,41】。
+在某些情況下,事務可以讀取被另一個事務覆蓋的資訊:這取決於發生了什麼,有時可以證明執行結果無論如何都是可序列化的。 PostgreSQL 使用這個理論來減少不必要的中止次數【11,41】。
 
 與兩階段鎖定相比,可序列化快照隔離的最大優點是一個事務不需要阻塞等待另一個事務所持有的鎖。就像在快照隔離下一樣,寫不會阻塞讀,反之亦然。這種設計原則使得查詢延遲更可預測,變數更少。特別是,只讀查詢可以執行在一致快照上,而不需要任何鎖定,這對於讀取繁重的工作負載非常有吸引力。
 
-與序列執行相比,可序列化快照隔離並不侷限於單個CPU核的吞吐量:FoundationDB將檢測到的序列化衝突分佈在多臺機器上,允許擴充套件到很高的吞吐量。即使資料可能跨多臺機器進行分割槽,事務也可以在保證可序列化隔離等級的同時讀寫多個分割槽中的資料【54】。
+與序列執行相比,可序列化快照隔離並不侷限於單個 CPU 核的吞吐量:FoundationDB 將檢測到的序列化衝突分佈在多臺機器上,允許擴充套件到很高的吞吐量。即使資料可能跨多臺機器進行分割槽,事務也可以在保證可序列化隔離等級的同時讀寫多個分割槽中的資料【54】。
 
-中止率顯著影響SSI的整體表現。例如,長時間讀取和寫入資料的事務很可能會發生衝突並中止,因此SSI要求同時讀寫的事務儘量短(只讀的長事務可能沒問題)。對於慢事務,SSI可能比兩階段鎖定或序列執行更不敏感。
+中止率顯著影響 SSI 的整體表現。例如,長時間讀取和寫入資料的事務很可能會發生衝突並中止,因此 SSI 要求同時讀寫的事務儘量短(只讀的長事務可能沒問題)。對於慢事務,SSI 可能比兩階段鎖定或序列執行更不敏感。
 
 
 ## 本章小結
@@ -831,11 +831,11 @@ WHERE room_id = 123 AND
 
 如果沒有事務處理,各種錯誤情況(程序崩潰,網路中斷,停電,磁碟已滿,意外併發等)意味著資料可能以各種方式變得不一致。例如,非規範化的資料可能很容易與源資料不同步。如果沒有事務處理,就很難推斷複雜的互動訪問可能對資料庫造成的影響。
 
-本章深入討論了**併發控制**的話題。我們討論了幾個廣泛使用的隔離級別,特別是**讀已提交**,**快照隔離**(有時稱為可重複讀)和**可序列化**。並透過研究競爭條件的各種例子,來描述這些隔離等級:
+本章深入討論了 **併發控制** 的話題。我們討論了幾個廣泛使用的隔離級別,特別是 **讀已提交**、**快照隔離**(有時稱為可重複讀)和 **可序列化**。並透過研究競爭條件的各種例子,來描述這些隔離等級:
 
 * 髒讀
 
-  一個客戶端讀取到另一個客戶端尚未提交的寫入。**讀已提交**或更強的隔離級別可以防止髒讀。
+  一個客戶端讀取到另一個客戶端尚未提交的寫入。**讀已提交** 或更強的隔離級別可以防止髒讀。
 
 * 髒寫
 
@@ -843,11 +843,11 @@ WHERE room_id = 123 AND
 
 * 讀取偏差(不可重複讀)
 
-  在同一個事務中,客戶端在不同的時間點會看見資料庫的不同狀態。**快照隔離**經常用於解決這個問題,它允許事務從一個特定時間點的一致性快照中讀取資料。快照隔離通常使用**多版本併發控制(MVCC)** 來實現。
+  在同一個事務中,客戶端在不同的時間點會看見資料庫的不同狀態。**快照隔離** 經常用於解決這個問題,它允許事務從一個特定時間點的一致性快照中讀取資料。快照隔離通常使用 **多版本併發控制(MVCC)** 來實現。
 
 * 更新丟失
 
-  兩個客戶端同時執行**讀取-修改-寫入序列**。其中一個寫操作,在沒有合併另一個寫入變更情況下,直接覆蓋了另一個寫操作的結果。所以導致資料丟失。快照隔離的一些實現可以自動防止這種異常,而另一些實現則需要手動鎖定(`SELECT FOR UPDATE`)。
+  兩個客戶端同時執行 **讀取 - 修改 - 寫入序列**。其中一個寫操作,在沒有合併另一個寫入變更情況下,直接覆蓋了另一個寫操作的結果。所以導致資料丟失。快照隔離的一些實現可以自動防止這種異常,而另一些實現則需要手動鎖定(`SELECT FOR UPDATE`)。
 
 * 寫偏差
 
@@ -861,7 +861,7 @@ WHERE room_id = 123 AND
 
 * 字面意義上的序列執行
 
-  如果每個事務的執行速度非常快,並且事務吞吐量足夠低,足以在單個CPU核上處理,這是一個簡單而有效的選擇。
+  如果每個事務的執行速度非常快,並且事務吞吐量足夠低,足以在單個 CPU 核上處理,這是一個簡單而有效的選擇。
 
 * 兩階段鎖定
 
@@ -871,7 +871,7 @@ WHERE room_id = 123 AND
 
   一個相當新的演算法,避免了先前方法的大部分缺點。它使用樂觀的方法,允許事務執行而無需阻塞。當一個事務想要提交時,它會進行檢查,如果執行不可序列化,事務就會被中止。
 
-本章中的示例主要是在關係資料模型的上下文中。但是,正如在討論中,無論使用哪種資料模型,如“**[多物件事務的需求](#多物件事務的需求)**”中所討論的,事務都是有價值的資料庫功能。
+本章中的示例主要是在關係資料模型的上下文中。但是,正如在討論中,無論使用哪種資料模型,如 “**[多物件事務的需求](#多物件事務的需求)**” 中所討論的,事務都是有價值的資料庫功能。
 
 本章主要是在單機資料庫的上下文中,探討了各種想法和演算法。分散式資料庫中的事務,則引入了一系列新的困難挑戰,我們將在接下來的兩章中討論。