2
0
Fork 0
mirror of https://github.com/Vonng/ddia.git synced 2026-06-23 01:47:00 +08:00

update text spacing with pangu and some manual adjustments

This commit is contained in:
Gang Yin 2022-01-20 14:42:27 +08:00
parent 11429ddb4e
commit 7f7bcad5b4
8 changed files with 1372 additions and 1370 deletions

18
ch4.md
View file

@ -36,13 +36,13 @@
向前兼容性可能会更棘手,因为旧版的程序需要忽略新版数据格式中新增的部分。 向前兼容性可能会更棘手,因为旧版的程序需要忽略新版数据格式中新增的部分。
本章中将介绍几种编码数据的格式,包括 JSONXMLProtocol BuffersThrift和Avro。尤其将关注这些格式如何应对模式变化以及它们如何对新旧代码数据需要共存的系统提供支持。然后将讨论如何使用这些格式进行数据存储和通信在Web服务中**表述性状态传递REST** 和**远程过程调用RPC**,以及**消息传递系统**如Actor和消息队列 本章中将介绍几种编码数据的格式,包括 JSON、XML、Protocol Buffers、Thrift 和 Avro。尤其将关注这些格式如何应对模式变化以及它们如何对新旧代码数据需要共存的系统提供支持。然后将讨论如何使用这些格式进行数据存储和通信 Web 服务中,**表述性状态传递REST** 和 **远程过程调用RPC**,以及 **消息传递系统**(如 Actor 和消息队列)。
## 编码数据的格式 ## 编码数据的格式
程序通常(至少)使用两种形式的数据: 程序通常(至少)使用两种形式的数据:
1. 在内存中,数据保存在对象,结构体,列表,数组,散列表,树等中。 这些数据结构针对CPU的高效访问和操作进行了优化(通常使用指针)。 1. 在内存中,数据保存在对象、结构体、列表、数组、散列表、树等中。 这些数据结构针对 CPU 的高效访问和操作进行了优化(通常使用指针)。
2. 如果要将数据写入文件,或通过网络发送,则必须将其 **编码encode** 为某种自包含的字节序列例如JSON 文档)。 由于每个进程都有自己独立的地址空间,一个进程中的指针对任何其他进程都没有意义,所以这个字节序列表示会与通常在内存中使用的数据结构完全不同 [^i]。 2. 如果要将数据写入文件,或通过网络发送,则必须将其 **编码encode** 为某种自包含的字节序列例如JSON 文档)。 由于每个进程都有自己独立的地址空间,一个进程中的指针对任何其他进程都没有意义,所以这个字节序列表示会与通常在内存中使用的数据结构完全不同 [^i]。
[^i]: 除一些特殊情况外,例如某些内存映射文件或直接在压缩数据上操作(如 “[列压缩](ch4.md#列压缩)” 中所述)。 [^i]: 除一些特殊情况外,例如某些内存映射文件或直接在压缩数据上操作(如 “[列压缩](ch4.md#列压缩)” 中所述)。
@ -82,13 +82,13 @@ JSONXML和CSV属于文本格式因此具有人类可读性尽管它们
* XML 【11】和 JSON 【12】都有可选的模式支持。这些模式语言相当强大所以学习和实现起来都相当复杂。 XML 模式的使用相当普遍,但许多基于 JSON 的工具才不会去折腾模式。对数据的正确解读(例如区分数值与二进制串)取决于模式中的信息,因此不使用 XML/JSON 模式的应用程序可能需要对相应的编码 / 解码逻辑进行硬编码。 * XML 【11】和 JSON 【12】都有可选的模式支持。这些模式语言相当强大所以学习和实现起来都相当复杂。 XML 模式的使用相当普遍,但许多基于 JSON 的工具才不会去折腾模式。对数据的正确解读(例如区分数值与二进制串)取决于模式中的信息,因此不使用 XML/JSON 模式的应用程序可能需要对相应的编码 / 解码逻辑进行硬编码。
* CSV 没有任何模式,因此每行和每列的含义完全由应用程序自行定义。如果应用程序变更添加了新的行或列,那么这种变更必须通过手工处理。 CSV 也是一个相当模糊的格式如果一个值包含逗号或换行符会发生什么。尽管其转义规则已经被正式指定【13】但并不是所有的解析器都正确的实现了标准。 * CSV 没有任何模式,因此每行和每列的含义完全由应用程序自行定义。如果应用程序变更添加了新的行或列,那么这种变更必须通过手工处理。 CSV 也是一个相当模糊的格式如果一个值包含逗号或换行符会发生什么。尽管其转义规则已经被正式指定【13】但并不是所有的解析器都正确的实现了标准。
尽管存在这些缺陷,但JSONXML和CSV对很多需求来说已经足够好了。它们很可能会继续流行下去,特别是作为数据交换格式来说(即将数据从一个组织发送到另一个组织)。在这种情况下,只要人们对格式是什么意见一致,格式有多美观或者效率有多高效就无所谓了。让不同的组织就这些东西达成一致的难度超过了绝大多数问题。 尽管存在这些缺陷,但 JSON、XML 和 CSV 对很多需求来说已经足够好了。它们很可能会继续流行下去,特别是作为数据交换格式来说(即将数据从一个组织发送到另一个组织)。在这种情况下,只要人们对格式是什么意见一致,格式有多美观或者效率有多高效就无所谓了。让不同的组织就这些东西达成一致的难度超过了绝大多数问题。
#### 二进制编码 #### 二进制编码
对于仅在组织内部使用的数据,使用最小公约数式的编码格式压力较小。例如,可以选择更紧凑或更快的解析格式。虽然对小数据集来说,收益可以忽略不计;但一旦达到 TB 级别,数据格式的选型就会产生巨大的影响。 对于仅在组织内部使用的数据,使用最小公约数式的编码格式压力较小。例如,可以选择更紧凑或更快的解析格式。虽然对小数据集来说,收益可以忽略不计;但一旦达到 TB 级别,数据格式的选型就会产生巨大的影响。
JSON比XML简洁,但与二进制格式相比还是太占空间。这一事实导致大量二进制编码版本JSONMessagePackBSONBJSONUBJSONBISON和Smile等 和 XML例如WBXML和Fast Infoset的出现。这些格式已经在各种各样的领域中采用但是没有一个能像文本版JSON和XML那样被广泛采用。 JSON 比 XML 简洁,但与二进制格式相比还是太占空间。这一事实导致大量二进制编码版本 JSONMessagePack、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`
@ -105,7 +105,7 @@ 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 里,那么它会用另一个不同的类型标识符,字段的数量被编码两个或四个字节)。 1. 第一个字节 `0x83` 表示接下来是 **3** 个字段(低四位 = `0x03`)的 **对象 object**(高四位 = `0x80`)。 (如果想知道如果一个对象有 15 个以上的字段会发生什么情况,字段的数量塞不进 4 个 bit 里,那么它会用另一个不同的类型标识符,字段的数量被编码两个或四个字节)。
2. 第二个字节`0xa8`表示接下来是**8**字节长(低四位=0x08的字符串高四位= 0x0a)。 2. 第二个字节 `0xa8` 表示接下来是 **8** 字节长(低四位 = `0x08`)的字符串(高四位 = `0x0a`)。
3. 接下来八个字节是 ASCII 字符串形式的字段名称 `userName`。由于之前已经指明长度,不需要任何标记来标识字符串的结束位置(或者任何转义)。 3. 接下来八个字节是 ASCII 字符串形式的字段名称 `userName`。由于之前已经指明长度,不需要任何标记来标识字符串的结束位置(或者任何转义)。
4. 接下来的七个字节对前缀为 `0xa6` 的六个字母的字符串值 `Martin` 进行编码,依此类推。 4. 接下来的七个字节对前缀为 `0xa6` 的六个字母的字符串值 `Martin` 进行编码,依此类推。
@ -373,7 +373,7 @@ Avro为静态类型编程语言提供了可选的代码生成功能但是它
当你需要通过网络进行通信的进程时,安排该通信的方式有几种。最常见的安排是有两个角色:客户端和服务器。服务器通过网络公开 API并且客户端可以连接到服务器以向该 API 发出请求。服务器公开的 API 被称为服务。 当你需要通过网络进行通信的进程时,安排该通信的方式有几种。最常见的安排是有两个角色:客户端和服务器。服务器通过网络公开 API并且客户端可以连接到服务器以向该 API 发出请求。服务器公开的 API 被称为服务。
Web以这种方式工作客户Web浏览器向Web服务器发出请求通过GET请求下载HTMLCSSJavaScript图像等并通过POST请求提交数据到服务器。 API包含一组标准的协议和数据格式HTTPURLSSL/TLSHTML等。由于网络浏览器网络服务器和网站作者大多同意这些标准,你可以使用任何网络浏览器访问任何网站(至少在理论上!)。 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 的细节达成一致。
@ -443,7 +443,7 @@ Web服务仅仅是通过网络进行API请求的一系列技术的最新版本
RPC 方案的前后向兼容性属性从它使用的编码方式中继承: RPC 方案的前后向兼容性属性从它使用的编码方式中继承:
* ThriftgRPCProtobuf和Avro RPC可以根据相应编码格式的兼容性规则进行演变。 * Thrift、gRPCProtobuf和 Avro RPC 可以根据相应编码格式的兼容性规则进行演变。
* 在 SOAP 中,请求和响应是使用 XML 模式指定的。这些可以演变但有一些微妙的陷阱【47】。 * 在 SOAP 中,请求和响应是使用 XML 模式指定的。这些可以演变但有一些微妙的陷阱【47】。
* RESTful API 通常使用 JSON没有正式指定的模式用于响应以及用于请求的 JSON 或 URI 编码 / 表单编码的请求参数。添加可选的请求参数并向响应对象添加新的字段通常被认为是保持兼容性的改变。 * RESTful API 通常使用 JSON没有正式指定的模式用于响应以及用于请求的 JSON 或 URI 编码 / 表单编码的请求参数。添加可选的请求参数并向响应对象添加新的字段通常被认为是保持兼容性的改变。
@ -481,7 +481,7 @@ RPC方案的前后向兼容性属性从它使用的编码方式中继承
#### 分布式的Actor框架 #### 分布式的Actor框架
Actor模型是单个进程中并发的编程模型。逻辑被封装在actor中而不是直接处理线程以及竞争条件锁定和死锁的相关问题。每个actor通常代表一个客户或实体,它可能有一些本地状态(不与其他任何角色共享),它通过发送和接收异步消息与其他角色通信。不保证消息传送:在某些错误情况下,消息将丢失。由于每个角色一次只能处理一条消息,因此不需要担心线程,每个角色可以由框架独立调度。 Actor 模型是单个进程中并发的编程模型。逻辑被封装在 actor 中,而不是直接处理线程(以及竞争条件、锁定和死锁的相关问题)。每个 actor 通常代表一个客户或实体,它可能有一些本地状态(不与其他任何角色共享),它通过发送和接收异步消息与其他角色通信。不保证消息传送:在某些错误情况下,消息将丢失。由于每个角色一次只能处理一条消息,因此不需要担心线程,每个角色可以由框架独立调度。
在分布式 Actor 框架中,此编程模型用于跨多个节点伸缩应用程序。不管发送方和接收方是在同一个节点上还是在不同的节点上,都使用相同的消息传递机制。如果它们在不同的节点上,则该消息被透明地编码成字节序列,通过网络发送,并在另一侧解码。 在分布式 Actor 框架中,此编程模型用于跨多个节点伸缩应用程序。不管发送方和接收方是在同一个节点上还是在不同的节点上,都使用相同的消息传递机制。如果它们在不同的节点上,则该消息被透明地编码成字节序列,通过网络发送,并在另一侧解码。
@ -492,7 +492,7 @@ Actor模型是单个进程中并发的编程模型。逻辑被封装在actor中
三个流行的分布式 actor 框架处理消息编码如下: 三个流行的分布式 actor 框架处理消息编码如下:
* 默认情况下Akka 使用 Java 的内置序列化,不提供前向或后向兼容性。 但是,你可以用类似 Prototol Buffers 的东西替代它从而获得滚动升级的能力【50】。 * 默认情况下Akka 使用 Java 的内置序列化,不提供前向或后向兼容性。 但是,你可以用类似 Prototol Buffers 的东西替代它从而获得滚动升级的能力【50】。
* Orleans 默认使用不支持滚动升级部署的自定义数据编码格式; 要部署新版本的应用程序你需要设置一个新的集群将流量从旧集群迁移到新集群然后关闭旧集群【51,52】。 像Akka一样可以使用自定义序列化插件。 * Orleans 默认使用不支持滚动升级部署的自定义数据编码格式要部署新版本的应用程序你需要设置一个新的集群将流量从旧集群迁移到新集群然后关闭旧集群【51,52】。 像 Akka 一样,可以使用自定义序列化插件。
* 在 Erlang OTP 中,对记录模式进行更改是非常困难的(尽管系统具有许多为高可用性设计的功能)。 滚动升级是可能的但需要仔细计划【53】。 一个新的实验性的 `maps` 数据类型2014 年在 Erlang R17 中引入的类似于 JSON 的结构可能使得这个数据类型在未来更容易【54】。 * 在 Erlang OTP 中,对记录模式进行更改是非常困难的(尽管系统具有许多为高可用性设计的功能)。 滚动升级是可能的但需要仔细计划【53】。 一个新的实验性的 `maps` 数据类型2014 年在 Erlang R17 中引入的类似于 JSON 的结构可能使得这个数据类型在未来更容易【54】。

8
ch5.md
View file

@ -2,9 +2,9 @@
![](img/ch5.png) ![](img/ch5.png)
> 与可能出错的东西比,'不可能'出错的东西最显著的特点就是:一旦真的出错,通常就彻底玩完了。 > 与可能出错的东西比,“不可能”出错的东西最显著的特点就是:一旦真的出错,通常就彻底玩完了。
> >
> —— 道格拉斯·亚当斯1992 > —— 道格拉斯亚当斯1992
------ ------
@ -134,7 +134,7 @@
* 任何调用 **非确定性函数nondeterministic** 的语句,可能会在每个副本上生成不同的值。例如,使用 `NOW()` 获取当前日期时间,或使用 `RAND()` 获取一个随机数。 * 任何调用 **非确定性函数nondeterministic** 的语句,可能会在每个副本上生成不同的值。例如,使用 `NOW()` 获取当前日期时间,或使用 `RAND()` 获取一个随机数。
* 如果语句使用了 **自增列auto increment**,或者依赖于数据库中的现有数据(例如,`UPDATE ... WHERE <某些条件>`),则必须在每个副本上按照完全相同的顺序执行它们,否则可能会产生不同的效果。当有多个并发执行的事务时,这可能成为一个限制。 * 如果语句使用了 **自增列auto increment**,或者依赖于数据库中的现有数据(例如,`UPDATE ... WHERE <某些条件>`),则必须在每个副本上按照完全相同的顺序执行它们,否则可能会产生不同的效果。当有多个并发执行的事务时,这可能成为一个限制。
* 有副作用的语句(例如,触发器,存储过程,用户定义的函数)可能会在每个副本上产生不同的副作用,除非副作用是绝对确定的。 * 有副作用的语句(例如:触发器、存储过程、用户定义的函数)可能会在每个副本上产生不同的副作用,除非副作用是绝对确定的。
的确有办法绕开这些问题 —— 例如,当语句被记录时,主库可以用固定的返回值替换任何不确定的函数调用,以便从库获得相同的值。但是由于边缘情况实在太多了,现在通常会选择其他的复制方法。 的确有办法绕开这些问题 —— 例如,当语句被记录时,主库可以用固定的返回值替换任何不确定的函数调用,以便从库获得相同的值。但是由于边缘情况实在太多了,现在通常会选择其他的复制方法。
@ -192,7 +192,7 @@ PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志
不幸的是,当应用程序从异步从库读取时,如果从库落后,它可能会看到过时的信息。这会导致数据库中出现明显的不一致:同时对主库和从库执行相同的查询,可能得到不同的结果,因为并非所有的写入都反映在从库中。这种不一致只是一个暂时的状态 —— 如果停止写入数据库并等待一段时间,从库最终会赶上并与主库保持一致。出于这个原因,这种效应被称为 **最终一致性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**,即写入主库到反映至从库之间的延迟,可能仅仅是几分之一秒,在实践中并不显眼。但如果系统在接近极限的情况下运行,或网络中存在问题,延迟可以轻而易举地超过几秒,甚至几分钟。

21
ch6.md
View file

@ -34,7 +34,8 @@
分区通常与复制结合使用,使得每个分区的副本存储在多个节点上。这意味着,即使每条记录属于一个分区,它仍然可以存储在多个不同的节点上以获得容错能力。 分区通常与复制结合使用,使得每个分区的副本存储在多个节点上。这意味着,即使每条记录属于一个分区,它仍然可以存储在多个不同的节点上以获得容错能力。
一个节点可能存储多个分区。 如果使用主从复制模型,则分区和复制的组合如[图6-1](img/fig6-1.png)所示。 每个分区领导者(主)被分配给一个节点,追随者(从)被分配给其他节点。 每个节点可能是某些分区的领导者,同时是其他分区的追随者。 一个节点可能存储多个分区。如果使用主从复制模型,则分区和复制的组合如 [图 6-1](img/fig6-1.png) 所示。每个分区领导者(主)被分配给一个节点,追随者(从)被分配给其他节点。 每个节点可能是某些分区的领导者,同时是其他分区的追随者。
我们在 [第五章](ch5.md) 讨论的关于数据库复制的所有内容同样适用于分区的复制。大多数情况下,分区方案的选择与复制方案的选择是独立的,为简单起见,本章中将忽略复制。 我们在 [第五章](ch5.md) 讨论的关于数据库复制的所有内容同样适用于分区的复制。大多数情况下,分区方案的选择与复制方案的选择是独立的,为简单起见,本章中将忽略复制。
![](img/fig6-1.png) ![](img/fig6-1.png)
@ -61,7 +62,7 @@
**图 6-2 印刷版百科全书按照关键字范围进行分区** **图 6-2 印刷版百科全书按照关键字范围进行分区**
键的范围不一定均匀分布,因为数据也很可能不均匀分布。例如在[图6-2](img/fig6-2.png)中第1卷包含以A和B开头的单词但第12卷则包含以TUVXY和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】。
@ -75,7 +76,7 @@
由于偏斜和热点的风险,许多分布式数据存储使用散列函数来确定给定键的分区。 由于偏斜和热点的风险,许多分布式数据存储使用散列函数来确定给定键的分区。
一个好的散列函数可以将偏斜的数据均匀分布。假设你有一个32位散列函数,无论何时给定一个新的字符串输入它将返回一个0到$2^{32}$ -1之间的“随机”数。即使输入的字符串非常相似,它们的散列也会均匀分布在这个数字范围内。 一个好的散列函数可以将偏斜的数据均匀分布。假设你有一个 32 位散列函数,无论何时给定一个新的字符串输入,它将返回一个 0 到 $2^{32}$ -1 之间的 “随机” 数。即使输入的字符串非常相似,它们的散列也会均匀分布在这个数字范围内。
出于分区的目的散列函数不需要多么强壮的加密算法例如Cassandra 和 MongoDB 使用 MD5Voldemort 使用 Fowler-Noll-Vo 函数。许多编程语言都有内置的简单哈希函数(它们用于散列表),但是它们可能不适合分区:例如,在 Java 的 `Object.hashCode()` 和 Ruby 的 `Object#hash`同一个键可能在不同的进程中有不同的哈希值【6】。 出于分区的目的散列函数不需要多么强壮的加密算法例如Cassandra 和 MongoDB 使用 MD5Voldemort 使用 Fowler-Noll-Vo 函数。许多编程语言都有内置的简单哈希函数(它们用于散列表),但是它们可能不适合分区:例如,在 Java 的 `Object.hashCode()` 和 Ruby 的 `Object#hash`同一个键可能在不同的进程中有不同的哈希值【6】。
@ -105,7 +106,7 @@ Cassandra采取了折衷的策略【11, 12, 13】。 Cassandra中的表可以使
这种场景也许并不常见但并非闻所未闻例如在社交媒体网站上一个拥有数百万追随者的名人用户在做某事时可能会引发一场风暴【14】。这个事件可能导致同一个键的大量写入键可能是名人的用户 ID或者人们正在评论的动作的 ID。哈希策略不起作用因为两个相同 ID 的哈希值仍然是相同的。 这种场景也许并不常见但并非闻所未闻例如在社交媒体网站上一个拥有数百万追随者的名人用户在做某事时可能会引发一场风暴【14】。这个事件可能导致同一个键的大量写入键可能是名人的用户 ID或者人们正在评论的动作的 ID。哈希策略不起作用因为两个相同 ID 的哈希值仍然是相同的。
如今,大多数数据系统无法自动补偿这种高度偏斜的负载,因此应用程序有责任减少偏斜。例如,如果一个主键被认为是非常火爆的,一个简单的方法是在主键的开始或结尾添加一个随机数。只要一个两位数的十进制随机数就可以将主键分散为100种不同的主键,从而存储在不同的分区中。 如今,大多数数据系统无法自动补偿这种高度偏斜的负载,因此应用程序有责任减少偏斜。例如,如果一个主键被认为是非常火爆的,一个简单的方法是在主键的开始或结尾添加一个随机数。只要一个两位数的十进制随机数就可以将主键分散为 100 种不同的主键,从而存储在不同的分区中。
然而,将主键进行分割之后,任何读取都必须要做额外的工作,因为他们必须从所有 100 个主键分布中读取数据并将其合并。此技术还需要额外的记录:只需要对少量热点附加随机数;对于写入吞吐量低的绝大多数主键来说是不必要的开销。因此,你还需要一些方法来跟踪哪些键需要被分割。 然而,将主键进行分割之后,任何读取都必须要做额外的工作,因为他们必须从所有 100 个主键分布中读取数据并将其合并。此技术还需要额外的记录:只需要对少量热点附加随机数;对于写入吞吐量低的绝大多数主键来说是不必要的开销。因此,你还需要一些方法来跟踪哪些键需要被分割。
@ -153,7 +154,7 @@ Cassandra采取了折衷的策略【11, 12, 13】。 Cassandra中的表可以使
**图 6-5 基于关键词对次级索引进行分区** **图 6-5 基于关键词对次级索引进行分区**
我们将这种索引称为**关键词分区term-partitioned**,因为我们寻找的关键词决定了索引的分区方式。例如,一个关键词可能是:`color:red`。**关键词(Term)** 这个名称来源于全文搜索索引(一种特殊的次级索引),指文档中出现的所有单词。 我们将这种索引称为 **关键词分区term-partitioned**,因为我们寻找的关键词决定了索引的分区方式。例如,一个关键词可能是:`color:red`。**关键词Term** 这个名称来源于全文搜索索引(一种特殊的次级索引),指文档中出现的所有单词。
和之前一样,我们可以通过 **关键词** 本身或者它的散列进行索引分区。根据关键词本身来分区对于范围扫描非常有用(例如对于数值类的属性,像汽车的报价),而对关键词的哈希分区提供了负载均衡的能力。 和之前一样,我们可以通过 **关键词** 本身或者它的散列进行索引分区。根据关键词本身来分区对于范围扫描非常有用(例如对于数值类的属性,像汽车的报价),而对关键词的哈希分区提供了负载均衡的能力。
@ -184,7 +185,7 @@ Cassandra采取了折衷的策略【11, 12, 13】。 Cassandra中的表可以使
### 再平衡策略 ### 再平衡策略
有几种不同的分区分配方法【23】,让我们依次简要讨论一下。 有几种不同的分区分配方法【23】让我们依次简要讨论一下。
#### 反面教材hash mod N #### 反面教材hash mod N
@ -208,7 +209,7 @@ Cassandra采取了折衷的策略【11, 12, 13】。 Cassandra中的表可以使
**图 6-6 将新节点添加到每个节点具有多个分区的数据库集群。** **图 6-6 将新节点添加到每个节点具有多个分区的数据库集群。**
原则上,你甚至可以解决集群中的硬件不匹配问题:通过为更强大的节点分配更多的分区,可以强制这些节点承载更多的负载。在Riak 【15】Elasticsearch 【24】Couchbase 【10】和Voldemort 【25】中使用了这种再平衡的方法。 原则上,你甚至可以解决集群中的硬件不匹配问题:通过为更强大的节点分配更多的分区,可以强制这些节点承载更多的负载。在 Riak 【15】、Elasticsearch 【24】、Couchbase 【10】和 Voldemort 【25】中使用了这种再平衡的方法。
在这种配置中,分区的数量通常在数据库第一次建立时确定,之后不会改变。虽然原则上可以分割和合并分区(请参阅下一节),但固定数量的分区在操作上更简单,因此许多固定分区数据库选择不实施分区分割。因此,一开始配置的分区数就是你可以拥有的最大节点数量,所以你需要选择足够多的分区以适应未来的增长。但是,每个分区也有管理开销,所以选择太大的数字会适得其反。 在这种配置中,分区的数量通常在数据库第一次建立时确定,之后不会改变。虽然原则上可以分割和合并分区(请参阅下一节),但固定数量的分区在操作上更简单,因此许多固定分区数据库选择不实施分区分割。因此,一开始配置的分区数就是你可以拥有的最大节点数量,所以你需要选择足够多的分区以适应未来的增长。但是,每个分区也有管理开销,所以选择太大的数字会适得其反。
@ -242,7 +243,7 @@ Cassandra和Ketama使用的第三种方法是使分区数与节点数成正比
关于再平衡有一个重要问题:自动还是手动进行? 关于再平衡有一个重要问题:自动还是手动进行?
在全自动重新平衡系统自动决定何时将分区从一个节点移动到另一个节点无须人工干预和完全手动分区指派给节点由管理员明确配置仅在管理员明确重新配置时才会更改之间有一个权衡。例如CouchbaseRiak和Voldemort会自动生成建议的分区分配,但需要管理员提交才能生效。 在全自动重新平衡系统自动决定何时将分区从一个节点移动到另一个节点无须人工干预和完全手动分区指派给节点由管理员明确配置仅在管理员明确重新配置时才会更改之间有一个权衡。例如Couchbase、Riak 和 Voldemort 会自动生成建议的分区分配,但需要管理员提交才能生效。
全自动重新平衡可以很方便,因为正常维护的操作工作较少。但是,这可能是不可预测的。再平衡是一个昂贵的操作,因为它需要重新路由请求并将大量数据从一个节点移动到另一个节点。如果没有做好,这个过程可能会使网络或节点负载过重,降低其他请求的性能。 全自动重新平衡可以很方便,因为正常维护的操作工作较少。但是,这可能是不可预测的。再平衡是一个昂贵的操作,因为它需要重新路由请求并将大量数据从一个节点移动到另一个节点。如果没有做好,这个过程可能会使网络或节点负载过重,降低其他请求的性能。
@ -254,7 +255,7 @@ Cassandra和Ketama使用的第三种方法是使分区数与节点数成正比
现在我们已经将数据集分割到多个机器上运行的多个节点上。但是仍然存在一个悬而未决的问题:当客户想要发出请求时,如何知道要连接哪个节点?随着分区重新平衡,分区对节点的分配也发生变化。为了回答这个问题,需要有人知晓这些变化:如果我想读或写键 “foo”需要连接哪个 IP 地址和端口号? 现在我们已经将数据集分割到多个机器上运行的多个节点上。但是仍然存在一个悬而未决的问题:当客户想要发出请求时,如何知道要连接哪个节点?随着分区重新平衡,分区对节点的分配也发生变化。为了回答这个问题,需要有人知晓这些变化:如果我想读或写键 “foo”需要连接哪个 IP 地址和端口号?
这个问题可以概括为 **服务发现(service discovery)** 它不仅限于数据库。任何可通过网络访问的软件都有这个问题特别是如果它的目标是高可用性在多台机器上运行冗余配置。许多公司已经编写了自己的内部服务发现工具其中许多已经作为开源发布【30】。 这个问题可以概括为 **服务发现service discovery** 它不仅限于数据库。任何可通过网络访问的软件都有这个问题特别是如果它的目标是高可用性在多台机器上运行冗余配置。许多公司已经编写了自己的内部服务发现工具其中许多已经作为开源发布【30】。
概括来说,这个问题有几种不同的方案(如图 6-7 所示): 概括来说,这个问题有几种不同的方案(如图 6-7 所示):
@ -276,7 +277,7 @@ Cassandra和Ketama使用的第三种方法是使分区数与节点数成正比
**图 6-8 使用 ZooKeeper 跟踪分区分配给节点。** **图 6-8 使用 ZooKeeper 跟踪分区分配给节点。**
例如LinkedIn的Espresso使用Helix 【31】进行集群管理依靠ZooKeeper实现了如[图6-8](img/fig6-8.png)所示的路由层。 HBaseSolrCloud和Kafka也使用ZooKeeper来跟踪分区分配。 MongoDB具有类似的体系结构但它依赖于自己的**配置服务器config server** 实现和mongos守护进程作为路由层。 例如LinkedIn的Espresso使用Helix 【31】进行集群管理依靠ZooKeeper实现了如[图6-8](img/fig6-8.png)所示的路由层。 HBaseSolrCloud和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 这样的外部协调服务的依赖。

14
ch7.md
View file

@ -36,7 +36,7 @@
## 事务的棘手概念 ## 事务的棘手概念
现今,几乎所有的关系型数据库和一些非关系数据库都支持**事务**。其中大多数遵循IBM System R第一个SQL数据库在1975年引入的风格【1,2,3】。40年里尽管一些实现细节发生了变化但总体思路大同小异MySQLPostgreSQLOracleSQL 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】。
@ -71,7 +71,7 @@ ACID原子性的定义特征是**能够在错误时中止事务,丢弃该
一致性这个词被赋予太多含义: 一致性这个词被赋予太多含义:
* 在 [第五章](ch5.md) 中,我们讨论了副本一致性,以及异步复制系统中的最终一致性问题(请参阅 “[复制延迟问题](ch5.md#复制延迟问题)”)。 * 在 [第五章](ch5.md) 中,我们讨论了副本一致性,以及异步复制系统中的最终一致性问题(请参阅 “[复制延迟问题](ch5.md#复制延迟问题)”)。
* [一致性哈希Consistency Hashing](ch6.md#一致性哈希))是某些系统用于重新分区的一种分区方法。 * [一致性哈希](ch6.md#一致性哈希) 是某些系统用于重新分区的一种分区方法。
* 在 [CAP 定理](ch9.md#CAP 定理) 中,一致性一词用于表示 [线性一致性](ch9.md#线性一致性)。 * 在 [CAP 定理](ch9.md#CAP 定理) 中,一致性一词用于表示 [线性一致性](ch9.md#线性一致性)。
* 在 ACID 的上下文中,**一致性** 是指数据库在应用程序的特定概念中处于 “良好状态”。 * 在 ACID 的上下文中,**一致性** 是指数据库在应用程序的特定概念中处于 “良好状态”。
@ -83,7 +83,7 @@ ACID一致性的概念是**对数据的一组特定约束必须始终成立**
原子性,隔离性和持久性是数据库的属性,而一致性(在 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】而且那时候大家都不怎么在乎一致性。
#### 隔离性 #### 隔离性
@ -271,7 +271,7 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
#### 实现读已提交 #### 实现读已提交
**读已提交**是一个非常流行的隔离级别。这是Oracle 11gPostgreSQLSQL Server 2012MemSQL和其他许多数据库的默认设置【8】。 **读已提交** 是一个非常流行的隔离级别。这是 Oracle 11g、PostgreSQL、SQL Server 2012、MemSQL 和其他许多数据库的默认设置【8】。
最常见的情况是,数据库通过使用 **行锁row-level lock** 来防止脏写:当事务想要修改特定对象(行或文档)时,它必须首先获得该对象的锁。然后必须持有该锁直到事务被提交或中止。一次只有一个事务可持有任何给定对象的锁;如果另一个事务要写入同一个对象,则必须等到第一个事务提交或中止后,才能获取该锁并继续。这种锁定是读已提交模式(或更强的隔离级别)的数据库自动完成的。 最常见的情况是,数据库通过使用 **行锁row-level lock** 来防止脏写:当事务想要修改特定对象(行或文档)时,它必须首先获得该对象的锁。然后必须持有该锁直到事务被提交或中止。一次只有一个事务可持有任何给定对象的锁;如果另一个事务要写入同一个对象,则必须等到第一个事务提交或中止后,才能获取该锁并继续。这种锁定是读已提交模式(或更强的隔离级别)的数据库自动完成的。
@ -313,7 +313,7 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
快照隔离对长时间运行的只读查询(如备份和分析)非常有用。如果查询的数据在查询执行的同时发生变化,则很难理解查询的含义。当一个事务可以看到数据库在某个特定时间点冻结时的一致快照,理解起来就很容易了。 快照隔离对长时间运行的只读查询(如备份和分析)非常有用。如果查询的数据在查询执行的同时发生变化,则很难理解查询的含义。当一个事务可以看到数据库在某个特定时间点冻结时的一致快照,理解起来就很容易了。
快照隔离是一个流行的功能PostgreSQL使用InnoDB引擎的MySQLOracleSQL Server等都支持【23,31,32】。 快照隔离是一个流行的功能PostgreSQL、使用 InnoDB 引擎的 MySQL、Oracle、SQL Server 等都支持【23,31,32】。
#### 实现快照隔离 #### 实现快照隔离
@ -361,7 +361,7 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
在实践中许多实现细节决定了多版本并发控制的性能。例如如果同一对象的不同版本可以放入同一个页面中PostgreSQL 的优化可以避免更新索引【31】。 在实践中许多实现细节决定了多版本并发控制的性能。例如如果同一对象的不同版本可以放入同一个页面中PostgreSQL 的优化可以避免更新索引【31】。
CouchDBDatomic和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 树;它们只能创建新的树根。但这种方法也需要一个负责压缩和垃圾收集的后台进程。
@ -831,7 +831,7 @@ WHERE room_id = 123 AND
如果没有事务处理,各种错误情况(进程崩溃,网络中断,停电,磁盘已满,意外并发等)意味着数据可能以各种方式变得不一致。例如,非规范化的数据可能很容易与源数据不同步。如果没有事务处理,就很难推断复杂的交互访问可能对数据库造成的影响。 如果没有事务处理,各种错误情况(进程崩溃,网络中断,停电,磁盘已满,意外并发等)意味着数据可能以各种方式变得不一致。例如,非规范化的数据可能很容易与源数据不同步。如果没有事务处理,就很难推断复杂的交互访问可能对数据库造成的影响。
本章深入讨论了**并发控制**的话题。我们讨论了几个广泛使用的隔离级别,特别是**读已提交****快照隔离**(有时称为可重复读)和**可串行化**。并通过研究竞争条件的各种例子,来描述这些隔离等级: 本章深入讨论了 **并发控制** 的话题。我们讨论了几个广泛使用的隔离级别,特别是 **读已提交**、**快照隔离**(有时称为可重复读)和 **可串行化**。并通过研究竞争条件的各种例子,来描述这些隔离等级:
* 脏读 * 脏读

View file

@ -36,13 +36,13 @@
向前相容性可能會更棘手,因為舊版的程式需要忽略新版資料格式中新增的部分。 向前相容性可能會更棘手,因為舊版的程式需要忽略新版資料格式中新增的部分。
本章中將介紹幾種編碼資料的格式,包括 JSONXMLProtocol BuffersThrift和Avro。尤其將關注這些格式如何應對模式變化以及它們如何對新舊程式碼資料需要共存的系統提供支援。然後將討論如何使用這些格式進行資料儲存和通訊在Web服務中**表述性狀態傳遞REST** 和**遠端過程呼叫RPC**,以及**訊息傳遞系統**如Actor和訊息佇列 本章中將介紹幾種編碼資料的格式,包括 JSON、XML、Protocol Buffers、Thrift 和 Avro。尤其將關注這些格式如何應對模式變化以及它們如何對新舊程式碼資料需要共存的系統提供支援。然後將討論如何使用這些格式進行資料儲存和通訊 Web 服務中,**表述性狀態傳遞REST** 和 **遠端過程呼叫RPC**,以及 **訊息傳遞系統**(如 Actor 和訊息佇列)。
## 編碼資料的格式 ## 編碼資料的格式
程式通常(至少)使用兩種形式的資料: 程式通常(至少)使用兩種形式的資料:
1. 在記憶體中,資料儲存在物件,結構體,列表,陣列,散列表,樹等中。 這些資料結構針對CPU的高效訪問和操作進行了最佳化(通常使用指標)。 1. 在記憶體中,資料儲存在物件、結構體、列表、陣列、散列表、樹等中。 這些資料結構針對 CPU 的高效訪問和操作進行了最佳化(通常使用指標)。
2. 如果要將資料寫入檔案,或透過網路傳送,則必須將其 **編碼encode** 為某種自包含的位元組序列例如JSON 文件)。 由於每個程序都有自己獨立的地址空間,一個程序中的指標對任何其他程序都沒有意義,所以這個位元組序列表示會與通常在記憶體中使用的資料結構完全不同 [^i]。 2. 如果要將資料寫入檔案,或透過網路傳送,則必須將其 **編碼encode** 為某種自包含的位元組序列例如JSON 文件)。 由於每個程序都有自己獨立的地址空間,一個程序中的指標對任何其他程序都沒有意義,所以這個位元組序列表示會與通常在記憶體中使用的資料結構完全不同 [^i]。
[^i]: 除一些特殊情況外,例如某些記憶體對映檔案或直接在壓縮資料上操作(如 “[列壓縮](ch4.md#列壓縮)” 中所述)。 [^i]: 除一些特殊情況外,例如某些記憶體對映檔案或直接在壓縮資料上操作(如 “[列壓縮](ch4.md#列壓縮)” 中所述)。
@ -82,13 +82,13 @@ JSONXML和CSV屬於文字格式因此具有人類可讀性儘管它們
* XML 【11】和 JSON 【12】都有可選的模式支援。這些模式語言相當強大所以學習和實現起來都相當複雜。 XML 模式的使用相當普遍,但許多基於 JSON 的工具才不會去折騰模式。對資料的正確解讀(例如區分數值與二進位制串)取決於模式中的資訊,因此不使用 XML/JSON 模式的應用程式可能需要對相應的編碼 / 解碼邏輯進行硬編碼。 * XML 【11】和 JSON 【12】都有可選的模式支援。這些模式語言相當強大所以學習和實現起來都相當複雜。 XML 模式的使用相當普遍,但許多基於 JSON 的工具才不會去折騰模式。對資料的正確解讀(例如區分數值與二進位制串)取決於模式中的資訊,因此不使用 XML/JSON 模式的應用程式可能需要對相應的編碼 / 解碼邏輯進行硬編碼。
* CSV 沒有任何模式,因此每行和每列的含義完全由應用程式自行定義。如果應用程式變更添加了新的行或列,那麼這種變更必須透過手工處理。 CSV 也是一個相當模糊的格式如果一個值包含逗號或換行符會發生什麼。儘管其轉義規則已經被正式指定【13】但並不是所有的解析器都正確的實現了標準。 * CSV 沒有任何模式,因此每行和每列的含義完全由應用程式自行定義。如果應用程式變更添加了新的行或列,那麼這種變更必須透過手工處理。 CSV 也是一個相當模糊的格式如果一個值包含逗號或換行符會發生什麼。儘管其轉義規則已經被正式指定【13】但並不是所有的解析器都正確的實現了標準。
儘管存在這些缺陷,但JSONXML和CSV對很多需求來說已經足夠好了。它們很可能會繼續流行下去,特別是作為資料交換格式來說(即將資料從一個組織傳送到另一個組織)。在這種情況下,只要人們對格式是什麼意見一致,格式有多美觀或者效率有多高效就無所謂了。讓不同的組織就這些東西達成一致的難度超過了絕大多數問題。 儘管存在這些缺陷,但 JSON、XML 和 CSV 對很多需求來說已經足夠好了。它們很可能會繼續流行下去,特別是作為資料交換格式來說(即將資料從一個組織傳送到另一個組織)。在這種情況下,只要人們對格式是什麼意見一致,格式有多美觀或者效率有多高效就無所謂了。讓不同的組織就這些東西達成一致的難度超過了絕大多數問題。
#### 二進位制編碼 #### 二進位制編碼
對於僅在組織內部使用的資料,使用最小公約數式的編碼格式壓力較小。例如,可以選擇更緊湊或更快的解析格式。雖然對小資料集來說,收益可以忽略不計;但一旦達到 TB 級別,資料格式的選型就會產生巨大的影響。 對於僅在組織內部使用的資料,使用最小公約數式的編碼格式壓力較小。例如,可以選擇更緊湊或更快的解析格式。雖然對小資料集來說,收益可以忽略不計;但一旦達到 TB 級別,資料格式的選型就會產生巨大的影響。
JSON比XML簡潔,但與二進位制格式相比還是太佔空間。這一事實導致大量二進位制編碼版本JSONMessagePackBSONBJSONUBJSONBISON和Smile等 和 XML例如WBXML和Fast Infoset的出現。這些格式已經在各種各樣的領域中採用但是沒有一個能像文字版JSON和XML那樣被廣泛採用。 JSON 比 XML 簡潔,但與二進位制格式相比還是太佔空間。這一事實導致大量二進位制編碼版本 JSONMessagePack、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`
@ -105,7 +105,7 @@ 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 裡,那麼它會用另一個不同的型別識別符號,欄位的數量被編碼兩個或四個位元組)。 1. 第一個位元組 `0x83` 表示接下來是 **3** 個欄位(低四位 = `0x03`)的 **物件 object**(高四位 = `0x80`)。 (如果想知道如果一個物件有 15 個以上的欄位會發生什麼情況,欄位的數量塞不進 4 個 bit 裡,那麼它會用另一個不同的型別識別符號,欄位的數量被編碼兩個或四個位元組)。
2. 第二個位元組`0xa8`表示接下來是**8**位元組長(低四位=0x08的字串高四位= 0x0a)。 2. 第二個位元組 `0xa8` 表示接下來是 **8** 位元組長(低四位 = `0x08`)的字串(高四位 = `0x0a`)。
3. 接下來八個位元組是 ASCII 字串形式的欄位名稱 `userName`。由於之前已經指明長度,不需要任何標記來標識字串的結束位置(或者任何轉義)。 3. 接下來八個位元組是 ASCII 字串形式的欄位名稱 `userName`。由於之前已經指明長度,不需要任何標記來標識字串的結束位置(或者任何轉義)。
4. 接下來的七個位元組對字首為 `0xa6` 的六個字母的字串值 `Martin` 進行編碼,依此類推。 4. 接下來的七個位元組對字首為 `0xa6` 的六個字母的字串值 `Martin` 進行編碼,依此類推。
@ -373,7 +373,7 @@ Avro為靜態型別程式語言提供了可選的程式碼生成功能但是
當你需要透過網路進行通訊的程序時,安排該通訊的方式有幾種。最常見的安排是有兩個角色:客戶端和伺服器。伺服器透過網路公開 API並且客戶端可以連線到伺服器以向該 API 發出請求。伺服器公開的 API 被稱為服務。 當你需要透過網路進行通訊的程序時,安排該通訊的方式有幾種。最常見的安排是有兩個角色:客戶端和伺服器。伺服器透過網路公開 API並且客戶端可以連線到伺服器以向該 API 發出請求。伺服器公開的 API 被稱為服務。
Web以這種方式工作客戶Web瀏覽器向Web伺服器發出請求透過GET請求下載HTMLCSSJavaScript影象等並透過POST請求提交資料到伺服器。 API包含一組標準的協議和資料格式HTTPURLSSL/TLSHTML等。由於網路瀏覽器網路伺服器和網站作者大多同意這些標準,你可以使用任何網路瀏覽器訪問任何網站(至少在理論上!)。 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 的細節達成一致。
@ -443,7 +443,7 @@ Web服務僅僅是透過網路進行API請求的一系列技術的最新版本
RPC 方案的前後向相容性屬性從它使用的編碼方式中繼承: RPC 方案的前後向相容性屬性從它使用的編碼方式中繼承:
* ThriftgRPCProtobuf和Avro RPC可以根據相應編碼格式的相容性規則進行演變。 * Thrift、gRPCProtobuf和 Avro RPC 可以根據相應編碼格式的相容性規則進行演變。
* 在 SOAP 中,請求和響應是使用 XML 模式指定的。這些可以演變但有一些微妙的陷阱【47】。 * 在 SOAP 中,請求和響應是使用 XML 模式指定的。這些可以演變但有一些微妙的陷阱【47】。
* RESTful API 通常使用 JSON沒有正式指定的模式用於響應以及用於請求的 JSON 或 URI 編碼 / 表單編碼的請求引數。新增可選的請求引數並向響應物件新增新的欄位通常被認為是保持相容性的改變。 * RESTful API 通常使用 JSON沒有正式指定的模式用於響應以及用於請求的 JSON 或 URI 編碼 / 表單編碼的請求引數。新增可選的請求引數並向響應物件新增新的欄位通常被認為是保持相容性的改變。
@ -481,7 +481,7 @@ RPC方案的前後向相容性屬性從它使用的編碼方式中繼承
#### 分散式的Actor框架 #### 分散式的Actor框架
Actor模型是單個程序中併發的程式設計模型。邏輯被封裝在actor中而不是直接處理執行緒以及競爭條件鎖定和死鎖的相關問題。每個actor通常代表一個客戶或實體,它可能有一些本地狀態(不與其他任何角色共享),它透過傳送和接收非同步訊息與其他角色通訊。不保證訊息傳送:在某些錯誤情況下,訊息將丟失。由於每個角色一次只能處理一條訊息,因此不需要擔心執行緒,每個角色可以由框架獨立排程。 Actor 模型是單個程序中併發的程式設計模型。邏輯被封裝在 actor 中,而不是直接處理執行緒(以及競爭條件、鎖定和死鎖的相關問題)。每個 actor 通常代表一個客戶或實體,它可能有一些本地狀態(不與其他任何角色共享),它透過傳送和接收非同步訊息與其他角色通訊。不保證訊息傳送:在某些錯誤情況下,訊息將丟失。由於每個角色一次只能處理一條訊息,因此不需要擔心執行緒,每個角色可以由框架獨立排程。
在分散式 Actor 框架中,此程式設計模型用於跨多個節點伸縮應用程式。不管傳送方和接收方是在同一個節點上還是在不同的節點上,都使用相同的訊息傳遞機制。如果它們在不同的節點上,則該訊息被透明地編碼成位元組序列,透過網路傳送,並在另一側解碼。 在分散式 Actor 框架中,此程式設計模型用於跨多個節點伸縮應用程式。不管傳送方和接收方是在同一個節點上還是在不同的節點上,都使用相同的訊息傳遞機制。如果它們在不同的節點上,則該訊息被透明地編碼成位元組序列,透過網路傳送,並在另一側解碼。
@ -492,7 +492,7 @@ Actor模型是單個程序中併發的程式設計模型。邏輯被封裝在act
三個流行的分散式 actor 框架處理訊息編碼如下: 三個流行的分散式 actor 框架處理訊息編碼如下:
* 預設情況下Akka 使用 Java 的內建序列化,不提供前向或後向相容性。 但是,你可以用類似 Prototol Buffers 的東西替代它從而獲得滾動升級的能力【50】。 * 預設情況下Akka 使用 Java 的內建序列化,不提供前向或後向相容性。 但是,你可以用類似 Prototol Buffers 的東西替代它從而獲得滾動升級的能力【50】。
* Orleans 預設使用不支援滾動升級部署的自定義資料編碼格式; 要部署新版本的應用程式你需要設定一個新的叢集將流量從舊叢集遷移到新叢集然後關閉舊叢集【51,52】。 像Akka一樣可以使用自定義序列化外掛。 * Orleans 預設使用不支援滾動升級部署的自定義資料編碼格式要部署新版本的應用程式你需要設定一個新的叢集將流量從舊叢集遷移到新叢集然後關閉舊叢集【51,52】。 像 Akka 一樣,可以使用自定義序列化外掛。
* 在 Erlang OTP 中,對記錄模式進行更改是非常困難的(儘管系統具有許多為高可用性設計的功能)。 滾動升級是可能的但需要仔細計劃【53】。 一個新的實驗性的 `maps` 資料型別2014 年在 Erlang R17 中引入的類似於 JSON 的結構可能使得這個資料型別在未來更容易【54】。 * 在 Erlang OTP 中,對記錄模式進行更改是非常困難的(儘管系統具有許多為高可用性設計的功能)。 滾動升級是可能的但需要仔細計劃【53】。 一個新的實驗性的 `maps` 資料型別2014 年在 Erlang R17 中引入的類似於 JSON 的結構可能使得這個資料型別在未來更容易【54】。

View file

@ -2,9 +2,9 @@
![](../img/ch5.png) ![](../img/ch5.png)
> 與可能出錯的東西比,'不可能'出錯的東西最顯著的特點就是:一旦真的出錯,通常就徹底玩完了。 > 與可能出錯的東西比,“不可能”出錯的東西最顯著的特點就是:一旦真的出錯,通常就徹底玩完了。
> >
> —— 道格拉斯·亞當斯1992 > —— 道格拉斯亞當斯1992
------ ------
@ -134,7 +134,7 @@
* 任何呼叫 **非確定性函式nondeterministic** 的語句,可能會在每個副本上生成不同的值。例如,使用 `NOW()` 獲取當前日期時間,或使用 `RAND()` 獲取一個隨機數。 * 任何呼叫 **非確定性函式nondeterministic** 的語句,可能會在每個副本上生成不同的值。例如,使用 `NOW()` 獲取當前日期時間,或使用 `RAND()` 獲取一個隨機數。
* 如果語句使用了 **自增列auto increment**,或者依賴於資料庫中的現有資料(例如,`UPDATE ... WHERE <某些條件>`),則必須在每個副本上按照完全相同的順序執行它們,否則可能會產生不同的效果。當有多個併發執行的事務時,這可能成為一個限制。 * 如果語句使用了 **自增列auto increment**,或者依賴於資料庫中的現有資料(例如,`UPDATE ... WHERE <某些條件>`),則必須在每個副本上按照完全相同的順序執行它們,否則可能會產生不同的效果。當有多個併發執行的事務時,這可能成為一個限制。
* 有副作用的語句(例如,觸發器,儲存過程,使用者定義的函式)可能會在每個副本上產生不同的副作用,除非副作用是絕對確定的。 * 有副作用的語句(例如:觸發器、儲存過程、使用者定義的函式)可能會在每個副本上產生不同的副作用,除非副作用是絕對確定的。
的確有辦法繞開這些問題 —— 例如,當語句被記錄時,主庫可以用固定的返回值替換任何不確定的函式呼叫,以便從庫獲得相同的值。但是由於邊緣情況實在太多了,現在通常會選擇其他的複製方法。 的確有辦法繞開這些問題 —— 例如,當語句被記錄時,主庫可以用固定的返回值替換任何不確定的函式呼叫,以便從庫獲得相同的值。但是由於邊緣情況實在太多了,現在通常會選擇其他的複製方法。
@ -192,7 +192,7 @@ PostgreSQL和Oracle等使用這種複製方法【16】。主要缺點是日誌
不幸的是,當應用程式從非同步從庫讀取時,如果從庫落後,它可能會看到過時的資訊。這會導致資料庫中出現明顯的不一致:同時對主庫和從庫執行相同的查詢,可能得到不同的結果,因為並非所有的寫入都反映在從庫中。這種不一致只是一個暫時的狀態 —— 如果停止寫入資料庫並等待一段時間,從庫最終會趕上並與主庫保持一致。出於這個原因,這種效應被稱為 **最終一致性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**,即寫入主庫到反映至從庫之間的延遲,可能僅僅是幾分之一秒,在實踐中並不顯眼。但如果系統在接近極限的情況下執行,或網路中存在問題,延遲可以輕而易舉地超過幾秒,甚至幾分鐘。

View file

@ -34,7 +34,8 @@
分割槽通常與複製結合使用,使得每個分割槽的副本儲存在多個節點上。這意味著,即使每條記錄屬於一個分割槽,它仍然可以儲存在多個不同的節點上以獲得容錯能力。 分割槽通常與複製結合使用,使得每個分割槽的副本儲存在多個節點上。這意味著,即使每條記錄屬於一個分割槽,它仍然可以儲存在多個不同的節點上以獲得容錯能力。
一個節點可能儲存多個分割槽。 如果使用主從複製模型,則分割槽和複製的組合如[圖6-1](../img/fig6-1.png)所示。 每個分割槽領導者(主)被分配給一個節點,追隨者(從)被分配給其他節點。 每個節點可能是某些分割槽的領導者,同時是其他分割槽的追隨者。 一個節點可能儲存多個分割槽。如果使用主從複製模型,則分割槽和複製的組合如 [圖 6-1](../img/fig6-1.png) 所示。每個分割槽領導者(主)被分配給一個節點,追隨者(從)被分配給其他節點。 每個節點可能是某些分割槽的領導者,同時是其他分割槽的追隨者。
我們在 [第五章](ch5.md) 討論的關於資料庫複製的所有內容同樣適用於分割槽的複製。大多數情況下,分割槽方案的選擇與複製方案的選擇是獨立的,為簡單起見,本章中將忽略複製。 我們在 [第五章](ch5.md) 討論的關於資料庫複製的所有內容同樣適用於分割槽的複製。大多數情況下,分割槽方案的選擇與複製方案的選擇是獨立的,為簡單起見,本章中將忽略複製。
![](../img/fig6-1.png) ![](../img/fig6-1.png)
@ -61,7 +62,7 @@
**圖 6-2 印刷版百科全書按照關鍵字範圍進行分割槽** **圖 6-2 印刷版百科全書按照關鍵字範圍進行分割槽**
鍵的範圍不一定均勻分佈,因為資料也很可能不均勻分佈。例如在[圖6-2](../img/fig6-2.png)中第1捲包含以A和B開頭的單詞但第12卷則包含以TUVXY和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】。
@ -75,7 +76,7 @@
由於偏斜和熱點的風險,許多分散式資料儲存使用雜湊函式來確定給定鍵的分割槽。 由於偏斜和熱點的風險,許多分散式資料儲存使用雜湊函式來確定給定鍵的分割槽。
一個好的雜湊函式可以將偏斜的資料均勻分佈。假設你有一個32位雜湊函式,無論何時給定一個新的字串輸入它將返回一個0到$2^{32}$ -1之間的“隨機”數。即使輸入的字串非常相似,它們的雜湊也會均勻分佈在這個數字範圍內。 一個好的雜湊函式可以將偏斜的資料均勻分佈。假設你有一個 32 位雜湊函式,無論何時給定一個新的字串輸入,它將返回一個 0 到 $2^{32}$ -1 之間的 “隨機” 數。即使輸入的字串非常相似,它們的雜湊也會均勻分佈在這個數字範圍內。
出於分割槽的目的雜湊函式不需要多麼強壯的加密演算法例如Cassandra 和 MongoDB 使用 MD5Voldemort 使用 Fowler-Noll-Vo 函式。許多程式語言都有內建的簡單雜湊函式(它們用於散列表),但是它們可能不適合分割槽:例如,在 Java 的 `Object.hashCode()` 和 Ruby 的 `Object#hash`同一個鍵可能在不同的程序中有不同的雜湊值【6】。 出於分割槽的目的雜湊函式不需要多麼強壯的加密演算法例如Cassandra 和 MongoDB 使用 MD5Voldemort 使用 Fowler-Noll-Vo 函式。許多程式語言都有內建的簡單雜湊函式(它們用於散列表),但是它們可能不適合分割槽:例如,在 Java 的 `Object.hashCode()` 和 Ruby 的 `Object#hash`同一個鍵可能在不同的程序中有不同的雜湊值【6】。
@ -105,7 +106,7 @@ Cassandra採取了折衷的策略【11, 12, 13】。 Cassandra中的表可以使
這種場景也許並不常見但並非聞所未聞例如在社交媒體網站上一個擁有數百萬追隨者的名人使用者在做某事時可能會引發一場風暴【14】。這個事件可能導致同一個鍵的大量寫入鍵可能是名人的使用者 ID或者人們正在評論的動作的 ID。雜湊策略不起作用因為兩個相同 ID 的雜湊值仍然是相同的。 這種場景也許並不常見但並非聞所未聞例如在社交媒體網站上一個擁有數百萬追隨者的名人使用者在做某事時可能會引發一場風暴【14】。這個事件可能導致同一個鍵的大量寫入鍵可能是名人的使用者 ID或者人們正在評論的動作的 ID。雜湊策略不起作用因為兩個相同 ID 的雜湊值仍然是相同的。
如今,大多數資料系統無法自動補償這種高度偏斜的負載,因此應用程式有責任減少偏斜。例如,如果一個主鍵被認為是非常火爆的,一個簡單的方法是在主鍵的開始或結尾新增一個隨機數。只要一個兩位數的十進位制隨機數就可以將主鍵分散為100種不同的主鍵,從而儲存在不同的分割槽中。 如今,大多數資料系統無法自動補償這種高度偏斜的負載,因此應用程式有責任減少偏斜。例如,如果一個主鍵被認為是非常火爆的,一個簡單的方法是在主鍵的開始或結尾新增一個隨機數。只要一個兩位數的十進位制隨機數就可以將主鍵分散為 100 種不同的主鍵,從而儲存在不同的分割槽中。
然而,將主鍵進行分割之後,任何讀取都必須要做額外的工作,因為他們必須從所有 100 個主鍵分佈中讀取資料並將其合併。此技術還需要額外的記錄:只需要對少量熱點附加隨機數;對於寫入吞吐量低的絕大多數主鍵來說是不必要的開銷。因此,你還需要一些方法來跟蹤哪些鍵需要被分割。 然而,將主鍵進行分割之後,任何讀取都必須要做額外的工作,因為他們必須從所有 100 個主鍵分佈中讀取資料並將其合併。此技術還需要額外的記錄:只需要對少量熱點附加隨機數;對於寫入吞吐量低的絕大多數主鍵來說是不必要的開銷。因此,你還需要一些方法來跟蹤哪些鍵需要被分割。
@ -153,7 +154,7 @@ Cassandra採取了折衷的策略【11, 12, 13】。 Cassandra中的表可以使
**圖 6-5 基於關鍵詞對次級索引進行分割槽** **圖 6-5 基於關鍵詞對次級索引進行分割槽**
我們將這種索引稱為**關鍵詞分割槽term-partitioned**,因為我們尋找的關鍵詞決定了索引的分割槽方式。例如,一個關鍵詞可能是:`color:red`。**關鍵詞(Term)** 這個名稱來源於全文搜尋索引(一種特殊的次級索引),指文件中出現的所有單詞。 我們將這種索引稱為 **關鍵詞分割槽term-partitioned**,因為我們尋找的關鍵詞決定了索引的分割槽方式。例如,一個關鍵詞可能是:`color:red`。**關鍵詞Term** 這個名稱來源於全文搜尋索引(一種特殊的次級索引),指文件中出現的所有單詞。
和之前一樣,我們可以透過 **關鍵詞** 本身或者它的雜湊進行索引分割槽。根據關鍵詞本身來分割槽對於範圍掃描非常有用(例如對於數值類的屬性,像汽車的報價),而對關鍵詞的雜湊分割槽提供了負載均衡的能力。 和之前一樣,我們可以透過 **關鍵詞** 本身或者它的雜湊進行索引分割槽。根據關鍵詞本身來分割槽對於範圍掃描非常有用(例如對於數值類的屬性,像汽車的報價),而對關鍵詞的雜湊分割槽提供了負載均衡的能力。
@ -184,7 +185,7 @@ Cassandra採取了折衷的策略【11, 12, 13】。 Cassandra中的表可以使
### 再平衡策略 ### 再平衡策略
有幾種不同的分割槽分配方法【23】,讓我們依次簡要討論一下。 有幾種不同的分割槽分配方法【23】讓我們依次簡要討論一下。
#### 反面教材hash mod N #### 反面教材hash mod N
@ -208,7 +209,7 @@ Cassandra採取了折衷的策略【11, 12, 13】。 Cassandra中的表可以使
**圖 6-6 將新節點新增到每個節點具有多個分割槽的資料庫叢集。** **圖 6-6 將新節點新增到每個節點具有多個分割槽的資料庫叢集。**
原則上,你甚至可以解決叢集中的硬體不匹配問題:透過為更強大的節點分配更多的分割槽,可以強制這些節點承載更多的負載。在Riak 【15】Elasticsearch 【24】Couchbase 【10】和Voldemort 【25】中使用了這種再平衡的方法。 原則上,你甚至可以解決叢集中的硬體不匹配問題:透過為更強大的節點分配更多的分割槽,可以強制這些節點承載更多的負載。在 Riak 【15】、Elasticsearch 【24】、Couchbase 【10】和 Voldemort 【25】中使用了這種再平衡的方法。
在這種配置中,分割槽的數量通常在資料庫第一次建立時確定,之後不會改變。雖然原則上可以分割和合並分割槽(請參閱下一節),但固定數量的分割槽在操作上更簡單,因此許多固定分割槽資料庫選擇不實施分割槽分割。因此,一開始配置的分割槽數就是你可以擁有的最大節點數量,所以你需要選擇足夠多的分割槽以適應未來的增長。但是,每個分割槽也有管理開銷,所以選擇太大的數字會適得其反。 在這種配置中,分割槽的數量通常在資料庫第一次建立時確定,之後不會改變。雖然原則上可以分割和合並分割槽(請參閱下一節),但固定數量的分割槽在操作上更簡單,因此許多固定分割槽資料庫選擇不實施分割槽分割。因此,一開始配置的分割槽數就是你可以擁有的最大節點數量,所以你需要選擇足夠多的分割槽以適應未來的增長。但是,每個分割槽也有管理開銷,所以選擇太大的數字會適得其反。
@ -242,7 +243,7 @@ Cassandra和Ketama使用的第三種方法是使分割槽數與節點數成正
關於再平衡有一個重要問題:自動還是手動進行? 關於再平衡有一個重要問題:自動還是手動進行?
在全自動重新平衡系統自動決定何時將分割槽從一個節點移動到另一個節點無須人工干預和完全手動分割槽指派給節點由管理員明確配置僅在管理員明確重新配置時才會更改之間有一個權衡。例如CouchbaseRiak和Voldemort會自動生成建議的分割槽分配,但需要管理員提交才能生效。 在全自動重新平衡系統自動決定何時將分割槽從一個節點移動到另一個節點無須人工干預和完全手動分割槽指派給節點由管理員明確配置僅在管理員明確重新配置時才會更改之間有一個權衡。例如Couchbase、Riak 和 Voldemort 會自動生成建議的分割槽分配,但需要管理員提交才能生效。
全自動重新平衡可以很方便,因為正常維護的操作工作較少。但是,這可能是不可預測的。再平衡是一個昂貴的操作,因為它需要重新路由請求並將大量資料從一個節點移動到另一個節點。如果沒有做好,這個過程可能會使網路或節點負載過重,降低其他請求的效能。 全自動重新平衡可以很方便,因為正常維護的操作工作較少。但是,這可能是不可預測的。再平衡是一個昂貴的操作,因為它需要重新路由請求並將大量資料從一個節點移動到另一個節點。如果沒有做好,這個過程可能會使網路或節點負載過重,降低其他請求的效能。
@ -254,7 +255,7 @@ Cassandra和Ketama使用的第三種方法是使分割槽數與節點數成正
現在我們已經將資料集分割到多個機器上執行的多個節點上。但是仍然存在一個懸而未決的問題:當客戶想要發出請求時,如何知道要連線哪個節點?隨著分割槽重新平衡,分割槽對節點的分配也發生變化。為了回答這個問題,需要有人知曉這些變化:如果我想讀或寫鍵 “foo”需要連線哪個 IP 地址和埠號? 現在我們已經將資料集分割到多個機器上執行的多個節點上。但是仍然存在一個懸而未決的問題:當客戶想要發出請求時,如何知道要連線哪個節點?隨著分割槽重新平衡,分割槽對節點的分配也發生變化。為了回答這個問題,需要有人知曉這些變化:如果我想讀或寫鍵 “foo”需要連線哪個 IP 地址和埠號?
這個問題可以概括為 **服務發現(service discovery)** 它不僅限於資料庫。任何可透過網路訪問的軟體都有這個問題特別是如果它的目標是高可用性在多臺機器上執行冗餘配置。許多公司已經編寫了自己的內部服務發現工具其中許多已經作為開源釋出【30】。 這個問題可以概括為 **服務發現service discovery** 它不僅限於資料庫。任何可透過網路訪問的軟體都有這個問題特別是如果它的目標是高可用性在多臺機器上執行冗餘配置。許多公司已經編寫了自己的內部服務發現工具其中許多已經作為開源釋出【30】。
概括來說,這個問題有幾種不同的方案(如圖 6-7 所示): 概括來說,這個問題有幾種不同的方案(如圖 6-7 所示):
@ -276,7 +277,7 @@ Cassandra和Ketama使用的第三種方法是使分割槽數與節點數成正
**圖 6-8 使用 ZooKeeper 跟蹤分割槽分配給節點。** **圖 6-8 使用 ZooKeeper 跟蹤分割槽分配給節點。**
例如LinkedIn的Espresso使用Helix 【31】進行叢集管理依靠ZooKeeper實現瞭如[圖6-8](../img/fig6-8.png)所示的路由層。 HBaseSolrCloud和Kafka也使用ZooKeeper來跟蹤分割槽分配。 MongoDB具有類似的體系結構但它依賴於自己的**配置伺服器config server** 實現和mongos守護程序作為路由層。 例如LinkedIn的Espresso使用Helix 【31】進行叢集管理依靠ZooKeeper實現瞭如[圖6-8](../img/fig6-8.png)所示的路由層。 HBaseSolrCloud和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 這樣的外部協調服務的依賴。

View file

@ -36,7 +36,7 @@
## 事務的棘手概念 ## 事務的棘手概念
現今,幾乎所有的關係型資料庫和一些非關係資料庫都支援**事務**。其中大多數遵循IBM System R第一個SQL資料庫在1975年引入的風格【1,2,3】。40年裡儘管一些實現細節發生了變化但總體思路大同小異MySQLPostgreSQLOracleSQL 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】。
@ -71,7 +71,7 @@ ACID原子性的定義特徵是**能夠在錯誤時中止事務,丟棄該
一致性這個詞被賦予太多含義: 一致性這個詞被賦予太多含義:
* 在 [第五章](ch5.md) 中,我們討論了副本一致性,以及非同步複製系統中的最終一致性問題(請參閱 “[複製延遲問題](ch5.md#複製延遲問題)”)。 * 在 [第五章](ch5.md) 中,我們討論了副本一致性,以及非同步複製系統中的最終一致性問題(請參閱 “[複製延遲問題](ch5.md#複製延遲問題)”)。
* [一致性雜湊Consistency Hashing](ch6.md#一致性雜湊))是某些系統用於重新分割槽的一種分割槽方法。 * [一致性雜湊](ch6.md#一致性雜湊) 是某些系統用於重新分割槽的一種分割槽方法。
* 在 [CAP 定理](ch9.md#CAP 定理) 中,一致性一詞用於表示 [線性一致性](ch9.md#線性一致性)。 * 在 [CAP 定理](ch9.md#CAP 定理) 中,一致性一詞用於表示 [線性一致性](ch9.md#線性一致性)。
* 在 ACID 的上下文中,**一致性** 是指資料庫在應用程式的特定概念中處於 “良好狀態”。 * 在 ACID 的上下文中,**一致性** 是指資料庫在應用程式的特定概念中處於 “良好狀態”。
@ -83,7 +83,7 @@ ACID一致性的概念是**對資料的一組特定約束必須始終成立**
原子性,隔離性和永續性是資料庫的屬性,而一致性(在 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】而且那時候大家都不怎麼在乎一致性。
#### 隔離性 #### 隔離性
@ -271,7 +271,7 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
#### 實現讀已提交 #### 實現讀已提交
**讀已提交**是一個非常流行的隔離級別。這是Oracle 11gPostgreSQLSQL Server 2012MemSQL和其他許多資料庫的預設設定【8】。 **讀已提交** 是一個非常流行的隔離級別。這是 Oracle 11g、PostgreSQL、SQL Server 2012、MemSQL 和其他許多資料庫的預設設定【8】。
最常見的情況是,資料庫透過使用 **行鎖row-level lock** 來防止髒寫:當事務想要修改特定物件(行或文件)時,它必須首先獲得該物件的鎖。然後必須持有該鎖直到事務被提交或中止。一次只有一個事務可持有任何給定物件的鎖;如果另一個事務要寫入同一個物件,則必須等到第一個事務提交或中止後,才能獲取該鎖並繼續。這種鎖定是讀已提交模式(或更強的隔離級別)的資料庫自動完成的。 最常見的情況是,資料庫透過使用 **行鎖row-level lock** 來防止髒寫:當事務想要修改特定物件(行或文件)時,它必須首先獲得該物件的鎖。然後必須持有該鎖直到事務被提交或中止。一次只有一個事務可持有任何給定物件的鎖;如果另一個事務要寫入同一個物件,則必須等到第一個事務提交或中止後,才能獲取該鎖並繼續。這種鎖定是讀已提交模式(或更強的隔離級別)的資料庫自動完成的。
@ -313,7 +313,7 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
快照隔離對長時間執行的只讀查詢(如備份和分析)非常有用。如果查詢的資料在查詢執行的同時發生變化,則很難理解查詢的含義。當一個事務可以看到資料庫在某個特定時間點凍結時的一致快照,理解起來就很容易了。 快照隔離對長時間執行的只讀查詢(如備份和分析)非常有用。如果查詢的資料在查詢執行的同時發生變化,則很難理解查詢的含義。當一個事務可以看到資料庫在某個特定時間點凍結時的一致快照,理解起來就很容易了。
快照隔離是一個流行的功能PostgreSQL使用InnoDB引擎的MySQLOracleSQL Server等都支援【23,31,32】。 快照隔離是一個流行的功能PostgreSQL、使用 InnoDB 引擎的 MySQL、Oracle、SQL Server 等都支援【23,31,32】。
#### 實現快照隔離 #### 實現快照隔離
@ -361,7 +361,7 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
在實踐中許多實現細節決定了多版本併發控制的效能。例如如果同一物件的不同版本可以放入同一個頁面中PostgreSQL 的最佳化可以避免更新索引【31】。 在實踐中許多實現細節決定了多版本併發控制的效能。例如如果同一物件的不同版本可以放入同一個頁面中PostgreSQL 的最佳化可以避免更新索引【31】。
CouchDBDatomic和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 樹;它們只能建立新的樹根。但這種方法也需要一個負責壓縮和垃圾收集的後臺程序。
@ -831,7 +831,7 @@ WHERE room_id = 123 AND
如果沒有事務處理,各種錯誤情況(程序崩潰,網路中斷,停電,磁碟已滿,意外併發等)意味著資料可能以各種方式變得不一致。例如,非規範化的資料可能很容易與源資料不同步。如果沒有事務處理,就很難推斷複雜的互動訪問可能對資料庫造成的影響。 如果沒有事務處理,各種錯誤情況(程序崩潰,網路中斷,停電,磁碟已滿,意外併發等)意味著資料可能以各種方式變得不一致。例如,非規範化的資料可能很容易與源資料不同步。如果沒有事務處理,就很難推斷複雜的互動訪問可能對資料庫造成的影響。
本章深入討論了**併發控制**的話題。我們討論了幾個廣泛使用的隔離級別,特別是**讀已提交****快照隔離**(有時稱為可重複讀)和**可序列化**。並透過研究競爭條件的各種例子,來描述這些隔離等級: 本章深入討論了 **併發控制** 的話題。我們討論了幾個廣泛使用的隔離級別,特別是 **讀已提交**、**快照隔離**(有時稱為可重複讀)和 **可序列化**。並透過研究競爭條件的各種例子,來描述這些隔離等級:
* 髒讀 * 髒讀