修复3,4,5,6章的格式

This commit is contained in:
Vonng 2018-03-07 01:56:27 +08:00
parent 854474e9bc
commit 8811de70b2
5 changed files with 371 additions and 418 deletions

View File

@ -83,8 +83,8 @@
| 序言 | [preface.md](preface.md) | 机翻 | | 序言 | [preface.md](preface.md) | 机翻 |
| 第一部分:数据系统基础 ——概览 | [part-i.md](part-i.md) | 初翻 | | 第一部分:数据系统基础 ——概览 | [part-i.md](part-i.md) | 初翻 |
| 第一章:可靠性、可扩展性、可维护性 | [ch1.md](ch1.md) | **精翻** | | 第一章:可靠性、可扩展性、可维护性 | [ch1.md](ch1.md) | **精翻** |
| 第二章:数据模型与查询语言 | [ch2.md](ch2.md) | 初翻 | | 第二章:数据模型与查询语言 | [ch2.md](ch2.md) | 初翻 50% |
| 第三章:存储与检索 | [ch3.md](ch3.md) | 初翻 | | 第三章:存储与检索 | [ch3.md](ch3.md) | 初翻 60% |
| 第四章:编码与演化 | [ch4.md](ch4.md) | 初翻 | | 第四章:编码与演化 | [ch4.md](ch4.md) | 初翻 |
| 第二部分:分布式数据——概览 | [part-ii.md](part-ii.md) | 初翻 | | 第二部分:分布式数据——概览 | [part-ii.md](part-ii.md) | 初翻 |
| 第五章:复制 | [ch5.md](ch5.md) | 初翻 | | 第五章:复制 | [ch5.md](ch5.md) | 初翻 |

View File

@ -11,15 +11,13 @@
[TOC] [TOC]
数据模型可能是开发软件最重要的部分,因为它们有着深远的影响:不仅影响软件的编写方式,而且会影响我们的解题思路。
数据模型可能是开发软件最重要的部分,因为它们有着深远的影响:不仅在于软件的编写方式,而且在于它如何影响对问题解决方案的思考。
大多数应用程序是通过将一个数据模型叠加在另一个之上来构建的。对于每一层,关键问题是:它是如何用下一层来表示的?例如: 大多数应用程序是通过将一个数据模型叠加在另一个之上来构建的。对于每一层,关键问题是:它是如何用下一层来表示的?例如:
1. 作为一名应用程序开发人员您将看到现实世界包括人员组织货物行为资金流向传感器等并根据对象或数据结构以及API进行建模操纵这些数据结构。这些结构通常是应用程序特定的。 1. 作为一名应用程序开发人员您将看到现实世界包括人员组织货物行为资金流向传感器等并根据对象或数据结构以及API进行建模操纵这些数据结构。这些结构通常是应用程序特定的。
2. 如果要存储这些数据结构可以使用通用数据模型如JSON或XML文档关系数据库中的表、或图模型来表示它们。 2. 如果要存储这些数据结构可以使用通用数据模型如JSON或XML文档关系数据库中的表、或图模型来表示它们。
3. 构建数据库软件的工程师决定以内存磁盘或网络上的字节表示JSON / XML /关系/图数据。该表示可以允许以各种方式查询,搜索,操纵和处理数据。 3. 构建数据库软件的工程师决定以内存磁盘或网络上的字节表示JSON/XML/关系/图数据。该表示可以允许以各种方式查询,搜索,操纵和处理数据。
4. 在更低的层面上,硬件工程师已经计算出如何用电流,光脉冲,磁场等来表示字节。 4. 在更低的层面上,硬件工程师已经计算出如何用电流,光脉冲,磁场等来表示字节。
在一个复杂的应用程序中可能会有更多的中间层次比如基于API的API但是基本思想仍然是一样的每个层都通过提供一个干净的数据模型来隐藏下面层的复杂性。这些抽象允许不同的人群例如数据库供应商的工程师和使用他们的数据库的应用程序开发人员有效地协作。 在一个复杂的应用程序中可能会有更多的中间层次比如基于API的API但是基本思想仍然是一样的每个层都通过提供一个干净的数据模型来隐藏下面层的复杂性。这些抽象允许不同的人群例如数据库供应商的工程师和使用他们的数据库的应用程序开发人员有效地协作。
@ -34,21 +32,21 @@
## 关系模型与文档模型 ## 关系模型与文档模型
现在最着名的数据模型可能是SQL它基于Edgar Codd在1970年提出的关系模型[1]数据被组织到关系中称为SQL表其中每个关系是元组的无序集合SQL中的行 现在最着名的数据模型可能是SQL它基于Edgar Codd在1970年提出的关系模型【1】数据被组织到关系中称为SQL表其中每个关系是元组的无序集合SQL中的行
关系模型是一个理论上的建议,当时很多人怀疑是否能够有效实施。然而,到了20世纪80年代中期关系数据库管理系统RDBMSes和SQL已成为大多数需要存储和查询具有某种规模结构的数据的人们的首选工具。关系数据库的优势已经持续了大约25 - 30年 - 计算历史上的一个永恒。 关系模型是一个理论上的提议,当时很多人都怀疑是否能够有效实现。然而到了20世纪80年代中期关系数据库管理系统RDBMSes和SQL已成为大多数需要存储和查询具有某种规模结构的数据的人们的首选工具。关系数据库的优势已经持续了大约25~30年——计算史中的永恒。
关系数据库的根源在于商业数据处理这是在20世纪60年代和70年代在大型计算机上进行的。从今天的角度来看用例显得很平常通常是交易处理进入销售或银行交易航空公司预订仓库库存和批处理客户发票工资单报告 关系数据库起源于商业数据处理这是在20世纪60年代和70年代在大型计算机上进行的。从今天的角度来看用例显得很平常通常是交易处理进入销售或银行交易航空公司预订仓库库存和批处理客户发票工资单报告
当时的其他数据库迫使应用程序开发人员考虑数据库内部的数据表示。关系模型的目标是将实现细节隐藏在更简洁的界面之后。 当时的其他数据库迫使应用程序开发人员考虑数据库内部的数据表示。关系模型的目标是将实现细节隐藏在更简洁的界面之后。
多年来在数据存储和查询方面存在着许多相互竞争的方法。在20世纪70年代和80年代初网络模型和分层模型是主要的选择但关系模型占据了主导地位。对象数据库在二十世纪八十年代末和九十年代初再次出现。 XML数据库出现在二十一世纪初但只有小众采用。关系模型的每个竞争者都在其时代产生了大量的炒作但从来没有持续[2] 多年来在数据存储和查询方面存在着许多相互竞争的方法。在20世纪70年代和80年代初网络模型和分层模型是主要的选择但关系模型占据了主导地位。对象数据库在二十世纪八十年代末和九十年代初再次出现。 XML数据库出现在二十一世纪初但只有小众采用。关系模型的每个竞争者都在其时代产生了大量的炒作但从来没有持续【2】
随着电脑越来越强大和联网,它们开始被用于日益多样化的目的。值得注意的是,关系数据库在业务数据处理的原始范围之外被推广到很广泛的用例。您今天在网上看到的大部分内容仍然是由关系数据库提供支持,无论是在线发布,讨论,社交网络,电子商务,游戏,软件即服务生产力应用程序等等。 随着电脑越来越强大和联网,它们开始被用于日益多样化的目的。值得注意的是,关系数据库在业务数据处理的原始范围之外被推广到很广泛的用例。您今天在网上看到的大部分内容仍然是由关系数据库提供支持,无论是在线发布,讨论,社交网络,电子商务,游戏,软件即服务生产力应用程序等等。
### NoSQL的诞生 ### NoSQL的诞生
现在在2010年代NoSQL是推翻关系模式主导地位的最新尝试。 “NoSQL”这个名字是不幸的因为它实际上并没有涉及到任何特定的技术它最初只是作为一个吸引人的Twitter标签在2009年的开源,分布式,非关系数据库上聚会。无论如何,这个术语触动了一个神经,并迅速通过网络启动社区和更远的地方传播开来。一些有趣的数据库系统现在与#NoSQL#标签相关联并被追溯性地重新解释为不仅是SQL [4] 现在在2010年代NoSQL是推翻关系模式主导地位的最新尝试。 “NoSQL”这个名字非常不幸因为它实际上并没有涉及到任何特定的技术它最初只是作为一个吸引人的Twitter标签在2009年的一个关于分布式,非关系数据库上的开源聚会。无论如何,这个术语触动了某些神经,并迅速通过网络启动社区和更远的地方传播开来。一些有趣的数据库系统现在与*#NoSQL#*标签相关联并被追溯性地重新解释为不仅是SQL 【4】
采用NoSQL数据库有几个驱动力其中包括 采用NoSQL数据库有几个驱动力其中包括
@ -65,19 +63,19 @@
[^i]: 从电子学借用一个术语。每个电路的输入和输出都有一定的阻抗(交流电阻)。当您将一个电路的输出连接到另一个电路的输入时,如果两个电路的输出和输入阻抗匹配,则连接上的功率传输将被最大化。阻抗不匹配可能导致信号反射和其他问题 [^i]: 从电子学借用一个术语。每个电路的输入和输出都有一定的阻抗(交流电阻)。当您将一个电路的输出连接到另一个电路的输入时,如果两个电路的输出和输入阻抗匹配,则连接上的功率传输将被最大化。阻抗不匹配可能导致信号反射和其他问题
像ActiveRecord和Hibernate这样的对象关系映射ORM框架减少了这个翻译层需要的样板代码的数量但是它们不能完全隐藏这两个模型之间的差异。 像ActiveRecord和Hibernate这样的**对象关系映射(object-relational mapping, ORM**框架减少了这个翻译层需要的样板代码的数量,但是它们不能完全隐藏这两个模型之间的差异。
![](img/fig2-1.png) ![](img/fig2-1.png)
**图2-1 使用关系型模式来表示领英简历** **图2-1 使用关系型模式来表示领英简历**
例如图2-1展示了如何在关系模式中表达简历一个LinkedIn简介。整个配置文件可以通过一个唯一的标识符user_id来标识。像first_name和last_name这样的字段每个用户只出现一次所以他们可以在用户表上建模为列。但是大多数人的职业职位多于一份工作人们可能有不同的教育期限和不同数量的联系信息。从用户到这些项目之间存在一对多的关系可以用多种方式来表示 例如,[图2-1](img/fig2-1.png)展示了如何在关系模式中表达简历一个LinkedIn简介。整个配置文件可以通过一个唯一的标识符`user_id`来标识。像`first_name``last_name`这样的字段每个用户只出现一次,所以他们可以在用户表上建模为列。但是,大多数人的职业(职位)多于一份工作,人们可能有不同的教育期限和不同数量的联系信息。从用户到这些项目之间存在一对多的关系,可以用多种方式来表示:
* 在传统SQL模型SQL1999之前最常见的规范化表示形式是将职位培训和联系信息放在单独的表中对用户表提供外键引用如图2-1所示。 * 在传统SQL模型SQL1999之前最常见的规范化表示形式是将职位培训和联系信息放在单独的表中对用户表提供外键引用[图2-1](img/fig2-1.png)所示。
* 更高版本的SQL标准增加了对结构化数据类型和XML数据的支持;这允许将多值数据存储在单行内支持在这些文档内查询和索引。这些功能在OracleIBM DB2MS SQL Server和PostgreSQL中都有不同程度的支持[6,7]。 JSON数据类型也受到几个数据库的支持包括IBM DB2MySQL和PostgreSQL [8] * 更高版本的SQL标准增加了对结构化数据类型和XML数据的支持;这允许将多值数据存储在单行内支持在这些文档内查询和索引。这些功能在OracleIBM DB2MS SQL Server和PostgreSQL中都有不同程度的支持【6,7】。 JSON数据类型也受到几个数据库的支持包括IBM DB2MySQL和PostgreSQL 【8】
* 第三种选择是将教育和联系信息编码为JSON或XML文档将其存储在数据库的文本列中并让应用程序解释其结构和内容。在此设置中,通常不能使用数据库查询该编码列中的值。 * 第三种选择是将教育和联系信息编码为JSON或XML文档将其存储在数据库的文本列中并让应用程序解释其结构和内容。在这种配置中,通常不能使用数据库查询该编码列中的值。
对于一个像简历这样的数据结构来说JSON表示可以是非常合适的参见例2-1。 JSON比XML更简单。 面向文档的数据库如MongoDB [9]RethinkDB [10]CouchDB [11]和Espresso [12])支持这种数据模型。 对于一个像简历这样自包含的数据结构而言JSON表示是非常合适的参见[例2-1]()。 JSON比XML更简单。 面向文档的数据库如MongoDB 【9】RethinkDB 【10】CouchDB 【11】和Espresso【12】)支持这种数据模型。
```json ```json
{ {
@ -117,11 +115,11 @@
} }
``` ```
一些开发人员认为JSON模型减少了应用程序代码和存储层之间的阻抗不匹配。但是正如我们将在第4章中看到的那样JSON作为数据编码格式也存在问题。缺乏一个模式往往被认为是一个优势;我们将在第39页的“文档模型中的模式灵活性”中讨论这个问题。 一些开发人员认为JSON模型减少了应用程序代码和存储层之间的阻抗不匹配。但是正如我们将在[第4章](ch4.md)中看到的那样JSON作为数据编码格式也存在问题。缺乏一个模式往往被认为是一个优势;我们将在“[文档模型中的模式灵活性](#文档模型中的模式灵活性)”中讨论这个问题。
JSON表示比图2-1中的多表模式具有更好的局部性。如果要在关系示例中获取配置文件则需要执行多个查询通过user_id查询每个表或者在用户表与其下属表之间执行混乱的多路连接。在JSON表示中所有的相关信息都在一个地方一个查询就足够了。 JSON表示比[图2-1](img/fig2-1.png)中的多表模式具有更好的局部性。如果要在关系示例中获取配置文件,则需要执行多个查询(通过`user_id`查询每个表或者在用户表与其下属表之间执行混乱的多路连接。在JSON表示中所有的相关信息都在一个地方一个查询就足够了。
从用户配置文件到用户位置教育历史和联系信息的一对多关系意味着数据中的树状结构而JSON表示使得这个树状结构变得明确见图2-2 从用户配置文件到用户位置教育历史和联系信息的一对多关系意味着数据中的树状结构而JSON表示使得这个树状结构变得明确[图2-2](img/fig2-2.png))。
![](img/fig2-2.png) ![](img/fig2-2.png)
@ -129,21 +127,27 @@ JSON表示比图2-1中的多表模式具有更好的局部性。如果要在关
### 多对一和多对多的关系 ### 多对一和多对多的关系
在上一节的例2-1中region_id和industry_id是以ID而不是纯字符串“Greater Seattle Area”和“Philanthropy”的形式给出的。为什么? 在上一节的[例2-1]()中,`region_id`和`industry_id`是以ID而不是纯字符串“大西雅图地区”和“慈善”的形式给出的。为什么?
如果用户界面具有用于输入区域和行业的自由文本字段,则将其存储为纯文本字符串是有意义的。但是,对地理区域和行业进行标准化,并让用户从下拉列表或自动填充器中进行选择是有好处的: 如果用户界面具有用于输入区域和行业的自由文本字段,则将其存储为纯文本字符串是有意义的。但是,对地理区域和行业进行标准化,并让用户从下拉列表或自动填充器中进行选择是有好处的:
* 统一的样式和拼写 * 统一的样式和拼写
* 避免歧义(例如,如果有几个同名的城市) * 避免歧义(例如,如果有几个同名的城市)
* 易于更新 - 名称只存储在一个地方,所以如果需要更改(例如,由于政治事件而改变城市名称),便于全面更新。 * 易于更新——名称只存储在一个地方,所以如果需要更改(例如,由于政治事件而改变城市名称),便于全面更新。
* 本地化支持 - 当网站翻译成其他语言时,标准化的名单可以被本地化,所以地区和行业可以用观众的语言表示 * 本地化支持——当网站翻译成其他语言时,标准化的名单可以被本地化,所以地区和行业可以使用用户的语言来表示
* 更好的搜索 - 例如,搜索华盛顿州的慈善家可以匹配这个概况,因为地区列表可以编码西雅图在华盛顿的事实(从“大西雅图地区”这个字符串中看不出来) * 更好的搜索——例如,搜索华盛顿州的慈善家可以匹配这份简历,因为地区列表可以编码记录西雅图在华盛顿的事实(从“大西雅图地区”这个字符串中看不出来)
无论是存储一个ID还是一个文本字符串都是一个关于**重复**的问题。当你使用一个ID时对人类有意义的信息比如慈善只存储在一个地方引用它的所有信息都使用一个IDID只在数据库中有意义。当你直接存储文本时每个使用它的记录中都存储的是有意义的信息。 无论是存储一个ID还是一个文本字符串都是一个关于**重复**的问题。当你使用一个ID时对人类有意义的信息比如单词:慈善只存储在一个地方引用它的所有信息都使用一个IDID只在数据库中有意义。当你直接存储文本时每个使用它的记录中都存储的是有意义的信息。
使用ID的好处是因为它对人类没有任何意义所以永远不需要改变ID可以保持不变即使它标识的信息发生变化。任何对人类有意义的东西都可能需要在将来某个时候改变 - 如果这些信息被复制,所有的冗余副本都需要更新。这会导致写入开销,并且存在不一致的风险(信息的一些副本被更新,但其他信息的副本不被更新)。去除这种重复是数据库规范化的关键思想。(关系模型区分了几种不同的范式,但这些区别实际上并不重要。 作为一个经验法则如果您重复只能存储在一个地方的值那么架构不会被规范化normalized 使用ID的好处是因为它对人类没有任何意义所以永远不需要改变ID可以保持不变即使它标识的信息发生变化。任何对人类有意义的东西都可能需要在将来某个时候改变——如果这些信息被复制,所有的冗余副本都需要更新。这会导致写入开销,并且存在不一致的风险(信息的一些副本被更新,但其他信息的副本不被更新)。去除这种重复是数据库规范化的关键思想。(关系模型区分了几种不同的范式,但这些区别实际上并不重要。 作为一个经验法则,如果您重复只能存储在一个地方的值,那么架构不会被**规范化normalized**[^ii]。)
不幸的是对这些数据进行规范化需要多对一的关系许多人生活在一个特定的地区许多人在一个特定的行业工作这与文档模型不太吻合。在关系数据库中通过ID来引用其他表中的行是正常的因为连接很容易。在文档数据库中一对多树结构不需要连接对连接的支持通常很弱 [^ii]: 关于关系模型的文献区分了几种不同的规范形式,但这些区别几乎没有实际意义。 作为一个经验法则,如果重复存储了只能存储在一个地方的值,则模式就不是规范化的。
> 数据库管理员和开发人员喜欢争论规范化和非规范化,但我们现在暂停判断。 在本书的[第三部分](part-iii.md),我们将回到这个话题,探讨处理缓存,非规范化和派生数据的系统方法。
不幸的是对这些数据进行规范化需要多对一的关系许多人生活在一个特定的地区许多人在一个特定的行业工作这与文档模型不太吻合。在关系数据库中通过ID来引用其他表中的行是正常的因为连接很容易。在文档数据库中一对多树结构不需要连接对连接的支持通常很弱[^iii]。
[^iii]: 在撰写本文时RethinkDB支持连接MongoDB不支持连接并且只支持CouchDB中的预先声明的视图。
如果数据库本身不支持连接,则必须通过对数据库进行多个查询来模拟应用程序代码中的连接。 (在这种情况下,地区和行业的名单可能很小,变化不大,应用程序可以简单地将它们留在内存中,但是,联接的工作从数据库转移到应用程序代码。 如果数据库本身不支持连接,则必须通过对数据库进行多个查询来模拟应用程序代码中的连接。 (在这种情况下,地区和行业的名单可能很小,变化不大,应用程序可以简单地将它们留在内存中,但是,联接的工作从数据库转移到应用程序代码。
@ -151,7 +155,7 @@ JSON表示比图2-1中的多表模式具有更好的局部性。如果要在关
***组织和学校作为实体*** ***组织和学校作为实体***
在前面的描述中组织用户工作的公司和school_name他们学习的地方只是字符串。也许他们应该是对实体的引用呢然后每个组织学校或大学都可以拥有自己的网页标识新闻提要等。每个简历可以链接到它所提到的组织和学校并且包括他们的标识和其他信息参见图2-3来自LinkedIn的一个例子 在前面的描述中,组织(用户工作的公司)和`school_name`(他们学习的地方)只是字符串。也许他们应该是对实体的引用呢?然后,每个组织,学校或大学都可以拥有自己的网页(标识,新闻提要等)。每个简历可以链接到它所提到的组织和学校,并且包括他们的标识和其他信息(参见[图2-3](img/fig2-3.png)来自LinkedIn的一个例子
***推荐*** ***推荐***
@ -161,7 +165,7 @@ JSON表示比图2-1中的多表模式具有更好的局部性。如果要在关
**图2-3 公司名不仅是字符串,还是一个指向公司实体的连接(领英截图)** **图2-3 公司名不仅是字符串,还是一个指向公司实体的连接(领英截图)**
图2-4 阐明了这些新功能怎样使用多对多关系。 每个虚线矩形内的数据可以分组成一个文档,但是对单位,学校和其他用户的引用需要表示为引用,并且在查询时需要连接。 [图2-4](img/fig2-4.png)阐明了这些新功能怎样使用多对多关系。 每个虚线矩形内的数据可以分组成一个文档,但是对单位,学校和其他用户的引用需要表示为引用,并且在查询时需要连接。
![](img/fig2-4.png) ![](img/fig2-4.png)
@ -171,78 +175,78 @@ JSON表示比图2-1中的多表模式具有更好的局部性。如果要在关
虽然关系数据库中经常使用多对多的关系和连接但文档数据库和NoSQL重新讨论了如何最好地在数据库中表示这种关系的争论。这个辩论比NoSQL早得多事实上它可以追溯到最早的计算机化数据库系统。 虽然关系数据库中经常使用多对多的关系和连接但文档数据库和NoSQL重新讨论了如何最好地在数据库中表示这种关系的争论。这个辩论比NoSQL早得多事实上它可以追溯到最早的计算机化数据库系统。
20世纪70年代最受欢迎的业务数据处理数据库是IBM的信息管理系统IMS最初是为了在阿波罗太空计划中进行存货而开发的并于1968年首次商业发布[13]。目前它仍在使用和维护在IBM大型机的OS / 390上运行[14] 20世纪70年代最受欢迎的业务数据处理数据库是IBM的信息管理系统IMS最初是为了在阿波罗太空计划中进行库存管理而开发的并于1968年首次商业发布【13】。目前它仍在使用和维护在IBM大型机的OS/390上运行【14】
IMS的设计使用了一个相当简单的数据模型称为层模型它与文档数据库使用的JSON模型有一些显着的相似之处[2]。它将所有数据表示为嵌套在记录中的记录树就像图2-2的JSON结构一样。 IMS的设计使用了一个相当简单的数据模型称为层模型它与文档数据库使用的JSON模型有一些显着的相似之处【2】。它将所有数据表示为嵌套在记录中的记录树,就像[图2-2](img/fig2-2.png)的JSON结构一样。
像文档数据库一样IMS在一对多的关系中运行良好但是它使多对多的关系变得困难并且不支持连接。开发人员必须决定是否冗余非规范化数据或手动解决从一个记录到另一个记录的引用。这些二十世纪六七十年代的问题与开发人员今天遇到的文档数据库问题非常相似[15] 像文档数据库一样IMS在一对多的关系中运行良好但是它使多对多的关系变得困难并且不支持连接。开发人员必须决定是否冗余非规范化数据或手动解决从一个记录到另一个记录的引用。这些二十世纪六七十年代的问题与开发人员今天遇到的文档数据库问题非常相似【15】
提出了各种解决方案来解决层次模型的局限性。其中最突出的两个是关系模型它变成了SQL接管了世界和网络模型最初很受关注但最终变得模糊。这两个阵营之间的“大辩论”持续了70年代的大部分时间[2] 提出了各种解决方案来解决层次模型的局限性。其中最突出的两个是关系模型它变成了SQL接管了世界和网络模型最初很受关注但最终变得模糊。这两个阵营之间的“大辩论”持续了70年代的大部分时间【2】
由于这两个模式解决的问题今天仍然如此相关,今天的辩论值得简要回顾一下。 由于这两个模式解决的问题今天仍然如此相关,今天的辩论值得简要回顾一下。
#### 网络模型 #### 网络模型
网络模型由一个称为数据系统语言会议CODASYL的委员会进行了标准化并由几个不同的数据源进行实施;它也被称为CODASYL模型[16] 网络模型由一个称为数据系统语言会议CODASYL的委员会进行了标准化并由几个不同的数据源进行实施;它也被称为CODASYL模型【16】
CODASYL模型是层次模型的推广。在分层模型的树结构中每条记录只有一个父节点在网络模式中一个记录可能有多个父母。例如“大西雅图地区”地区可能有一条记录而且每个居住在该地区的用户都可以与之相关联。这允许对多对一和多对多的关系进行建模。 CODASYL模型是层次模型的推广。在分层模型的树结构中每条记录只有一个父节点在网络模式中一个记录可能有多个父母。例如“大西雅图地区”地区可能有一条记录而且每个居住在该地区的用户都可以与之相关联。这允许对多对一和多对多的关系进行建模。
网络模型中记录之间的链接不是外键,而更像编程语言中的指针(同时仍然存储在磁盘上)。访问记录的唯一方法是沿着这些链路链上的根记录进行路径。这被称为访问路径。 网络模型中记录之间的链接不是外键,而更像编程语言中的指针(同时仍然存储在磁盘上)。访问记录的唯一方法是沿着这些链路链上的根记录进行路径。这被称为**访问路径**
在最简单的情况下,访问路径可能类似于遍历链表:从列表头开始,一次查看一条记录,直到找到所需的记录。但在一个多对多关系的世界里,几条不同的路径可能会导致相同的记录,一个使用网络模型的程序员必须跟踪这些不同的访问路径。 在最简单的情况下,访问路径可能类似于遍历链表:从列表头开始,一次查看一条记录,直到找到所需的记录。但在一个多对多关系的世界里,几条不同的路径可能会导致相同的记录,一个使用网络模型的程序员必须跟踪这些不同的访问路径。
CODASYL中的查询是通过遍历记录列表和访问路径后通过在数据库中移动游标来执行的。如果记录有多个父母即来自其他记录的多个传入指针则应用程序代码必须跟踪所有的各种关系。甚至CODASYL委员会成员也承认这就像在一个n维数据空间中进行导航[17] CODASYL中的查询是通过遍历记录列表和访问路径后通过在数据库中移动游标来执行的。如果记录有多个父母即来自其他记录的多个传入指针则应用程序代码必须跟踪所有的各种关系。甚至CODASYL委员会成员也承认这就像在一个n维数据空间中进行导航【17】
尽管手动访问路径选择能够最有效地利用20世纪70年代非常有限的硬件功能如磁带驱动器其搜索速度非常慢但问题是他们使查询和更新数据库的代码变得复杂不灵活。无论是分层还是网络模型如果你没有一个你想要的数据的路径那么你就处于一个困难的境地。你可以改变访问路径但是你必须经过大量的手写数据库查询代码并重写它来处理新的访问路径。很难对应用程序的数据模型进行更改。 尽管手动访问路径选择能够最有效地利用20世纪70年代非常有限的硬件功能如磁带驱动器其搜索速度非常慢但问题是他们使查询和更新数据库的代码变得复杂不灵活。无论是分层还是网络模型如果你没有一个你想要的数据的路径那么你就处于一个困难的境地。你可以改变访问路径但是你必须经过大量的手写数据库查询代码并重写它来处理新的访问路径。很难对应用程序的数据模型进行更改。
#### 关系模型 #### 关系模型
相比之下,关系模型做的就是将所有的数据放在open中一个关系table只是一个元组的集合,就是这样。没有迷宫似的嵌套结构,如果你想看看数据,没有复杂的访问路径。您可以读取表中的任何或所有行,选择符合任意条件的行。您可以通过指定某些列作为关键字并匹配这些关键字来读取特定行。您可以在任何表中插入一个新的行,而不必担心与其他表的外键关系[^iii]。 相比之下,关系模型做的就是将所有的数据放在光天化日之下:一个**关系table**只是一个**元组(行)**的集合,就是这样。没有迷宫似的嵌套结构,如果你想看看数据,没有复杂的访问路径。您可以读取表中的任何或所有行,选择符合任意条件的行。您可以通过指定某些列作为关键字并匹配这些关键字来读取特定行。您可以在任何表中插入一个新的行,而不必担心与其他表的外键关系[^iv]。
[^iii]: 外键约束允许对修改做限制,对于关系模型这并不是必选项。 即使有约束在查询时执行外键连接而在CODASYL中连接在插入时高效完成。 [^iv]: 外键约束允许对修改做限制,对于关系模型这并不是必选项。 即使有约束在查询时执行外键连接而在CODASYL中连接在插入时高效完成。
在关系数据库中,查询优化器自动决定查询的哪些部分以哪个顺序执行,以及使用哪些索引。这些选择实际上是“访问路径”,但最大的区别在于它们是由查询优化器自动生成的,而不是由程序员生成,所以我们很少需要考虑它们。 在关系数据库中,查询优化器自动决定查询的哪些部分以哪个顺序执行,以及使用哪些索引。这些选择实际上是“访问路径”,但最大的区别在于它们是由查询优化器自动生成的,而不是由程序员生成,所以我们很少需要考虑它们。
如果你想以新的方式查询你的数据,你可以声明一个新的索引,查询会自动使用哪个索引是最合适的。您不需要更改查询来利用新的索引。 另请参阅第42页上的“用于数据的查询语言”。)关系模型因此使向应用程序添加新功能变得更加容易。 如果你想以新的方式查询你的数据,你可以声明一个新的索引,查询会自动使用哪个索引是最合适的。您不需要更改查询来利用新的索引。 请参阅“[用于数据的查询语言](#用于数据的查询语言)”。)关系模型因此使向应用程序添加新功能变得更加容易。
关系数据库的查询优化器是复杂的,他们已经耗费了多年的研究和开发工作[18]。但关系模型的一个关键洞察是:您只需构建一次查询优化器,然后使用该数据库的所有应用程序都可以从中受益。如果您没有查询优化器,那么为特定查询手动编写访问路径比编写通用优化器更容易 - 但通用解决方案长期获胜 关系数据库的查询优化器是复杂的,他们已经耗费了多年的研究和开发工作【18】。但关系模型的一个关键洞察是只需构建一次查询优化器,然后使用该数据库的所有应用程序都可以从中受益。如果您没有查询优化器,那么为特定查询手动编写访问路径比编写通用优化器更容易——但通用解决方案从长期看更好
#### 与文档数据库相比 #### 与文档数据库相比
文档数据库在一个方面还原为层次模型在其父记录中存储嵌套记录图2-1中的一对多关系如位置教育和contact_info而不是在单独的表中。 文档数据库在一个方面还原为层次模型:在其父记录中存储嵌套记录([图2-1]()中的一对多关系,如位置,教育和`contact_info`),而不是在单独的表中。
但是,在表示多对一和多对多的关系时,关系数据库和文档数据库并没有根本的不同:在这两种情况下,相关项目都被一个唯一的标识符引用,这个标识符在关系模型中被称为外键,在文档模型中称为文档引用[9]。该标识符在读取时通过使用加入或后续查询来解决。迄今为止文档数据库没有遵循CODASYL的路径。 但是,在表示多对一和多对多的关系时,关系数据库和文档数据库并没有根本的不同:在这两种情况下,相关项目都被一个唯一的标识符引用,这个标识符在关系模型中被称为外键,在文档模型中称为文档引用【9】。该标识符在读取时通过使用加入或后续查询来解决。迄今为止文档数据库没有遵循CODASYL的路径。
### 关系型数据库与文档数据库在今日的对比 ### 关系型数据库与文档数据库在今日的对比
将关系数据库与文档数据库进行比较时,需要考虑许多差异,包括它们的容错属性(参阅第5章和处理并发性参阅第7章。在本章中我们将只关注数据模型中的差异。 将关系数据库与文档数据库进行比较时,需要考虑许多差异,包括它们的容错属性(参阅[第5章](ch5.md))和处理并发性(参阅[第7章](ch7.md))。在本章中,我们将只关注数据模型中的差异。
支持文档数据模型的主要论据是架构灵活性,由于局部性而导致的更好的性能,对于某些应用程序而言更接近于应用程序使用的数据结构。关系模型通过为连接提供更好的支持以及支持多对一和多对多的关系来反击。 支持文档数据模型的主要论据是架构灵活性,由于局部性而导致的更好的性能,对于某些应用程序而言更接近于应用程序使用的数据结构。关系模型通过为连接提供更好的支持以及支持多对一和多对多的关系来反击。
#### 哪个数据模型更方便写代码? #### 哪个数据模型更方便写代码?
如果应用程序中的数据具有类似文档的结构一对多关系树通常整个树被一次加载那么使用文档模型可能是一个好主意。将类似文档的结构分解成多个表如图2-1中的位置教育和contact_info的关系技术可能导致繁琐的模式和不必要的复杂的应用程序代码。 如果应用程序中的数据具有类似文档的结构(即,一对多关系树,通常整个树被一次加载),那么使用文档模型可能是一个好主意。将类似文档的结构分解成多个表(如[图2-1](img/fig2-1.png)中的位置,教育和`contact_info`)的关系技术可能导致繁琐的模式和不必要的复杂的应用程序代码。
文档模型有一定的局限性:例如,不能直接引用文档中的需要的项目而是需要说“用户251的位置列表中的第二项”很像访问路径在分层模型中。但是只要文件嵌套不太深通常不是问题。 文档模型有一定的局限性:例如,不能直接引用文档中的需要的项目而是需要说“用户251的位置列表中的第二项”很像访问路径在分层模型中。但是只要文件嵌套不太深通常不是问题。
应用程序对文档数据库连接的垃圾支持也许或也许不是一个问题。例如,在使用文档数据库记录 哪个事件发生在哪儿 的分析应用程序中,可能永远不需要多对多的关系[19] 应用程序对文档数据库连接的垃圾支持也许或也许不是一个问题。例如,在使用文档数据库记录 哪个事件发生在哪儿 的分析应用程序中,可能永远不需要多对多的关系【19】
但是,如果您的应用程序确实使用多对多关系,则文档模型变得不太吸引人。通过反规范化可以减少对连接的需求,但是应用程序代码需要做额外的工作来保持数据的一致性。通过向数据库发出多个请求,可以在应用程序代码中模拟连接,但是这也将复杂性转移到应用程序中,并且通常比由数据库内的专用代码执行的连接慢。在这种情况下,使用文档模型会导致更复杂的应用程序代码和更差的性能[15] 但是,如果您的应用程序确实使用多对多关系,那么文档模型就没有那么吸引人了。通过反规范化可以减少对连接的需求,但是应用程序代码需要做额外的工作来保持数据的一致性。通过向数据库发出多个请求,可以在应用程序代码中模拟连接,但是这也将复杂性转移到应用程序中,并且通常比由数据库内的专用代码执行的连接慢。在这种情况下,使用文档模型会导致更复杂的应用程序代码和更差的性能【15】
说哪个数据模型在一般情况下导致更简单的应用程序代码是不可能的;它取决于数据项之间存在的关系种类。对于高度相互关联的数据,文档模型很尴尬,关系模型是可接受的,而图形模型(参见第49页上的“类图形数据模型”)是最自然的。 说哪个数据模型在一般情况下导致更简单的应用程序代码是不可能的它取决于数据项之间存在的关系种类。对于高度相互关联的数据,文档模型很尴尬,关系模型是可接受的,而图形模型(参见“[图数据模型](#图数据模型)”)是最自然的。
#### 文档模型中的架构灵活性 #### 文档模型中的架构灵活性
大多数文档数据库以及关系数据库中的JSON支持都不会对文档中的数据执行任何模式。关系数据库中的XML支持通常带有可选的模式验证。没有模式意味着可以将任意的键和值添加到文档中并且在读取时客户端对于文档可能包含的字段没有保证。 大多数文档数据库以及关系数据库中的JSON支持都不会对文档中的数据执行任何模式。关系数据库中的XML支持通常带有可选的模式验证。没有模式意味着可以将任意的键和值添加到文档中并且在读取时客户端对于文档可能包含的字段没有保证。
文档数据库有时称为无模式,但这是误导性的,因为读取数据的代码通常采用某种结构 - 即存在隐式模式,但不由数据库强制执行[20]。一个更精确的术语是 读时模式schema-on-read数据的结构是隐含的只有在数据被读取时才被解释相应的是写时模式schema-on-write 传统的关系数据库方法,模式是明确,数据库确保所有的数据都符合它的形式)[21] 文档数据库有时称为**无模式schemaless**,但这是误导性的,因为读取数据的代码通常采用某种结构——即存在隐式模式但不由数据库强制执行【20】。一个更精确的术语是**读时模式schema-on-read**(数据的结构是隐含的,只有在数据被读取时才被解释),相应的是**写时模式schema-on-write**传统的关系数据库方法,模式是明确,数据库确保所有的数据都符合它的形式)【21】
读取模式类似于编程语言中的动态(运行时)类型检查,而模式写入类似于静态(编译时)类型检查。就像静态和动态类型检查的倡导者对于它们的相对优点有很大的争议[22],数据库中模式的执行是一个有争议的话题,一般来说没有正确或错误的答案。 读取模式类似于编程语言中的动态(运行时)类型检查,而模式写入类似于静态(编译时)类型检查。就像静态和动态类型检查的倡导者对于它们的相对优点有很大的争议【22】,数据库中模式的执行是一个有争议的话题,一般来说没有正确或错误的答案。
在应用程序想要改变其数据格式的情况下,这些方法之间的区别特别明显。例如,假设你正在将每个用户的全名存储在一个字段中,而你想分别存储名字和姓氏[23]。在文档数据库中,只需开始使用新字段写入新文档,并在应用程序中使用代码来处理读取旧文档时的情况。例如: 在应用程序想要改变其数据格式的情况下,这些方法之间的区别特别明显。例如,假设你正在将每个用户的全名存储在一个字段中,而你想分别存储名字和姓氏【23】。在文档数据库中,只需开始使用新字段写入新文档,并在应用程序中使用代码来处理读取旧文档时的情况。例如:
```go ```go
if (user && user.name && !user.first_name) { if (user && user.name && !user.first_name) {
// Documents written before Dec 8, 2013 don't have first_name // Documents written before Dec 8, 2013 don't have first_name
user.first_name = user.name.split(" ")[0]; user.first_name = user.name.split(" ")[0];
} }
``` ```
@ -255,9 +259,9 @@ UPDATE users SET first_name = split_part(name, ' ', 1); -- PostgreSQL
UPDATE users SET first_name = substring_index(name, ' ', 1); -- MySQL UPDATE users SET first_name = substring_index(name, ' ', 1); -- MySQL
``` ```
模式变更的速度很慢而且需要停机。这种声誉并不是完全应得的大多数关系数据库系统在几毫秒内执行ALTER TABLE语句。 MySQL是一个值得注意的例外在ALTER TABLE上复制整个表,这可能意味着在更改一个大表时会花费几分钟甚至几个小时的停机时间,尽管存在各种工具可以解决这个限制[24,25,26] 模式变更的速度很慢,而且需要停机。这种声誉并不是完全应得的:大多数关系数据库系统在几毫秒内执行`ALTER TABLE`语句。 MySQL是一个值得注意的例外执行`ALTER TABLE`时会复制整个表,这可能意味着在更改一个大表时会花费几分钟甚至几个小时的停机时间,尽管存在各种工具可以解决这个限制【24,25,26】
在大型表上运行UPDATE语句在任何数据库上都可能会很慢因为每一行都需要重写。如果这是不可接受的应用程序可以将first_name设置为默认值NULL并在读取时填充它就像使用文档数据库一样。 在大型表上运行`UPDATE`语句在任何数据库上都可能会很慢,因为每一行都需要重写。如果这是不可接受的,应用程序可以将`first_name`设置为默认值`NULL`,并在读取时填充它,就像使用文档数据库一样。
如果由于某种原因(例如,数据是异构的)集合中的项目并不都具有相同的结构,例如,因为: 如果由于某种原因(例如,数据是异构的)集合中的项目并不都具有相同的结构,例如,因为:
@ -268,32 +272,34 @@ UPDATE users SET first_name = substring_index(name, ' ', 1); -- MySQL
#### 查询的数据局部性 #### 查询的数据局部性
文档通常以单个连续字符串形式存储编码为JSONXML或其二进制变体如MongoDB的BSON。如果您的应用程序经常需要访问整个文档例如将其渲染至网页则此存储局部性会带来性能优势。如果将数据分割到多个表中如图2-1所示则需要进行多次索引查找才能将其全部检索出来这可能需要更多的磁盘查找并花费更多的时间。 文档通常以单个连续字符串形式存储编码为JSONXML或其二进制变体如MongoDB的BSON。如果您的应用程序经常需要访问整个文档例如将其渲染至网页则此存储局部性会带来性能优势。如果将数据分割到多个表中[图2-1](img/fig2-1.png)所示),则需要进行多次索引查找才能将其全部检索出来,这可能需要更多的磁盘查找并花费更多的时间。
局部性仅适用于需要文档绝大部分内容的情况。数据库通常需要加载整个文档,即使只访问其中的一小部分,这对于大型文档来说是很浪费的。更新文档时,通常需要整个重写。 只有不改变文档大小的修改才可以容易地原地执行。因此通常建议,保持相对小的文档,并避免增加文档的大小的写入[9]。这些性能限制大大减少了文档数据库的实用场景。 局部性仅适用于需要文档绝大部分内容的情况。数据库通常需要加载整个文档,即使只访问其中的一小部分,这对于大型文档来说是很浪费的。更新文档时,通常需要整个重写。 只有不改变文档大小的修改才可以容易地原地执行。因此通常建议,保持相对小的文档,并避免增加文档的大小的写入【9】。这些性能限制大大减少了文档数据库的实用场景。
值得指出的是将相关的数据分组在一起的想法并不局限于文档模型。例如Google的Spanner数据库在关系数据模型中提供了相同的位置属性允许模式声明一个表的行应该在父表内交织 interleaved嵌套[27]。 Oracle允许使用一个称为多表索引集群表multi-table index cluster tables的特性相同的表[28]。 Bigtable数据模型在Cassandra和HBase中使用中的列族(column-family)概念与管理局部性的目的类似[29] 值得指出的是将相关的数据分组在一起的想法并不局限于文档模型。例如Google的Spanner数据库在关系数据模型中提供了相同的位置属性允许模式声明一个表的行应该在父表内**交织interleaved**嵌套【27】。 Oracle允许使用一个称为**多表索引集群表multi-table index cluster tables**的类似特性【28】。 Bigtable数据模型在Cassandra和HBase中使用中的**列族column-family**概念与管理局部性的目的类似【29】
在第3章我们还会看到更多关于本地的内容。 [第3章](ch3.md)我们还会看到更多关于本地的内容。
#### 文档和关系数据库的融合 #### 文档和关系数据库的融合
自2000年代以来大多数关系数据库系统MySQL除外都支持XML。这包括对XML文档进行本地修改的功能以及在XML文档中进行索引和查询的功能这允许应用程序使用与使用文档数据库时所做的非常相似的数据模型。 自2000年代以来大多数关系数据库系统MySQL除外都支持XML。这包括对XML文档进行本地修改的功能以及在XML文档中进行索引和查询的功能这允许应用程序使用与使用文档数据库时所做的非常相似的数据模型。
从9.3版本开始的PostgreSQL [8]从5.7版本开始的MySQL以及从版本10.5开始的IBM DB2 [30]也对JSON文档提供了类似的支持级别。鉴于Web API的JSON流行其他关系数据库很可能会跟随他们的脚步并添加JSON支持。 从9.3版本开始的PostgreSQL 【8】从5.7版本开始的MySQL以及从版本10.5开始的IBM DB2 [30]也对JSON文档提供了类似的支持级别。鉴于Web API的JSON流行其他关系数据库很可能会跟随他们的脚步并添加JSON支持。
在文档数据库方面RethinkDB支持其查询语言中的类似关系的连接一些MongoDB驱动程序自动解决数据库引用有效地执行客户端JOIN尽管这可能比在数据库中执行的JOIN慢,需要额外的网络往返,并且没有优化)。 在文档数据库方面RethinkDB支持其查询语言中的类似关系的连接一些MongoDB驱动程序自动解决数据库引用有效地执行客户端连接,尽管这可能比在数据库中执行的连接慢,需要额外的网络往返,并且没有优化)。
似乎随着时间的推移,关系数据库和文档数据库变得越来越相似,这是一件好事:数据模型相互补充,如果一个数据库能够处理类似文档的数据,并对其执行关系查询,那么应用可以使用最适合其需要的特征的组合。 似乎随着时间的推移,关系数据库和文档数据库变得越来越相似,这是一件好事:数据模型相互补充[^v],如果一个数据库能够处理类似文档的数据,并对其执行关系查询,那么应用可以使用最适合其需要的特征的组合。关系模型和文档模型的混合是数据库未来的一个很好的途径。
关系模型和文档模型的混合是数据库未来的一个很好的途径。
Codd对关系模型[1]的原始描述实际上允许在关系模式中与JSON文档非常相似。 他称之为 非简单域nonsimple domains。 这个想法是,一行中的值不一定是一个像数字或字符串一样的原始数据类型,也可以是一个嵌套的关系(表格) - 所以你可以有一个任意嵌套的树结构作为一个值, 很像30年后添加到SQL中的JSON或XML支持。 [^v]: Codd对关系模型【1】的原始描述实际上允许在关系模式中与JSON文档非常相似。 他称之为**非简单域nonsimple domains**。 这个想法是,一行中的值不一定是一个像数字或字符串一样的原始数据类型,也可以是一个嵌套的关系(表格)所以你可以有一个任意嵌套的树结构作为一个值, 很像30年后添加到SQL中的JSON或XML支持。
Codd对关系模型【1】的原始描述实际上允许在关系模式中与JSON文档非常相似。 他称之为**非简单域
## 数据查询语言 ## 数据查询语言
当引入关系模型时它包含了一种查询数据的新方法SQL是一个声明式查询语言而IMS和CODASYL使用命令式的代码来查询数据库。那是什么意思 当引入关系模型时它包含了一种查询数据的新方法SQL是一个声明式查询语言而IMS和CODASYL使用命令式的代码来查询数据库。那是什么意思
许多常用的编程语言是命令式的。例如,如果你有一个动物物种的列表,返回列表中的鲨鱼可以这样写: 许多常用的编程语言是命令式的。例如,如果你有一个动物物种的列表,返回列表中的鲨鱼可以这样写:
```js ```js
@ -312,7 +318,7 @@ function getSharks() {
$$ $$
sharks = σ_{family = "sharks"}(animals) sharks = σ_{family = "sharks"}(animals)
$$ $$
σ(希腊字母西格玛)是选择操作符,只返回符合条件的动物,family=“shark” σ(希腊字母西格玛)是选择操作符,只返回符合条件的动物,`family="shark"`
在定义SQL时它紧密地遵循关系代数的结构 在定义SQL时它紧密地遵循关系代数的结构
@ -327,9 +333,10 @@ SELECT * FROM animals WHERE family ='Sharks';
声明式查询语言是有吸引力的因为它通常比命令式API更加简洁和容易。但更重要的是它还隐藏了数据库引擎的实现细节这使得数据库系统可以在不需要对查询进行任何更改的情况下提高性能。 声明式查询语言是有吸引力的因为它通常比命令式API更加简洁和容易。但更重要的是它还隐藏了数据库引擎的实现细节这使得数据库系统可以在不需要对查询进行任何更改的情况下提高性能。
例如,在本节开头所示的命令代码中,动物列表以特定顺序出现。如果数据库想要在场景后面回收未使用的磁盘空间,则可能需要移动记录,改变动物出现的顺序。数据库能否安全地执行,而不会中断查询? 例如,在本节开头所示的命令代码中,动物列表以特定顺序出现。如果数据库想要在场景后面回收未使用的磁盘空间,则可能需要移动记录,改变动物出现的顺序。数据库能否安全地执行,而不会中断查询?
SQL示例不保证任何特定的顺序所以它不介意顺序是否改变。但是如果查询被写为命令式的代码那么数据库就永远不能确定代码是否依赖于排序。 SQL在功能上更加有限的事实为数据库提供了更多自动优化的空间。 SQL示例不保证任何特定的顺序所以它不介意顺序是否改变。但是如果查询被写为命令式的代码那么数据库就永远不能确定代码是否依赖于排序。 SQL在功能上更加有限的事实为数据库提供了更多自动优化的空间。
最后声明性语言往往适合于并行执行。现在通过增加更多的内核CPU的速度会更快而不是以比以前更高的时钟速度运行[31]。命令代码很难在多个内核和多个机器之间并行化,因为它指定了必须以特定顺序执行的指令。声明性语言在并行执行中获得更快的机会,因为它们仅指定结果的模式,而不是用于确定结果的算法。如果合适,数据库可以自由使用查询语言的并行实现[32] 最后声明性语言往往适合于并行执行。现在通过增加更多的内核CPU的速度会更快而不是以比以前更高的时钟速度运行【31】。命令代码很难在多个内核和多个机器之间并行化,因为它指定了必须以特定顺序执行的指令。声明性语言在并行执行中获得更快的机会,因为它们仅指定结果的模式,而不是用于确定结果的算法。如果合适,数据库可以自由使用查询语言的并行实现【32】
### Web上的声明式查询 ### Web上的声明式查询
@ -379,7 +386,7 @@ li.selected > p {
在这里XPath表达式`li[@class='selected']/p`相当于上例中的CSS选择器`li.selected> p`。 CSS和XSL的共同之处在于它们都是用于指定文档样式的声明性语言。 在这里XPath表达式`li[@class='selected']/p`相当于上例中的CSS选择器`li.selected> p`。 CSS和XSL的共同之处在于它们都是用于指定文档样式的声明性语言。
想象一下如果你必须使用一命令式的方法生活会是什么样子。在Javascript中使用文档对象模型DOMAPI结果可能如下所示 想象一下如果你必须使用一命令式的方法生活会是什么样子。在Javascript中使用**文档对象模型DOM**API结果可能如下所示
```js ```js
var liElements = document.getElementsByTagName("li"); var liElements = document.getElementsByTagName("li");
@ -403,12 +410,14 @@ for (var i = 0; i < liElements.length; i++) {
* 如果您想要利用新的API例如`document.getElementsBy ClassName“selected”`)甚至`document.evaluate()`来提高性能则必须重写代码。另一方面浏览器供应商可以在不破坏兼容性的情况下提高CSS和XPath的性能。 * 如果您想要利用新的API例如`document.getElementsBy ClassName“selected”`)甚至`document.evaluate()`来提高性能则必须重写代码。另一方面浏览器供应商可以在不破坏兼容性的情况下提高CSS和XPath的性能。
在Web浏览器中使用声明式CSS样式比在JavaScript中强制操作样式好得多。类似地在数据库中像SQL这样的声明式查询语言比命令式查询APIIMS与CODASYL都是命令式的应用程序通常通过COBOL代码来一次一行迭代数据库中的记录要好得多 在Web浏览器中使用声明式CSS样式比在JavaScript中强制操作样式好得多。类似地在数据库中像SQL这样的声明式查询语言比命令式查询APIIMS与CODASYL都是命令式的应用程序通常通过COBOL代码来一次一行迭代数据库中的记录要好得多[^vi]。
[^vi]: vi IMS和CODASYL都使用命令式API。 应用程序通常使用COBOL代码遍历数据库中的记录一次一条记录【2,16】。
### MapReduce查询 ### MapReduce查询
MapReduce是一个编程模型用于在许多机器上批量处理大量的数据由Google推广[33]。一些NoSQL数据存储包括MongoDB和CouchDB支持有限形式的MapReduce作为在多个文档中执行只读查询的机制。 MapReduce是一个编程模型用于在许多机器上批量处理大量的数据由Google推广【33】。一些NoSQL数据存储包括MongoDB和CouchDB支持有限形式的MapReduce作为在多个文档中执行只读查询的机制。
总的来说MapReduce在第10章中有更详细的描述。现在我们将简要讨论一下MongoDB对这一模型的应用。 总的来说MapReduce在[第10章](ch10.md)中有更详细的描述。现在我们将简要讨论一下MongoDB对这一模型的应用。
MapReduce既不是一个声明性的查询语言也不是一个完全强制性的查询API而是位于两者之间的地方查询的逻辑用代码片断来表示这些代码片段被处理框架重复地调用。它基于许多函数式编程语言中存在的map也称为collect和reduce也称为fold或inject函数。 MapReduce既不是一个声明性的查询语言也不是一个完全强制性的查询API而是位于两者之间的地方查询的逻辑用代码片断来表示这些代码片段被处理框架重复地调用。它基于许多函数式编程语言中存在的map也称为collect和reduce也称为fold或inject函数。
@ -450,9 +459,9 @@ db.observations.mapReduce(function map() {
* 可以声明性地指定只考虑鲨鱼种类的过滤器这是对MapReduce的特定于MongoDB的扩展 * 可以声明性地指定只考虑鲨鱼种类的过滤器这是对MapReduce的特定于MongoDB的扩展
* 每个匹配查询的文档都会调用一次JavaScript函数`map`,将`this`设置为文档对象。 * 每个匹配查询的文档都会调用一次JavaScript函数`map`,将`this`设置为文档对象。
* map函数发出一个键包括年份和月份的字符串“2013-12”或“2014-1”)和一个值(该观察中的动物数量)。 * `map`函数发出一个键(包括年份和月份的字符串,如`"2013-12"`或`"2014-1"`)和一个值(该观察中的动物数量)。
* map发出的键值对按键组合。 对于具有相同键(即,相同的月份和年份)的所有键值对,`reduce`函数被调用一次。 * `map`发出的键值对按键组合。 对于具有相同键(即,相同的月份和年份)的所有键值对,`reduce`函数被调用一次。
* reduce函数将特定月份内所有观测值的动物数量相加。 * `reduce`函数将特定月份内所有观测值的动物数量相加。
* 最终的输出被写入到`monthlySharkReport`集合中。 * 最终的输出被写入到`monthlySharkReport`集合中。
例如假设`observations`集合包含这两个文档: 例如假设`observations`集合包含这两个文档:
@ -476,11 +485,11 @@ db.observations.mapReduce(function map() {
map和reduce函数在功能上有限制它们必须是纯函数这意味着它们只使用传递给它们的数据作为输入它们不能执行额外的数据库查询也不能有任何副作用。这些限制允许数据库以任何顺序运行任何功能并在失败时重新运行它们。然而它们仍然是强大的它们可以解析字符串调用库函数执行计算等等。 map和reduce函数在功能上有限制它们必须是纯函数这意味着它们只使用传递给它们的数据作为输入它们不能执行额外的数据库查询也不能有任何副作用。这些限制允许数据库以任何顺序运行任何功能并在失败时重新运行它们。然而它们仍然是强大的它们可以解析字符串调用库函数执行计算等等。
MapReduce是一个相当低级的编程模型用于在一组机器上进行分布式执行。像SQL这样的更高级的查询语言可以用一系列的MapReduce操作来实现见第10章但是也有很多不使用MapReduce的分布式SQL实现。请注意SQL中没有任何内容限制它在单个机器上运行而MapReduce在分布式查询执行方面没有垄断权。 MapReduce是一个相当低级的编程模型用于在一组机器上进行分布式执行。像SQL这样的更高级的查询语言可以用一系列的MapReduce操作来实现[第10章](ch10.md)但是也有很多不使用MapReduce的分布式SQL实现。请注意SQL中没有任何内容限制它在单个机器上运行而MapReduce在分布式查询执行方面没有垄断权。
能够在查询中使用JavaScript代码是高级查询的一个重要特性但不限于MapReduce一些SQL数据库也可以使用JavaScript函数进行扩展[34] 能够在查询中使用JavaScript代码是高级查询的一个重要特性但不限于MapReduce一些SQL数据库也可以使用JavaScript函数进行扩展【34】
MapReduce的可用性问题是你必须编写两个协调的JavaScript函数这通常比编写单个查询更困难。此外声明性查询语言为查询优化器提供了更多机会来提高查询的性能。由于这些原因MongoDB 2.2添加了对称为聚合管道的声明性查询语言的支持[9]。在这种语言中,相同的鲨鱼计数查询如下所示: MapReduce的可用性问题是你必须编写两个协调的JavaScript函数这通常比编写单个查询更困难。此外声明性查询语言为查询优化器提供了更多机会来提高查询的性能。由于这些原因MongoDB 2.2添加了对称为聚合管道的声明性查询语言的支持【9】。在这种语言中,相同的鲨鱼计数查询如下所示:
```js ```js
db.observations.aggregate([ db.observations.aggregate([
@ -504,13 +513,13 @@ db.observations.aggregate([
但是,如果多对多的关系在您的数据中很常见呢?关系模型可以处理多对多关系的简单情况,但是随着数据之间的连接变得更加复杂,开始将数据建模为图形变得更加自然。 但是,如果多对多的关系在您的数据中很常见呢?关系模型可以处理多对多关系的简单情况,但是随着数据之间的连接变得更加复杂,开始将数据建模为图形变得更加自然。
一个图由两种对象组成:顶点vertices 也称为节点nodes 或实体 entities和边edges 也称为关系relationships或弧 arcs )。许多种数据可以被模拟为一个图形。典型的例子包括: 一个图由两种对象组成:**顶点vertices**(也称为**节点nodes** 或**实体entities**),和**边edges** 也称为**关系relationships**或**弧 arcs** )。许多种数据可以被模拟为一个图形。典型的例子包括:
***社交图*** ***社交图***
顶点是人,边指示哪些人彼此认识。 顶点是人,边指示哪些人彼此认识。
***网络图*** ***网络图***
顶点是网页边缘表示到其他页面的HTML链接。 顶点是网页边缘表示到其他页面的HTML链接。
@ -520,35 +529,36 @@ db.observations.aggregate([
众所周知的算法可以在这些图上进行操作例如汽车导航系统搜索道路网络中两点之间的最短路径PageRank可以用在网络图上来确定网页的流行程度从而其在搜索结果中的排名。 众所周知的算法可以在这些图上进行操作例如汽车导航系统搜索道路网络中两点之间的最短路径PageRank可以用在网络图上来确定网页的流行程度从而其在搜索结果中的排名。
在刚刚给出的例子中图中的所有顶点代表了相同类型的东西网页或交叉路口。然而图并不局限于这样的同类数据图的同样强大的用途是提供一种在单个数据存储中存储完全不同类型的对象的一致方式。例如Facebook维护一个包含许多不同类型的顶点和边的单个图顶点表示用户所做的人地点事件签到和评论;边缘表示哪些人是彼此的朋友,哪个位置发生了检查,谁评论了哪个职位,谁参加了哪个事件,等等[35] 在刚刚给出的例子中图中的所有顶点代表了相同类型的东西网页或交叉路口。然而图并不局限于这样的同类数据图的同样强大的用途是提供一种在单个数据存储中存储完全不同类型的对象的一致方式。例如Facebook维护一个包含许多不同类型的顶点和边的单个图顶点表示用户所做的人地点事件签到和评论;边缘表示哪些人是彼此的朋友,哪个位置发生了检查,谁评论了哪个职位,谁参加了哪个事件,等等【35】
在本节中我们将使用图2-5所示的示例。它可以从社交网络或系谱数据库中获得它显示了两个人来自爱达荷州的Lucy和来自法国Beaune的Alain。他们已婚住在伦敦。 在本节中,我们将使用[图2-5](img/fig2-5.png)所示的示例。它可以从社交网络或系谱数据库中获得它显示了两个人来自爱达荷州的Lucy和来自法国Beaune的Alain。他们已婚住在伦敦。
![](img/fig2-5.png) ![](img/fig2-5.png)
**图2-5 图数据结构示例(框代表顶点,箭头代表边)** **图2-5 图数据结构示例(框代表顶点,箭头代表边)**
有几种不同但相关的方法来构建和查询图表中的数据。 在本节中我们将讨论属性图模型由Neo4jTitan和InfiniteGraph实现和三存储triple-store模型由DatomicAllegroGraph等实现。 我们将看图的三种声明性查询语言CypherSPARQL和Datalog。 除此之外还有像Gremlin [36]这样的图形查询语言和像Pregel这样的图形处理框架见第10章 有几种不同但相关的方法来构建和查询图表中的数据。 在本节中我们将讨论属性图模型由Neo4jTitan和InfiniteGraph实现和三元组存储triple-store模型由DatomicAllegroGraph等实现。 我们将看图的三种声明性查询语言CypherSPARQL和Datalog。 除此之外还有像Gremlin 【36】这样的图形查询语言和像Pregel这样的图形处理框架[第10章](ch10.md))。
### 属性图 ### 属性图
在属性图模型中每个顶点vertex包括 在属性图模型中,每个**顶点vertex**包括:
* 唯一的标识符 * 唯一的标识符
* 出向边集合outgoing edges * **出向边集合outgoing edges**
* 入向边集合ingoing edges * **入向边集合ingoing edges**
* 一组属性(键值对) * 一组属性(键值对)
每条边edge包括 每条**edge**包括:
* 唯一标识符 * 唯一标识符
* 边的起点tail vertex 箭头出发的点 * **边的起点/尾tail vertex**
* 边的终点head vertex 箭头指向的点) * **边的终点/头head vertex**
* 描述两个顶点之间关系类型的标签 * 描述两个顶点之间关系类型的标签
* 一组属性(键值对) * 一组属性(键值对)
可以将图存储看作两个关系表一个存储顶点另一个存储边如例2-2所示该模式使用PostgreSQL json数据类型来存储每个顶点或边的属性。头部和尾部顶点存储为每个边缘。如果您想要一组顶点的输入或输出边您可以分别通过head_vertex或tail_vertex来查询边表。 可以将图存储看作两个关系表:一个存储顶点,另一个存储边,如[例2-2]()所示该模式使用PostgreSQL json数据类型来存储每个顶点或边的属性。头部和尾部顶点存储为每个边缘。如果您想要一组顶点的输入或输出边您可以分别通过`head_vertex`或`tail_vertex`来查询边表。
例2-2 使用关系模式来表示属性图
**例2-2 使用关系模式来表示属性图**
```sql ```sql
CREATE TABLE vertices ( CREATE TABLE vertices (
@ -571,20 +581,20 @@ CREATE INDEX edges_heads ON edges (head_vertex);
这个模型的一些重要方面是: 这个模型的一些重要方面是:
1. 任何顶点都可以有一个边连接到任何其他顶点。没有哪种事物可不可以关联的模式限制。 1. 任何顶点都可以有一个边连接到任何其他顶点。没有哪种事物可不可以关联的模式限制。
2. 给定任何顶点,您可以高效地找到它的入边和出边,从而遍历图,即沿着一系列顶点的路径前后移动。 (这就是为什么示例2-2在tail_vertex和head_vertex列上都有索引的原因。) 2. 给定任何顶点,您可以高效地找到它的入边和出边,从而遍历图,即沿着一系列顶点的路径前后移动。 (这就是为什么[例2-2]()在`tail_vertex`和`head_vertex`列上都有索引的原因。)
3. 通过对不同类型的关系使用不同的标签,可以在一个图中存储几种不同的信息,同时仍然保持一个干净的数据模型。 3. 通过对不同类型的关系使用不同的标签,可以在一个图中存储几种不同的信息,同时仍然保持一个干净的数据模型。
这些特性为数据建模提供了很大的灵活性如图2-5所示。图中显示了一些传统关系模式难以表达的东西例如不同国家的不同地区结构法国有省和州美国有不同的州和州国中国的怪事先忽略主权国家和国家错综复杂的烂摊子不同的数据粒度露西现在的住所被指定为一个城市而她的出生地点只是在一个州的级别 这些特性为数据建模提供了很大的灵活性,如[图2-5](img/fig2-5.png)所示。图中显示了一些传统关系模式难以表达的东西,例如不同国家的不同地区结构(法国有省和州,美国有不同的州和州),国中国的怪事(先忽略主权国家和国家错综复杂的烂摊子),不同的数据粒度(露西现在的住所被指定为一个城市,而她的出生地点只是在一个州的级别)。
你可以想象延伸图还包括许多关于露西和阿兰,或其他人的其他事实。例如,您可以用它来表示食物过敏(通过为每个过敏原引入一个顶点,以及人与过敏原之间的边缘来指示过敏),并将过敏原与一组过敏原显示哪些食物含有哪些物质的顶点。然后,你可以写一个查询,找出每个人吃什么是安全的。图表对于可演化性是有利的:当您向应用程序添加功能时,可以轻松扩展图形以适应应用程序数据结构的变化。 你可以想象延伸图还包括许多关于露西和阿兰,或其他人的其他事实。例如,您可以用它来表示食物过敏(通过为每个过敏原引入一个顶点,以及人与过敏原之间的边缘来指示过敏),并将过敏原与一组过敏原显示哪些食物含有哪些物质的顶点。然后,你可以写一个查询,找出每个人吃什么是安全的。图表对于可演化性是有利的:当您向应用程序添加功能时,可以轻松扩展图形以适应应用程序数据结构的变化。
### Cypher查询语言 ### Cypher查询语言
Cypher是属性图的声明式查询语言为Neo4j图形数据库发明[37]。 (它是以电影“黑客帝国”中的角色命名的,与密码术中的密码无关[38]。) Cypher是属性图的声明式查询语言为Neo4j图形数据库发明【37】。 它是以电影“黑客帝国”中的角色命名的与密码术中的密码无关【38】。)
例2-3显示了将图2-5的左边部分插入图形数据库的Cypher查询。图的其余部分可以类似地添加为了便于阅读而省略。每个顶点都有一个像USA或Idaho这样的符号名称查询的其他部分可以使用这些名称在顶点之间创建边使用箭头符号`Idaho - [WITHIN] - >USA`创建一个带有标记`WITHIN`的边,爱达荷州为尾节点,美国为头节点。 [例2-3]()显示了将[图2-5](img/fig2-5.png)的左边部分插入图形数据库的Cypher查询。图的其余部分可以类似地添加为了便于阅读而省略。每个顶点都有一个像USA或Idaho这样的符号名称查询的其他部分可以使用这些名称在顶点之间创建边使用箭头符号`Idaho - [WITHIN] ->USA`创建一个带有标记`WITHIN`的边,爱达荷州为尾节点,美国为头节点。
例2-3 将图2-5中的数据子集表示为Cypher查询 **例2-3 将图2-5中的数据子集表示为Cypher查询**
```cypher ```cypher
CREATE CREATE
@ -596,9 +606,9 @@ CREATE
(Lucy) -[:BORN_IN]-> (Idaho) (Lucy) -[:BORN_IN]-> (Idaho)
``` ```
当图2-5的所有顶点和边被添加到数据库时我们可以开始提出有趣的问题例如找到所有从美国移民到欧洲的人的名字。更确切地说在这里我们想要找到在美国有一个BORN_IN边缘的所有顶点还有一个LIVING_IN边缘到欧洲的一个位置并且返回每个这些顶点的名称属性。 [图2-5](img/fig2-5.png)的所有顶点和边被添加到数据库时,我们可以开始提出有趣的问题:例如,找到所有从美国移民到欧洲的人的名字。更确切地说,在这里我们想要找到在美国有一个`BORN_IN`边缘的所有顶点,还有一个`LIVING_IN`边缘到欧洲的一个位置,并且返回每个这些顶点的名称属性。
例2-4展示了如何在Cypher中表达这个查询。在MATCH子句中使用相同的箭头符号来查找图中的模式 **例2-4 展示了如何在Cypher中表达这个查询。在MATCH子句中使用相同的箭头符号来查找图中的模式**
```cypher ```cypher
MATCH MATCH
@ -609,26 +619,29 @@ RETURN person.name
查询可以被读取如下: 查询可以被读取如下:
* 找到满足以下两者的所有顶点称之为person > 找到满足以下两者的所有顶点称之为person
* 有一条到某个顶点`BORN_IN`类型的出边。从那个顶点开始可以沿着一系列`WITHIN`出边最终到达类型为`Location``name=United States`的顶点 > 1. 有一条到某个顶点`BORN_IN`类型的出边。从那个顶点开始可以沿着一系列`WITHIN`出边最终到达类型为`Location``name=United States`的顶点
* 也有一条到某个顶点`LIVES_IN`类型的出边。沿着这条边,可以通过一系列`WITHIN`出边最终到达类型为`Location``name=Europe`的顶点 >
* 对于这样的`Person`类型顶点返回其name属性。 >
> 2. 也有一条到某个顶点`LIVES_IN`类型的出边。沿着这条边,可以通过一系列`WITHIN`出边最终到达类型为`Location``name=Europe`的顶点
>
> 对于这样的`Person`类型顶点,返回其`name`属性。
这条查询有几种可行的查询路径。这里给出的描述建议你首先扫描数据库中的所有人,检查每个人的出生地和居住地,然后只返回符合条件的人。 这条查询有几种可行的查询路径。这里给出的描述建议你首先扫描数据库中的所有人,检查每个人的出生地和居住地,然后只返回符合条件的人。
等价地也可以从两个位置顶点开始并向后查找。如果名称属性上有一个索引则可以高效地找到代表美国和欧洲的两个顶点。然后您可以继续查找所有在WITHIN边中的位置美国和欧洲的所有位置地区城市等。最后您可以查找可以通过在某个位置顶点处传入的BORN_IN或LIVES_IN边找到的人员。 等价地,也可以从两个位置顶点开始并向后查找。如果名称属性上有一个索引,则可以高效地找到代表美国和欧洲的两个顶点。然后,您可以继续查找所有在`WITHIN`边中的位置(美国和欧洲的所有位置(州,地区,城市等))。最后,您可以查找可以通过在某个位置顶点处传入的`BORN_IN``LIVES_IN`边找到的人员。
对于声明性查询语言来说,典型的情况是,在编写查询语句时,您不需要指定执行细节:查询优化程序会自动选择预测效率最高的策略,因此您可以继续编写其余的应用程序。 对于声明性查询语言来说,典型的情况是,在编写查询语句时,您不需要指定执行细节:查询优化程序会自动选择预测效率最高的策略,因此您可以继续编写其余的应用程序。
### SQL中的图表查询 ### SQL中的图表查询
例2-2建议可以在关系数据库中表示图形数据。但是如果我们把图形数据放入关系结构中我们是否也可以使用SQL查询它答案是肯定的但有些困难。在关系数据库中您通常会事先知道在查询中需要哪些连接。在图表查询中您可能需要在找到要查找的顶点之前遍历可变数量的边。也就是说JOIN的数量事先并不确定。 [例2-2]()建议可以在关系数据库中表示图形数据。但是如果我们把图形数据放入关系结构中我们是否也可以使用SQL查询它答案是肯定的但有些困难。在关系数据库中您通常会事先知道在查询中需要哪些连接。在图表查询中您可能需要在找到要查找的顶点之前遍历可变数量的边。也就是说连接的数量事先并不确定。
在我们的例子中这发生在Cypher查询中的` - [WITHIN * 0 ..] - >`规则中。一个人的LIVES_IN边缘可以指向任何类型的位置:街道,城市,地区,地区,国家等。城市可以在一个地区,在一个州内的一个地区,在一个国家内的一个州等等.LIVES_IN边缘可以直接指向你正在查找的位置,或者可以在位置层次结构中删除几个级别。 在我们的例子中这发生在Cypher查询中的`() -[:WITHIN*0..]-> ()`规则中。一个人的`LIVES_IN`边缘可以指向任何类型的位置:街道,城市,地区,地区,国家等。城市可以在一个地区,在一个州内的一个地区,在一个国家内的一个州等等。`LIVES_IN`边可以直接指向你正在查找的位置,或者可以在位置层次结构中删除几个级别。
在Cypher中`WITHIN * 0`表示这个事实非常简洁意思是“沿着一个WITHIN边零次或多次”。它就像正则表达式中的*运算符。 在Cypher中`WITHIN * 0`表示这个事实非常简洁:意思是“沿着一个`WITHIN`边,零次或多次”。它就像正则表达式中的`*`运算符。
由于SQL1999查询中可变长度遍历路径的思想可以使用称为递归公用表表达式`WITH RECURSIVE`语法的东西来表示。例2-5显示了同样的查询 - 查找使用这种技术PostgreSQLIBM DB2Oracle和SQL Server支持中从美国移民到欧洲的人的姓名。但是与Cypher相比语法非常笨拙。 由于SQL1999查询中可变长度遍历路径的思想可以使用称为递归公用表表达式`WITH RECURSIVE`语法)的东西来表示。[例2-5]()显示了同样的查询 - 查找使用这种技术PostgreSQLIBM DB2Oracle和SQL Server支持中从美国移民到欧洲的人的姓名。但是与Cypher相比语法非常笨拙。
例2-5 与示例2-4相同的查询使用递归公用表表达式在SQL中表示 **例2-5 与示例2-4相同的查询使用递归CTE表达式在SQL中表示**
```sql ```sql
WITH RECURSIVE WITH RECURSIVE
@ -666,10 +679,10 @@ WITH RECURSIVE
JOIN lives_in_europe ON vertices.vertex_id = lives_in_europe.vertex_id; JOIN lives_in_europe ON vertices.vertex_id = lives_in_europe.vertex_id;
``` ```
* 集合in_usa以Location name=United States作为种子沿着边表从种子集作为起点将所有具有`with_in`边的终点加入种子集,不断递归直到边表内的所有条目都被访问完毕。 * 集合`in_usa`以(`Location name=United States`)作为种子,沿着边表,从种子集作为起点,将所有具有`with_in`边的终点加入种子集,不断递归直到边表内的所有条目都被访问完毕。
* 同理建立点集in_europe获取欧洲的地点列表。 * 同理建立点集`in_europe`,获取欧洲的地点列表。
* 对于in_usa集合中的每个顶点按照传入的born_in边缘来查找出生在美国某个地方的人。 * 对于`in_usa`集合中的每个顶点,按照传入的`born_in`边缘来查找出生在美国某个地方的人。
* 同样对于in_europe集合中的每个顶点请按照传入的lives_in边来查找居住在欧洲的人。 * 同样,对于`in_europe`集合中的每个顶点,请按照传入的`lives_in`边来查找居住在欧洲的人。
* 最后,把在美国出生的人与在欧洲居住的人相交,获取他们的名称。 * 最后,把在美国出生的人与在欧洲居住的人相交,获取他们的名称。
同一个查询可以用一个查询语言写成4行而另一个查询需要29行这说明了不同的数据模型是为不同的应用场景设计的。选择适合应用程序的数据模型非常重要。 同一个查询可以用一个查询语言写成4行而另一个查询需要29行这说明了不同的数据模型是为不同的应用场景设计的。选择适合应用程序的数据模型非常重要。
@ -682,11 +695,12 @@ WITH RECURSIVE
三元组的主体相当于图中的一个顶点。而客体是两件事情之一: 三元组的主体相当于图中的一个顶点。而客体是两件事情之一:
1. 原始数据类型中的值,例如字符串或数字。在这种情况下,三元组的谓词和对象相当于主题顶点上的属性的键和值。例如,lucyage33就像属性{“age”33}的顶点lucy。 1. 原始数据类型中的值,例如字符串或数字。在这种情况下,三元组的谓词和对象相当于主题顶点上的属性的键和值。例如,`(lucy, age, 33)`就像属性`{“age”33}`的顶点lucy。
2. 图中的另一个顶点。在这种情况下,谓词是图中的边,主体是尾部顶点,而对象是顶点。例如,在lucymarriedToalain主语和宾语lucy和alain都是顶点并且谓词marriedTo是连接他们的边的标签。 2. 图中的另一个顶点。在这种情况下,谓词是图中的边,主体是尾部顶点,而对象是顶点。例如,在`(lucy, marriedTo, alain)`主语和宾语`lucy`和`alain`都是顶点,并且谓词`marriedTo`是连接他们的边的标签。
示例2-6显示了与示例2-3相同的数据以称为Turtle的格式Notation3N3[39])的一个子集形式写成三元组。 [例2-6]()显示了与[例2-3]()相同的数据以称为Turtle的格式Notation3N3【39】的一个子集形式写成三元组。
例2-6。图2-5中的数据子集表示为Turtle三元组
**例2-6 图2-5中的数据子集表示为Turtle三元组**
```reStructuredText ```reStructuredText
@prefix : <urn:example:>. @prefix : <urn:example:>.
@ -706,9 +720,11 @@ _:namerica :name "North America"
_:namerica :type :"continent" _:namerica :type :"continent"
``` ```
在这个例子中,图的顶点被写为:`_someName`。这个名字并不意味着这个文件以外的任何东西。它的存在只是帮助我们明确三元组之间的相互引用。当谓词表示边时该对象是一个顶点如_idaho在_usa内。当谓词是一个属性时该对象是一个字符串如_usaname“United States”。 在这个例子中,图的顶点被写为:`_someName`。这个名字并不意味着这个文件以外的任何东西。它的存在只是帮助我们明确三元组之间的相互引用。当谓词表示边时,该对象是一个顶点,如:`_:idaho :within _:usa.`。当谓词是一个属性时,该对象是一个字符串,如:`_:usa :name "United States"`
一遍又一遍地重复相同的主题是相当重复的但幸运的是您可以使用分号来说明关于同一主题的多个事情。这使得Turtle格式相当不错可读性强参见例2-7。
例2-7。在示例2-6 @prefix<urnexample>中写入数据的更简洁的方法。 一遍又一遍地重复相同的主题是相当重复的但幸运的是您可以使用分号来说明关于同一主题的多个事情。这使得Turtle格式相当不错可读性强参见[例2-7]()。
**例2-7 在示例2-6 `@prefix<urnexample>`中写入数据的更简洁的方法。**
``` ```
@prefix : <urn:example:>. @prefix : <urn:example:>.
@ -720,9 +736,11 @@ _:namerica a :Location; :name "North America"; :type "continent".
#### 语义网络 #### 语义网络
如果您阅读更多关于三元组存储的信息您可能会被卷入关于语义网的文章中。三元组存储数据模型完全独立于语义网络例如Datomic [40]是三元组存储,并不声称与它有任何关系。但是,由于在很多人眼中这两者紧密相连,我们应该简要地讨论一下。 如果您阅读更多关于三元组存储的信息您可能会被卷入关于语义网的文章中。三元组存储数据模型完全独立于语义网络例如Datomic 【40】是三元组存储[^vii],并不声称与它有任何关系。但是,由于在很多人眼中这两者紧密相连,我们应该简要地讨论一下。
语义网从本质上讲是一个简单而合理的想法网站已经将信息发布为文字和图片供人类阅读为什么不把它们作为机器可读的数据发布给计算机呢资源描述框架RDF[41]的目的是作为不同网站以一致的格式发布数据的一种机制,允许来自不同网站的数据自动合并成一个数据网络 - 一种互联网范围内的“一切的数据库“。 [^vii]: 从技术上讲Datomic使用的是五元组而不是三元组两个额外的字段是用于版本控制的元数据
语义网从本质上讲是一个简单而合理的想法网站已经将信息发布为文字和图片供人类阅读为什么不把它们作为机器可读的数据发布给计算机呢资源描述框架RDF【41】的目的是作为不同网站以一致的格式发布数据的一种机制允许来自不同网站的数据自动合并成一个数据网络 - 一种互联网范围内的“一切的数据库“。
不幸的是,这个语义网在二十一世纪初被过度使用,但到目前为止还没有显示出在实践中有任何实现的迹象,这使得许多人愤世嫉俗。它也遭受了令人眼花缭乱的缩略词,过于复杂的标准提议和自大。 不幸的是,这个语义网在二十一世纪初被过度使用,但到目前为止还没有显示出在实践中有任何实现的迹象,这使得许多人愤世嫉俗。它也遭受了令人眼花缭乱的缩略词,过于复杂的标准提议和自大。
@ -730,9 +748,9 @@ _:namerica a :Location; :name "North America"; :type "continent".
#### RDF数据模型 #### RDF数据模型
例2-7中使用的Turtle语言是RDF数据的可读格式。有时候RDF也是以XML格式编写的它可以更详细地完成同样的事情参见例2-8。Turtle N3是最好的因为它更容易阅读像Apache Jena [42]这样的工具可以根据需要在不同的RDF格式之间自动转换。 [例2-7]()中使用的Turtle语言是RDF数据的可读格式。有时候RDF也是以XML格式编写的它可以更详细地完成同样的事情参见[例2-8]()。Turtle N3是最好的因为它更容易阅读像Apache Jena 【42】这样的工具可以根据需要在不同的RDF格式之间自动转换。
例2-8。 例2-7的数据用RDF / XML语法表示 **例2-8 例2-7的数据用RDF/XML语法表示**
```xml ```xml
<rdf:RDF xmlns="urn:example:" <rdf:RDF xmlns="urn:example:"
@ -761,13 +779,15 @@ _:namerica a :Location; :name "North America"; :type "continent".
``` ```
RDF有一些奇怪之处因为它是为了在互联网上交换数据而设计的。三元组的主题谓词和对象通常是URI。例如谓词可能是一个URI`<http://my-company.com/namespace#within>`或`<http://my-company.com/namespace#lives_in>`而不仅仅是WITHIN或LIVES_IN。这个设计背后的原因是你应该能够把你的数据和其他人的数据结合起来如果他们给这个单词或者lives_in附加不同的含义你不会得到冲突因为它们的谓词实际上是`<http://other.org/foo#within>`和`<http://other.org/foo#lives_in>`。 RDF有一些奇怪之处因为它是为了在互联网上交换数据而设计的。三元组的主题谓词和对象通常是URI。例如谓词可能是一个URI`<http://my-company.com/namespace#within>`或`<http://my-company.com/namespace#lives_in>`而不仅仅是WITHIN或LIVES_IN。这个设计背后的原因是你应该能够把你的数据和其他人的数据结合起来如果他们给这个单词或者lives_in附加不同的含义你不会得到冲突因为它们的谓词实际上是`<http://other.org/foo#within>`和`<http://other.org/foo#lives_in>`。
从RDF的角度来看URL <http://my-company.com/namespace> 不一定需要能解析成什么东西,它只是一个命名空间。为避免与`http://URL`混淆本节中的示例使用不可解析的URI如urnexamplewithin。幸运的是您只需在文件顶部指定一个前缀然后忘记它。
从RDF的角度来看URL `<http://my-company.com/namespace>` 不一定需要能解析成什么东西,它只是一个命名空间。为避免与`http://URL`混淆本节中的示例使用不可解析的URI如`urnexamplewithin`。幸运的是,您只需在文件顶部指定一个前缀,然后忘记它。
### SPARQL查询语言 ### SPARQL查询语言
SPARQL是 RDF数据模型三元组存储的查询语言[43]。 它是SPARQL协议和RDF查询语言的缩写发音为“sparkle”。它早于Cypher并且由于Cypher的模式匹配是从SPARQL中借用的所以它们看起来非常相似[37]。 SPARQL是RDF数据模型三元组存储的查询语言【43】。 它是SPARQL协议和RDF查询语言的缩写发音为“sparkle”。它早于Cypher并且由于Cypher的模式匹配是从SPARQL中借用的所以它们看起来非常相似【37】。
与从前从美国转移到欧洲的人相同的查询 - 在SPARQL中比在Cypher中更加简洁请参见示例2-9 与从前从美国转移到欧洲的人相同的查询——在SPARQL中比在Cypher中更加简洁参见[例2-9]())。
例2-9。与示例2-4相同的查询用SPARQL表示
**例2-9 与示例2-4相同的查询用SPARQL表示**
```sparql ```sparql
PREFIX : <urn:example:> PREFIX : <urn:example:>
@ -785,36 +805,39 @@ SELECT ?personName WHERE {
?person :bornIn / :within* ?location. # SPARQL ?person :bornIn / :within* ?location. # SPARQL
``` ```
由于RDF不区分属性和边而只是使用两个属性所以可以使用相同的语法来匹配属性。在下面的表达式中变量usa被绑定到任何具有值为字符串“United States”的name属性的顶点: 由于RDF不区分属性和边而只是使用两个属性所以可以使用相同的语法来匹配属性。在下面的表达式中变量usa被绑定到任何具有值为字符串`"United States"`的`name`属性的顶点:
``` ```
(usa {name:'United States'}) # Cypher (usa {name:'United States'}) # Cypher
?usa :name "United States". # SPARQL ?usa :name "United States". # SPARQL
``` ```
SPARQL是一种很好的查询语言 - 即使语义网从来没有出现,它也可以成为应用程序内部使用的强大工具。 SPARQL是一种很好的查询语言——即使语义网从来没有出现它也可以成为应用程序内部使用的强大工具。
#### 图形数据库与网络模型相比较
在第36页的“文档数据库是否重复历史”中我们讨论了CODASYL和关系模型如何竞争解决IMS中的多对多关系问题。乍一看CODASYL的网络模型看起来与图模型相似。 CODASYL是否是图形数据库的第二个变种
不,他们在几个重要方面有所不同:
* 在CODASYL中数据库有一个模式指定哪种记录类型可以嵌套在其他记录类型中。在图形数据库中不存在这样的限制任何顶点都可以具有到其他任何顶点的边。这为应用程序适应不断变化的需求提供了更大的灵活性。
* 在CODASYL中达到特定记录的唯一方法是遍历其中的一个访问路径。在图形数据库中可以通过其唯一ID直接引用任何顶点也可以使用索引来查找具有特定值的顶点。
* 在CODASYL记录的孩子们的有序集合所以数据库的人不得不维持排序其中有用于存储布局的后果并且插入新记录到数据库的应用程序不得不担心的新记录的位置在这些集合中。在图形数据库中顶点和边不是有序的您只能在查询时对结果进行排序
* 在CODASYL中所有查询都是必要的难以编写并且很容易被架构中的变化破坏。在图形数据库中如果需要可以在命令式代码中编写遍历但大多数图形数据库也支持高级声明式查询语言如Cypher或SPARQL。
> #### 图形数据库与网络模型相比较
>
> 在“[文档数据库是否在重蹈覆辙?](#文档数据库是否在重蹈覆辙?)”中我们讨论了CODASYL和关系模型如何竞争解决IMS中的多对多关系问题。乍一看CODASYL的网络模型看起来与图模型相似。 CODASYL是否是图形数据库的第二个变种
>
> 不,他们在几个重要方面有所不同:
>
> * 在CODASYL中数据库有一个模式指定哪种记录类型可以嵌套在其他记录类型中。在图形数据库中不存在这样的限制任何顶点都可以具有到其他任何顶点的边。这为应用程序适应不断变化的需求提供了更大的灵活性。
> * 在CODASYL中达到特定记录的唯一方法是遍历其中的一个访问路径。在图形数据库中可以通过其唯一ID直接引用任何顶点也可以使用索引来查找具有特定值的顶点。
> * 在CODASYL记录的孩子们的有序集合所以数据库的人不得不维持排序其中有用于存储布局的后果并且插入新记录到数据库的应用程序不得不担心的新记录的位置在这些集合中。在图形数据库中顶点和边不是有序的您只能在查询时对结果进行排序
> * 在CODASYL中所有查询都是必要的难以编写并且很容易被架构中的变化破坏。在图形数据库中如果需要可以在命令式代码中编写遍历但大多数图形数据库也支持高级声明式查询语言如Cypher或SPARQL。
>
>
### 基础Datalog ### 基础Datalog
Datalog是比SPARQL或Cypher更古老的语言在20世纪80年代被学者广泛研究[44,45,46]。它在软件工程师中是不太知名的,但是它是重要的,因为它为以后的查询语言提供了基础。 Datalog是比SPARQL或Cypher更古老的语言在20世纪80年代被学者广泛研究【44,45,46】。它在软件工程师中是不太知名的,但是它是重要的,因为它为以后的查询语言提供了基础。
在实践中Datalog在一些数据系统中被使用例如它是Datomic [40]的查询语言而Cascalog [47]是用于查询Hadoop中的大数据集的Datalog实现Datomic和Cascalog使用Datalog的Clojure S表达式语法。 在下面的例子中使用了一个更容易阅读的Prolog语法没有任何功能差异。 在实践中Datalog在一些数据系统中被使用例如它是Datomic 【40】的查询语言而Cascalog 【47】是用于查询Hadoop中的大数据集的Datalog实现[^viii]。
Datalog的数据模型类似于三元组模式但有一点泛化。我们把它写成谓词主语宾语而不是写三元语主语宾语宾语。例2-10显示了如何在Datalog中写入我们的例子中的数据。 [^viii]: Datomic和Cascalog使用Datalog的Clojure S表达式语法。 在下面的例子中使用了一个更容易阅读的Prolog语法没有任何功能差异
例2-10. 图2-5中的数据子集表示为Datalog事实 Datalog的数据模型类似于三元组模式但有一点泛化。我们把它写成谓词主语宾语而不是写三元语主语宾语宾语。[例2-10]()显示了如何在Datalog中写入我们的例子中的数据。
**例2-10 图2-5中的数据子集表示为Datalog事实**
```prolog ```prolog
name(namerica, 'North America'). name(namerica, 'North America').
@ -832,9 +855,9 @@ name(lucy, 'Lucy').
born_in(lucy, idaho). born_in(lucy, idaho).
``` ```
现在我们已经定义了数据我们可以像之前一样编写相同的查询如例2-11所示。它看起来有点不同于Cypher或SPARQL的等价物但是不要让你失望。 Datalog是Prolog的一个子集如果你已经学过计算机科学你可能已经见过。 现在我们已经定义了数据,我们可以像之前一样编写相同的查询,如**例2-11**所示。它看起来有点不同于Cypher或SPARQL的等价物但是不要让你失望。 Datalog是Prolog的一个子集如果你已经学过计算机科学你可能已经见过。
例2-11。与示例2-4相同的查询在Datalog中表示 **例2-11 与示例2-4相同的查询在Datalog中表示**
``` ```
within_recursive(Location, Name) :- name(Location, Name). /* Rule 1 */ within_recursive(Location, Name) :- name(Location, Name). /* Rule 1 */
@ -852,25 +875,25 @@ migrated(Name, BornIn, LivingIn) :- name(Person, Name), /* Rule 3 */
``` ```
Cypher和SPARQL使用SELECT立即跳转但是Datalog一次只需要一小步。我们定义告诉数据库有关新谓词的规则在这里我们定义了两个新的谓词在_recursive和migrated内。这些谓词不是存储在数据库中的三元组而是它们是从数据或其他规则派生而来的。规则可以引用其他规则就像函数可以调用其他函数或者自发地调用自己一样。像这样复杂的查询可以一次构建一小块。 Cypher和SPARQL使用SELECT立即跳转但是Datalog一次只需要一小步。我们定义告诉数据库有关新谓词的规则在这里我们定义了两个新的谓词`_recursive``migrated`内。这些谓词不是存储在数据库中的三元组,而是它们是从数据或其他规则派生而来的。规则可以引用其他规则,就像函数可以调用其他函数或者自发地调用自己一样。像这样,复杂的查询可以一次构建一小块。
在规则中以大写字母开头的单词是变量谓词匹配如Cypher和SPARQL。例如nameLocationName与可变绑定Location = namerica和Name ='North America'的三重名称namerica'North America'匹配。 在规则中以大写字母开头的单词是变量谓词匹配如Cypher和SPARQL。例如`name(Location, Name)`与可变绑定`Location = namerica`和`Name ='North America'`的三重名称`(namerica, 'North America')`匹配。
如果系统可以在 - 操作符的右侧找到与所有谓词相匹配的规则,则适用该规则。当规则适用时,就好像将 - 的左边添加到数据库(用变量替换它们的值)。 如果系统可以在` -` 操作符的右侧找到与所有谓词相匹配的规则,则适用该规则。当规则适用时,就好像将`-`的左边添加到数据库(用变量替换它们的值)。
因此,应用规则的一种可能的方式是: 因此,应用规则的一种可能的方式是:
1. 名称namerica'北美'存在于数据库中故规则1适用。它生成within_recursivenamerica'北美' 1. 名称`(namerica, '北美')`存在于数据库中故规则1适用。它生成`within_recursive(namerica, '北美')`
2. 数据库中存在usanamerica并在之前的步骤中生成namerica'North America'所以适用规则2。它会产生within_recursive美国“北美” 2. 数据库中存在`(usa, namerica)`,并在之前的步骤中生成`(namerica, 'North America')`所以适用规则2。它会产生`within_recursive(美国, “北美”)`
3. 在爱达荷州美国存在于数据库和上一步生成within_recursive美国“北美”所以规则2适用。它产生within_recursive爱达荷州“北美” 3. 在`(爱达荷州, 美国)`存在于数据库和上一步生成`within_recursive(美国, “北美”)`所以规则2适用。它产生`within_recursive(爱达荷州, "北美")`
通过重复应用规则1和2within_recursive谓词可以告诉我们在我们的数据库中包含的北美或任何其他位置名称的所有位置。这个过程如图2-6所示。 通过重复应用规则1和2`within_recursive`谓词可以告诉我们在我们的数据库中包含的北美(或任何其他位置名称)的所有位置。这个过程如[图2-6](img/fig2-6.png)所示。
![](img/fig2-6.png) ![](img/fig2-6.png)
**图2-6 使用示例2-11中的Datalog规则确定爱达荷州在北美。** **图2-6 使用示例2-11中的Datalog规则确定爱达荷州在北美。**
现在规则3可以找到出生在某个地方BornIn的人并住在某个地方LivingIn。通过查询BornIn ='United States'和LivingIn ='Europe'并将此人作为变量Who我们要求Datalog系统找出变量Who可以出现哪些值。因此最后我们得到了与早先的Cypher和SPARQL查询相同的答案。 现在规则3可以找到出生在某个地方BornIn的人并住在某个地方LivingIn。通过查询`BornIn ='United States'``LivingIn ='Europe'`,并将此人作为变量`Who`我们要求Datalog系统找出变量`Who`可以出现哪些值。因此最后我们得到了与早先的Cypher和SPARQL查询相同的答案。
Datalog方法需要对本章讨论的其他查询语言采取不同的思维方式但这是一种非常强大的方法因为规则可以在不同的查询中进行组合和重用。简单的一次性查询不太方便但是如果数据很复杂它可以更好地处理。 Datalog方法需要对本章讨论的其他查询语言采取不同的思维方式但这是一种非常强大的方法因为规则可以在不同的查询中进行组合和重用。简单的一次性查询不太方便但是如果数据很复杂它可以更好地处理。
@ -889,12 +912,12 @@ Datalog方法需要对本章讨论的其他查询语言采取不同的思维方
文档数据库和图数据库有一个共同点,那就是它们通常不会为存储的数据强制实施一个模式,这可以使应用程序更容易适应不断变化的需求。但是应用程序很可能仍假定数据具有一定的结构:这只是模式是明确的(强制写入)还是隐含的(在读取时处理)的问题。 文档数据库和图数据库有一个共同点,那就是它们通常不会为存储的数据强制实施一个模式,这可以使应用程序更容易适应不断变化的需求。但是应用程序很可能仍假定数据具有一定的结构:这只是模式是明确的(强制写入)还是隐含的(在读取时处理)的问题。
每个数据模型都带有自己的查询语言或框架我们讨论了几个例子SQLMapReduceMongoDB的聚合管道CypherSPARQL和Datalog。我们也谈到了CSS和XSL / XPath它们不是数据库查询语言而包含有趣的相似之处。 每个数据模型都带有自己的查询语言或框架我们讨论了几个例子SQLMapReduceMongoDB的聚合管道CypherSPARQL和Datalog。我们也谈到了CSS和XSL/XPath它们不是数据库查询语言而包含有趣的相似之处。
虽然我们已经覆盖了很多地方,但仍然有许多数据模型没有提到。举几个简单的例子: 虽然我们已经覆盖了很多地方,但仍然有许多数据模型没有提到。举几个简单的例子:
* 研究人员使用基因组数据通常需要执行序列相似性搜索这意味着需要一个很长的字符串代表一个DNA分子并将其与一个类似但不完全相同的大型字符串数据库进行匹配。这里所描述的数据库都不能处理这种用法这就是为什么研究人员编写了像GenBank这样的专门的基因组数据库软件的原因[48] * 研究人员使用基因组数据通常需要执行序列相似性搜索这意味着需要一个很长的字符串代表一个DNA分子并将其与一个类似但不完全相同的大型字符串数据库进行匹配。这里所描述的数据库都不能处理这种用法这就是为什么研究人员编写了像GenBank这样的专门的基因组数据库软件的原因【48】
* 粒子物理学家数十年来一直在进行大数据类型的大规模数据分析像大型强子对撞机LHC这样的项目现在可以工作在数百亿兆字节的范围内在这样的规模下需要定制解决方案来阻止硬件成本从失控中解脱出来[49] * 粒子物理学家数十年来一直在进行大数据类型的大规模数据分析像大型强子对撞机LHC这样的项目现在可以工作在数百亿兆字节的范围内在这样的规模下需要定制解决方案来阻止硬件成本从失控中解脱出来【49】
* 全文搜索可以说是一种经常与数据库一起使用的数据模型。信息检索是一个大的专业课题,在本书中我们不会详细介绍,但是我们将在第三章和第三章中介绍搜索指标。 * 全文搜索可以说是一种经常与数据库一起使用的数据模型。信息检索是一个大的专业课题,在本书中我们不会详细介绍,但是我们将在第三章和第三章中介绍搜索指标。
在下一章中,我们将讨论在实现本章描述的数据模型时会发挥的一些权衡。 在下一章中,我们将讨论在实现本章描述的数据模型时会发挥的一些权衡。

View File

@ -11,74 +11,77 @@
[TOC] [TOC]
应用程序不可避免地随时间而变化。新产品的推出,对需求的深入理解,或者商业环境的变化,总会伴随着*功能(feature)*的增增改改。[第一章](ch1.md)介绍了*可演化性(evolvability)*的概念:应该尽力构建能灵活适应变化的系统(参阅“可演化性:拥抱变化”)。 应用程序不可避免地随时间而变化。新产品的推出,对需求的深入理解,或者商业环境的变化,总会伴随着**功能feature**的增增改改。[第一章](ch1.md)介绍了[**可演化性(evolvability)**](ch1.md#可演化性:拥抱变化)的概念:应该尽力构建能灵活适应变化的系统(参阅“[可演化性:拥抱变化]()”)。
在大多数情况下,修改应用程序的功能也意味着需要更改其存储的数据:可能需要使用新的字段或记录类型,或者以新方式展示现有数据。 在大多数情况下,修改应用程序的功能也意味着需要更改其存储的数据:可能需要使用新的字段或记录类型,或者以新方式展示现有数据。
我们在[第二章](ch2.md)讨论的数据模型有不同的方法来应对这种变化。关系数据库通常假定数据库中的所有数据都遵循一个模式:尽管可以更改该模式(通过模式迁移,即`ALTER`语句),但是在任何时间点都有且仅有一个正确的模式。相比之下,*读时模式 schema-on-read*(或 “无模式” schemaless)数据库不会强制一个模式,因此数据库可以包含在不同时间写入的新老数据格式的混合(参阅 “文档模型中的模式灵活性” )。 我们在[第二章](ch2.md)讨论的数据模型有不同的方法来应对这种变化。关系数据库通常假定数据库中的所有数据都遵循一个模式:尽管可以更改该模式(通过模式迁移,即`ALTER`语句),但是在任何时间点都有且仅有一个正确的模式。相比之下,**读时模式schema-on-read**(或 **无模式schemaless**)数据库不会强制一个模式,因此数据库可以包含在不同时间写入的新老数据格式的混合(参阅 “文档模型中的模式灵活性” )。
当数据格式(format)或模式(schema)发生变化时,通常需要对应用程序代码进行相应的更改(例如,为记录添加新字段,然后修改程序开始读写该字段)。但在大型应用程序中,代码变更通常不会立即完成: 当数据**格式format**或**模式schema**发生变化时,通常需要对应用程序代码进行相应的更改(例如,为记录添加新字段,然后修改程序开始读写该字段)。但在大型应用程序中,代码变更通常不会立即完成:
* 对于服务端server-side应用程序可能需要执行*滚动升级 (rolling upgrade)*(也称为*阶段发布 staged rollout*),一次将新版本部署到少数几个节点,检查新版本是否运行正常,然后逐渐部完所有的节点。这样无需中断服务即可部署新版本,为频繁发布提供了可行性,从而带来更好的可演化性。 * 对于**服务端server-side**应用程序,可能需要执行**滚动升级 rolling upgrade**(也称为**阶段发布staged rollout**),一次将新版本部署到少数几个节点,检查新版本是否运行正常,然后逐渐部完所有的节点。这样无需中断服务即可部署新版本,为频繁发布提供了可行性,从而带来更好的可演化性。
* 对于客户端client-side应用程序升不升级就要看用户的心情了。用户可能相当长一段时间里都不会去升级软件。 * 对于**客户端client-side**应用程序,升不升级就要看用户的心情了。用户可能相当长一段时间里都不会去升级软件。
这意味着,新旧版本的代码,以及新旧数据格式可能会在系统中同时共处。系统想要继续顺利运行,就需要保持*双向兼容性* 这意味着,新旧版本的代码,以及新旧数据格式可能会在系统中同时共处。系统想要继续顺利运行,就需要保持**双向兼容性**
* **向后兼容** (backward compatibility) ***向后兼容 (backward compatibility)***
新代码可以读旧数据。 新代码可以读旧数据。
* **向前兼容** (forward compatibility) ***向前兼容 (forward compatibility)***
旧代码可以读新数据。 旧代码可以读新数据。
向后兼容性通常并不难实现:新代码的作者当然知道由旧代码使用的数据格式,因此可以显示地处理它(最简单的办法是,保留旧代码即可读取旧数据)。 向后兼容性通常并不难实现:新代码的作者当然知道由旧代码使用的数据格式,因此可以显示地处理它(最简单的办法是,保留旧代码即可读取旧数据)。
向前兼容性可能会更棘手,因为旧版的程序需要忽略新版数据格式中新增的部分。 向前兼容性可能会更棘手,因为旧版的程序需要忽略新版数据格式中新增的部分。
本章中将介绍几种编码数据的格式,包括 JSONXMLProtocol BuffersThrift和Avro。尤其将关注这些格式如何应对模式变化以及它们如何对新旧代码数据需要共存的系统提供支持。然后将讨论如何使用这些格式进行数据存储和通信在Web服务中具象状态传输REST和远程过程调用RPC以及消息传递系统如Actor和消息队列 本章中将介绍几种编码数据的格式,包括 JSONXMLProtocol BuffersThrift和Avro。尤其将关注这些格式如何应对模式变化以及它们如何对新旧代码数据需要共存的系统提供支持。然后将讨论如何使用这些格式进行数据存储和通信在Web服务中**具象状态传输REST****远程过程调用RPC**,以及**消息传递系统**如Actor和消息队列
## 编码数据的格式 ## 编码数据的格式
程序通常(至少)使用两种形式的数据: 程序通常(至少)使用两种形式的数据:
1. 在内存中,数据保存在对象,结构体,列表,数组,哈希表,树等中。 这些数据结构针对CPU的高效访问和操作进行了优化通常使用指针 1. 在内存中,数据保存在对象,结构体,列表,数组,哈希表,树等中。 这些数据结构针对CPU的高效访问和操作进行了优化通常使用指针
2. 如果要将数据写入文件,或通过网络发送,则必须将其*编码encode*为某种自包含的字节序列例如JSON文档。 由于每个进程都有自己独立的地址空间,一个进程中的指针对任何其他进程都没有意义,所以这个字节序列表示会与通常在内存中使用的数据结构完全不同。 2. 如果要将数据写入文件,或通过网络发送,则必须将其**编码encode**为某种自包含的字节序列例如JSON文档。 由于每个进程都有自己独立的地址空间,一个进程中的指针对任何其他进程都没有意义,所以这个字节序列表示会与通常在内存中使用的数据结构完全不同[^i]
所以,需要在两种表示之间进行某种类型的翻译。 从内存中表示到字节序列的转换称为**编码(Encoding)**(也称为**序列化 serialization**或**编组 marshalling**),反过来称为**解码(Decoding)****解析Parsing****反序列化 deserialization****反编组 unmarshalling**)。 [^i]: 除一些特殊情况外,例如某些内存映射文件或直接在压缩数据上操作(如“[列压缩](ch4.md#列压缩)”中所述)。
所以,需要在两种表示之间进行某种类型的翻译。 从内存中表示到字节序列的转换称为**编码Encoding**(也称为**序列化serialization**或**编组marshalling**),反过来称为**解码Decoding**[^ii]**解析Parsing****反序列化deserialization****反编组() unmarshalling**[^译i]。
[^ii]: 请注意,**编码encode**与**加密encryption**无关。 本书不讨论加密。
[^译i]: Marshal与Serialization的区别Marshal不仅传输对象的状态而且会一起传输对象的方法相关代码
> #### 术语冲突 > #### 术语冲突
> 不幸的是,在第七章:事务(Transaction)的上下文里,序列化(Serialization)这个术语也出现了而且具有完全不同的含义。尽管序列化可能是更常见的术语为了避免术语重载本书中坚持使用编码Encoding表达此含义。 > 不幸的是,在[第七章](ch7.md)**事务Transaction**的上下文里,**序列化Serialization**这个术语也出现了,而且具有完全不同的含义。尽管序列化可能是更常见的术语,为了避免术语重载,本书中坚持使用**编码Encoding**表达此含义。
>
> 译者注Marshal与Serialization的区别Marshal不仅传输对象的状态而且会一起传输对象的方法相关代码
这是一个常见的问题,因而有许多库和编码格式可供选择。 首先让我们概览一下。 这是一个常见的问题,因而有许多库和编码格式可供选择。 首先让我们概览一下。
### 语言特定的格式 ### 语言特定的格式
许多编程语言都内建了将内存对象编码为字节序列的支持。例如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】
因此,除非临时使用,采用语言内置编码通常是一个坏主意。 因此,除非临时使用,采用语言内置编码通常是一个坏主意。
### JSONXML和二进制变体 ### JSONXML和二进制变体
谈到可以被许多编程语言编写和读取的标准化编码JSON和XML是显眼的竞争者。它们广为人知广受支持也“广受憎恶”。 XML经常被批评为过于冗长和不必要的复杂[9]。 JSON倍受欢迎主要由于它在Web浏览器中的内置支持通过成为JavaScript的一个子集以及相对于XML的简单性。 CSV是另一种流行的与语言无关的格式尽管功能较弱。 谈到可以被许多编程语言编写和读取的标准化编码JSON和XML是显眼的竞争者。它们广为人知广受支持也“广受憎恶”。 XML经常被批评为过于冗长和不必要的复杂【9】。 JSON倍受欢迎主要由于它在Web浏览器中的内置支持通过成为JavaScript的一个子集以及相对于XML的简单性。 CSV是另一种流行的与语言无关的格式尽管功能较弱。
JSONXML和CSV是文本格式因此具有人类可读性尽管语法是一个热门辩题。除了表面的语法问题之外它们也有一些微妙的问题 JSONXML和CSV是文本格式因此具有人类可读性尽管语法是一个热门辩题。除了表面的语法问题之外它们也有一些微妙的问题
* 数字的编码多有歧义之处。XML和CSV不能区分数字和字符串除非引用外部模式。 JSON虽然区分字符串和数字但不区分整数和浮点数而且不能指定精度。 * 数字的编码多有歧义之处。XML和CSV不能区分数字和字符串除非引用外部模式。 JSON虽然区分字符串和数字但不区分整数和浮点数而且不能指定精度。
* 当处理大量数据时,这个问题更严重了。例如,大于$2^{53}$的整数不能在IEEE 754双精度浮点数中精确表示因此在使用浮点数例如JavaScript的语言进行分析时这些数字会变得不准确。 Twitter上有一个大于$2^{53}$的数字的例子它使用一个64位的数字来标识每条推文。 Twitter API返回的JSON包含了两次推特ID一次是JSON数字一次是十进制字符串以此避免JavaScript程序无法正确解析数字的问题[10] * 当处理大量数据时,这个问题更严重了。例如,大于$2^{53}$的整数不能在IEEE 754双精度浮点数中精确表示因此在使用浮点数例如JavaScript的语言进行分析时这些数字会变得不准确。 Twitter上有一个大于$2^{53}$的数字的例子它使用一个64位的数字来标识每条推文。 Twitter API返回的JSON包含了两种推特ID一个JSON数字另一个是十进制字符串以此避免JavaScript程序无法正确解析数字的问题【10】
* JSON和XML对Unicode字符串即人类可读的文本有很好的支持但是它们不支持二进制数据不带字符编码(character encoding)的字节序列。二进制串是很实用的功能所以人们通过使用Base64将二进制数据编码为文本来绕开这个限制。模式然后用于表示该值应该被解释为Base64编码。这个工作但它有点hacky并增加了33的数据大小。 XML [11]和JSON [12]都有可选的模式支持。这些模式语言相当强大,所以学习和实现起来相当复杂。 XML模式的使用相当普遍但许多基于JSON的工具嫌麻烦才不会使用模式。由于数据的正确解释例如数字和二进制字符串取决于模式中的信息因此不使用XML / JSON模式的应用程序可能需要对相应的编码/解码逻辑进行硬编码。 * JSON和XML对Unicode字符串即人类可读的文本有很好的支持但是它们不支持二进制数据不带字符编码(character encoding)的字节序列。二进制串是很实用的功能所以人们通过使用Base64将二进制数据编码为文本来绕开这个限制。模式然后用于表示该值应该被解释为Base64编码。这个工作但它有点hacky并增加了33的数据大小。 XML 【11】和JSON 【12】都有可选的模式支持。这些模式语言相当强大,所以学习和实现起来相当复杂。 XML模式的使用相当普遍但许多基于JSON的工具嫌麻烦才不会使用模式。由于数据的正确解释例如数字和二进制字符串取决于模式中的信息因此不使用XML/JSON模式的应用程序可能需要对相应的编码/解码逻辑进行硬编码。
* CSV没有任何模式因此应用程序需要定义每行和每列的含义。如果应用程序更改添加新的行或列则必须手动处理该变更。 CSV也是一个相当模糊的格式如果一个值包含逗号或换行符会发生什么。尽管其转义规则已经被正式指定[13],但并不是所有的解析器都正确的实现了标准。 * CSV没有任何模式因此应用程序需要定义每行和每列的含义。如果应用程序更改添加新的行或列则必须手动处理该变更。 CSV也是一个相当模糊的格式如果一个值包含逗号或换行符会发生什么。尽管其转义规则已经被正式指定【13】,但并不是所有的解析器都正确的实现了标准。
尽管存在这些缺陷但JSONXML和CSV已经足够用于很多目的。特别是作为数据交换格式即将数据从一个组织发送到另一个组织它们很可能仍然很受欢迎。这种情况下只要人们对格式是什么意见一致格式多么美观或者高效就没有关系。让不同的组织达成一致的难度超过了其他大多数问题。 尽管存在这些缺陷但JSONXML和CSV已经足够用于很多目的。特别是作为数据交换格式即将数据从一个组织发送到另一个组织它们很可能仍然很受欢迎。这种情况下只要人们对格式是什么意见一致格式多么美观或者高效就没有关系。**让不同的组织达成一致的难度超过了其他大多数问题。**
#### 二进制编码 #### 二进制编码
@ -86,7 +89,7 @@ JSONXML和CSV是文本格式因此具有人类可读性尽管语法是
JSON比XML简洁但与二进制格式一比还是太占地方。这一事实导致大量二进制编码版本JSON & XML的出现JSONMessagePackBSONBJSONUBJSONBISON和Smile等例如WBXML和Fast Infoset。这些格式已经被各种各样的领域所采用但是没有一个像JSON和XML的文本版本那样被广泛采用。 JSON比XML简洁但与二进制格式一比还是太占地方。这一事实导致大量二进制编码版本JSON & XML的出现JSONMessagePackBSONBJSONUBJSONBISON和Smile等例如WBXML和Fast Infoset。这些格式已经被各种各样的领域所采用但是没有一个像JSON和XML的文本版本那样被广泛采用。
这些格式中的一些扩展了一组数据类型例如区分整数和浮点数或者增加对二进制字符串的支持另一方面它们没有盖面JSON / XML的数据模型。特别是由于它们没有规定模式所以它们需要在编码数据中包含所有的对象字段名称。也就是说**例4-1**中的JSON文档的二进制编码中需要在某处包含字符串`userName``favoriteNumber`和`interest`。 这些格式中的一些扩展了一组数据类型例如区分整数和浮点数或者增加对二进制字符串的支持另一方面它们没有盖面JSON / XML的数据模型。特别是由于它们没有规定模式所以它们需要在编码数据中包含所有的对象字段名称。也就是说[例4-1]()中的JSON文档的二进制编码中需要在某处包含字符串`userName``favoriteNumber`和`interest`。
**例4-1 本章中用于展示二进制编码的示例记录** **例4-1 本章中用于展示二进制编码的示例记录**
@ -98,9 +101,9 @@ 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 2. 第二个字节`0xa8`表示接下来是**8**字节长的字符串(最高四位= 0x08
3. 接下来八个字节是ASCII字符串形式的字段名称`userName`。由于之前已经指明长度,不需要任何标记来标识字符串的结束位置(或者任何转义)。 3. 接下来八个字节是ASCII字符串形式的字段名称`userName`。由于之前已经指明长度,不需要任何标记来标识字符串的结束位置(或者任何转义)。
4. 接下来的七个字节对前缀为`0xa6`的六个字母的字符串值`Martin`进行编码,依此类推。 4. 接下来的七个字节对前缀为`0xa6`的六个字母的字符串值`Martin`进行编码,依此类推。
@ -114,12 +117,10 @@ JSON比XML简洁但与二进制格式一比还是太占地方。这一事
**图4-1 使用MessagePack编码的记录例4-1** **图4-1 使用MessagePack编码的记录例4-1**
### Thrift与Protocol Buffers ### Thrift与Protocol Buffers
Apache Thrift [15]和Protocol Buffersprotobuf[16]是基于相同原理的二进制编码库。 Protocol Buffers最初是在Google开发的Thrift最初是在Facebook开发的并且在2007 - 2008年都是开源的[17] Apache Thrift 【15】和Protocol Buffersprotobuf【16】是基于相同原理的二进制编码库。 Protocol Buffers最初是在Google开发的Thrift最初是在Facebook开发的并且在2007~2008年都是开源的【17】
Thrift和Protocol Buffers都需要一个模式来编码任何数据。要在Thrift的例4-1中对数据进行编码可以使用Thrift接口定义语言IDL来描述模式如下所示 Thrift和Protocol Buffers都需要一个模式来编码任何数据。要在Thrift的[例4-1]()中对数据进行编码可以使用Thrift**接口定义语言IDL**来描述模式,如下所示:
```c ```c
struct Person { struct Person {
@ -139,28 +140,26 @@ message Person {
} }
``` ```
Thrift和Protocol Buffers每一个都带有一个代码生成工具它采用了类似于这里所示的模式定义并且生成了以各种编程语言实现模式的类[18]。您的应用程序代码可以调用此生成的代码来对模式的记录进行编码或解码。 Thrift和Protocol Buffers每一个都带有一个代码生成工具它采用了类似于这里所示的模式定义并且生成了以各种编程语言实现模式的类【18】。您的应用程序代码可以调用此生成的代码来对模式的记录进行编码或解码。
用这个模式编码的数据是什么样的令人困惑的是Thrift有两种不同的二进制编码格式[^iii]分别称为BinaryProtocol和CompactProtocol。先来看看BinaryProtocol。这种格式的编码示例4-1需要59个字节如图4-2 [19]所示。 用这个模式编码的数据是什么样的令人困惑的是Thrift有两种不同的二进制编码格式[^iii]分别称为BinaryProtocol和CompactProtocol。先来看看BinaryProtocol。使用这种格式的编码来编码[例4-1]()中的消息只需要59个字节如[图4-2](img/fig4-2.png)所示【19】。
![](img/fig4-2.png) ![](img/fig4-2.png)
**图4-2 使用Thrift二进制协议编码的记录** **图4-2 使用Thrift二进制协议编码的记录**
[^iii]: 实际上Thrift有三种二进制协议CompactProtocol和DenseProtocol尽管DenseProtocol只支持C ++实现,所以不算作跨语言[18]。 除此之外它还有两种不同的基于JSON的编码格式[19]。 真逗! [^iii]: 实际上Thrift有三种二进制协议CompactProtocol和DenseProtocol尽管DenseProtocol只支持C ++实现,所以不算作跨语言[18]。 除此之外它还有两种不同的基于JSON的编码格式【19】。 真逗!
与图4-1类似每个字段都有一个类型注释用于指示它是一个字符串整数列表等还可以根据需要指定长度字符串的长度列表中的项目数 。出现在数据中的字符串“Martin”“daydreaming”“hacking”也被编码为ASCII或者说UTF-8与之前类似。 [图4-1](Img/fig4-1.png)类似,每个字段都有一个类型注释(用于指示它是一个字符串,整数,列表等),还可以根据需要指定长度(字符串的长度,列表中的项目数) 。出现在数据中的字符串`(“Martin”, “daydreaming”, “hacking”)`也被编码为ASCII或者说UTF-8与之前类似。
图4-1相比最大的区别是没有字段名userNamefavoriteNumberinterest。相反编码数据包含字段标签它们是数字12和3。这些是模式定义中出现的数字。字段标记就像字段的别名 - 它们是说我们正在谈论的字段的一种紧凑的方式,而不必拼出字段名称。 [图4-1](img/fig4-1.png)相比,最大的区别是没有字段名`(userName, favoriteNumber, interest)`。相反,编码数据包含字段标签,它们是数字`(1, 2和3)`。这些是模式定义中出现的数字。字段标记就像字段的别名 - 它们是说我们正在谈论的字段的一种紧凑的方式,而不必拼出字段名称。
Thrift CompactProtocol编码在语义上等同于BinaryProtocol但是如图4-3所示它只将相同的信息打包成只有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) ![](img/fig4-3.png)
**图4-3 使用Thrift压缩协议编码的记录** **图4-3 使用Thrift压缩协议编码的记录**
最后Protocol Buffers只有一种二进制编码格式对相同的数据进行编码如图4-4所示。 它的打包方式稍有不同但与Thrift的CompactProtocol非常相似。 Protobuf将同样的记录塞进了33个字节中。 最后Protocol Buffers只有一种二进制编码格式对相同的数据进行编码[图4-4](img/fig4-4.png)所示。 它的打包方式稍有不同但与Thrift的CompactProtocol非常相似。 Protobuf将同样的记录塞进了33个字节中。
![](img/fig4-4.png) ![](img/fig4-4.png)
@ -176,21 +175,21 @@ Thrift CompactProtocol编码在语义上等同于BinaryProtocol但是如图4-
您可以添加新的字段到架构,只要您给每个字段一个新的标签号码。如果旧的代码(不知道你添加的新的标签号码)试图读取新代码写入的数据,包括一个新的字段,其标签号码不能识别,它可以简单地忽略该字段。数据类型注释允许解析器确定需要跳过的字节数。这保持了前向兼容性:旧代码可以读取由新代码编写的记录。 您可以添加新的字段到架构,只要您给每个字段一个新的标签号码。如果旧的代码(不知道你添加的新的标签号码)试图读取新代码写入的数据,包括一个新的字段,其标签号码不能识别,它可以简单地忽略该字段。数据类型注释允许解析器确定需要跳过的字节数。这保持了前向兼容性:旧代码可以读取由新代码编写的记录。
向后兼容性呢?只要每个字段都有一个唯一的标签号码,新的代码总是可以读取旧的数据,因为标签号码仍然具有相同的含义。唯一的细节是,如果你添加一个新的领域,你不能要求。如果您要添加一个字段并将其设置为必需,那么如果新代码读取旧代码写入的数据,则该检查将失败,因为旧代码不会写入您添加的新字段。因此,为了保持向后兼容性,在模式的初始部署之后添加的每个字段必须是可选的或具有默认值。 向后兼容性呢?只要每个字段都有一个唯一的标签号码,新的代码总是可以读取旧的数据,因为标签号码仍然具有相同的含义。唯一的细节是,如果你添加一个新的领域,你不能要求。如果您要添加一个字段并将其设置为必需,那么如果新代码读取旧代码写入的数据,则该检查将失败,因为旧代码不会写入您添加的新字段。因此,为了保持向后兼容性,在模式的初始部署之后**添加的每个字段必须是可选的或具有默认值**
删除一个字段就像添加一个字段,倒退和向前兼容性问题相反。这意味着您只能删除一个可选的字段(必填字段永远不能删除),而且您不能再次使用相同的标签号码(因为您可能仍然有数据写在包含旧标签号码的地方,而该字段必须被新代码忽略)。 删除一个字段就像添加一个字段,倒退和向前兼容性问题相反。这意味着您只能删除一个可选的字段(必填字段永远不能删除),而且您不能再次使用相同的标签号码(因为您可能仍然有数据写在包含旧标签号码的地方,而该字段必须被新代码忽略)。
#### 数据类型和模式演变 #### 数据类型和模式演变
如何改变字段的数据类型?这可能是可能的 - 检查文件的细节 - 但是有一个风险,价值将失去精度或被扼杀。例如假设你将一个32位的整数变成一个64位的整数。新代码可以轻松读取旧代码写入的数据因为解析器可以用零填充任何缺失的位。但是如果旧代码读取由新代码写入的数据则旧代码仍使用32位变量来保存该值。如果解码的64位值不适合32位则它将被截断。 如何改变字段的数据类型?这可能是可能的——检查文件的细节——但是有一个风险,值将失去精度或被扼杀。例如假设你将一个32位的整数变成一个64位的整数。新代码可以轻松读取旧代码写入的数据因为解析器可以用零填充任何缺失的位。但是如果旧代码读取由新代码写入的数据则旧代码仍使用32位变量来保存该值。如果解码的64位值不适合32位则它将被截断。
Protobuf的一个奇怪的细节是它没有列表或数组数据类型而是有一个字段的重复标记这是第三个选项旁边必要和可选。如图4-4所示重复字段的编码正如它所说的那样同一个字段标记只是简单地出现在记录中。这具有很好的效果可以将可选单值字段更改为重复多值字段。读取旧数据的新代码会看到一个包含零个或一个元素的列表取决于该字段是否存在。读取新数据的旧代码只能看到列表的最后一个元素。 Protobuf的一个奇怪的细节是它没有列表或数组数据类型而是有一个字段的重复标记这是第三个选项旁边必要和可选。如[图4-4](img/fig4-4.png)所示,重复字段的编码正如它所说的那样:同一个字段标记只是简单地出现在记录中。这具有很好的效果,可以将可选(单值)字段更改为重复(多值)字段。读取旧数据的新代码会看到一个包含零个或一个元素的列表(取决于该字段是否存在)。读取新数据的旧代码只能看到列表的最后一个元素。
Thrift有一个专用的列表数据类型它使用列表元素的数据类型进行参数化。这不允许Protocol Buffers所做的从单值到多值的相同演变但是它具有支持嵌套列表的优点。 Thrift有一个专用的列表数据类型它使用列表元素的数据类型进行参数化。这不允许Protocol Buffers所做的从单值到多值的相同演变但是它具有支持嵌套列表的优点。
### Avro ### 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更易于机器读取。
@ -218,7 +217,7 @@ record Person {
} }
``` ```
首先,请注意架构中没有标签号码。 如果我们使用这个模式编码我们的例子记录例4-1Avro二进制编码只有32个字节长这是我们所见过的所有编码中最紧凑的。 编码字节序列的分解如图4-5所示。 首先,请注意架构中没有标签号码。 如果我们使用这个模式编码我们的例子记录([例4-1]()Avro二进制编码只有32个字节长这是我们所见过的所有编码中最紧凑的。 编码字节序列的分解如[图4-5](img/fig4-5.png)所示。
如果您检查字节序列,您可以看到没有什么可以识别字段或其数据类型。 编码只是由连在一起的值组成。 一个字符串只是一个长度前缀后跟UTF-8字节但是在被包含的数据中没有任何内容告诉你它是一个字符串。 它可以是一个整数,也可以是其他的整数。 整数使用可变长度编码与Thrift的CompactProtocol相同进行编码。 如果您检查字节序列,您可以看到没有什么可以识别字段或其数据类型。 编码只是由连在一起的值组成。 一个字符串只是一个长度前缀后跟UTF-8字节但是在被包含的数据中没有任何内容告诉你它是一个字符串。 它可以是一个整数,也可以是其他的整数。 整数使用可变长度编码与Thrift的CompactProtocol相同进行编码。
@ -236,7 +235,7 @@ record Person {
当一个应用程序想要解码一些数据(从一个文件或数据库读取数据,从网络接收数据等)时,它希望数据在某个模式中,这就是读者的模式。这是应用程序代码所依赖的模式,在应用程序的构建过程中,代码可能是从该模式生成的。 当一个应用程序想要解码一些数据(从一个文件或数据库读取数据,从网络接收数据等)时,它希望数据在某个模式中,这就是读者的模式。这是应用程序代码所依赖的模式,在应用程序的构建过程中,代码可能是从该模式生成的。
Avro的关键思想是作者的模式和读者的模式不必是相同的 - 他们只需要兼容。当数据解码读取Avro库通过并排查看作者的模式和读者的模式并将数据从作者的模式转换到读者的模式来解决差异。 Avro规范[20]确切地定义了这种解析的工作原理如图4-6所示。 Avro的关键思想是作者的模式和读者的模式不必是相同的 - 他们只需要兼容。当数据解码读取Avro库通过并排查看作者的模式和读者的模式并将数据从作者的模式转换到读者的模式来解决差异。 Avro规范【20】确切地定义了这种解析的工作原理,如[图4-6](img/fig4-6.png)所示。
例如,如果作者的模式和读者的模式的字段顺序不同,这是没有问题的,因为模式解析通过字段名匹配字段。如果读取数据的代码遇到出现在作者模式中但不在读者模式中的字段,则忽略它。如果读取数据的代码需要某个字段,但是作者的模式不包含该名称的字段,则使用在读者模式中声明的默认值填充。 例如,如果作者的模式和读者的模式的字段顺序不同,这是没有问题的,因为模式解析通过字段名匹配字段。如果读取数据的代码遇到出现在作者模式中但不在读者模式中的字段,则忽略它。如果读取数据的代码需要某个字段,但是作者的模式不包含该名称的字段,则使用在读者模式中声明的默认值填充。
@ -244,15 +243,15 @@ Avro的关键思想是作者的模式和读者的模式不必是相同的 - 他
**图4-6 一个Avro Reader解决读写模式的差异** **图4-6 一个Avro Reader解决读写模式的差异**
#### 模式演变规则 #### 模式演变规则
使用Avro向前兼容性意味着您可以将新版本的架构作为编写器并将旧版本的架构作为读者。相反向后兼容意味着你可以有一个作为读者的新版本的模式和作为作者的旧版本。 使用Avro向前兼容性意味着您可以将新版本的架构作为编写器并将旧版本的架构作为读者。相反向后兼容意味着你可以有一个作为读者的新版本的模式和作为作者的旧版本。
为了保持兼容性,您只能添加或删除具有默认值的字段。 我们的Avro模式中的字段favourNumber的默认值为null。例如假设您添加一个默认值的字段所以这个新的字段存在于新的模式中而不是旧的。当使用新模式的阅读器读取使用旧模式写入的记录时将为缺少的字段填充默认值。 为了保持兼容性,您只能添加或删除具有默认值的字段。 我们的Avro模式中的字段`favourNumber`的默认值为`null`)。例如,假设您添加一个默认值的字段,所以这个新的字段存在于新的模式中,而不是旧的。当使用新模式的阅读器读取使用旧模式写入的记录时,将为缺少的字段填充默认值。
如果你要添加一个没有默认值的字段新的阅读器将无法读取旧作者写的数据所以你会破坏向后兼容性。如果您要删除没有默认值的字段旧的阅读器将无法读取新作者写入的数据因此您会打破兼容性。在一些编程语言中null是任何变量可以接受的默认值但在Avro中并不是这样如果要允许一个字段为null则必须使用联合类型。例如`union {nulllongstring}`字段;表示该字段可以是数字或字符串,也可以是`null`。如果它是union的分支之一那么只能使用null作为默认值.iv这比默认情况下可以为null是更加冗长的但是通过明确什么可以和不可以是什么有助于防止错误null [22]。 如果你要添加一个没有默认值的字段新的阅读器将无法读取旧作者写的数据所以你会破坏向后兼容性。如果您要删除没有默认值的字段旧的阅读器将无法读取新作者写入的数据因此您会打破兼容性。在一些编程语言中null是任何变量可以接受的默认值但在Avro中并不是这样如果要允许一个字段为`null`,则必须使用联合类型。例如,`union {nulllongstring}`字段;表示该字段可以是数字或字符串,也可以是`null`。如果它是union的分支之一那么只能使用null作为默认值[^iv]。这比默认情况下可以为`null`是更加冗长的,但是通过明确什么可以和不可以是什么,有助于防止错误的`null` 【22】。
[^iv]: 确切地说默认值必须是联合的第一个分支的类型尽管这是Avro的特定限制而不是联合类型的一般特征。
因此Avro没有像Protocol Buffers和Thrift那样的`optional`和`required`标记(它有联合类型和默认值)。 因此Avro没有像Protocol Buffers和Thrift那样的`optional`和`required`标记(它有联合类型和默认值)。
@ -265,23 +264,23 @@ Avro的关键思想是作者的模式和读者的模式不必是相同的 - 他
* 有很多记录的大文件 * 有很多记录的大文件
Avro的一个常见用途 - 尤其是在Hadoop环境中 - 用于存储包含数百万条记录的大文件,所有记录都使用相同的模式进行编码。 我们将在第10章讨论这种情况。在这种情况下该文件的作者可以在文件的开头只包含一次作者的模式。 Avro指定一个文件格式对象容器文件来做到这一点。 Avro的一个常见用途 - 尤其是在Hadoop环境中 - 用于存储包含数百万条记录的大文件,所有记录都使用相同的模式进行编码。 (我们将在[第10章](ch10.md)讨论这种情况。)在这种情况下,该文件的作者可以在文件的开头只包含一次作者的模式。 Avro指定一个文件格式对象容器文件来做到这一点。
* 支持独立写入的记录的数据库 * 支持独立写入的记录的数据库
在一个数据库中,不同的记录可能会在不同的时间点使用不同的作者的模式编写 - 你不能假定所有的记录都有相同的模式。最简单的解决方案是在每个编码记录的开始处包含一个版本号,并在数据库中保留一个模式版本列表。读者可以获取记录,提取版本号,然后从数据库中获取该版本号的作者模式。使用该作者的模式,它可以解码记录的其余部分。 例如Espresso [23]就是这样工作的。) 在一个数据库中,不同的记录可能会在不同的时间点使用不同的作者的模式编写 - 你不能假定所有的记录都有相同的模式。最简单的解决方案是在每个编码记录的开始处包含一个版本号,并在数据库中保留一个模式版本列表。读者可以获取记录,提取版本号,然后从数据库中获取该版本号的作者模式。使用该作者的模式,它可以解码记录的其余部分。 例如Espresso 【23】就是这样工作的。)
* 通过网络连接发送记录 * 通过网络连接发送记录
当两个进程通过双向网络连接进行通信时,他们可以在连接设置上协商模式版本,然后在连接的生命周期中使用该模式。 Avro RPC协议参阅第131页上的“通过服务的数据流REST和RPC”如此工作。 当两个进程通过双向网络连接进行通信时,他们可以在连接设置上协商模式版本,然后在连接的生命周期中使用该模式。 Avro RPC协议参阅“[通过服务的数据流REST和RPC](#通过服务的数据流REST和RPC)”)如此工作。
具有模式版本的数据库在任何情况下都是非常有用的,因为它充当文档并为您提供了检查模式兼容性的机会[24]。作为版本号,你可以使用一个简单的递增整数,或者你可以使用模式的散列。 具有模式版本的数据库在任何情况下都是非常有用的,因为它充当文档并为您提供了检查模式兼容性的机会【24】。作为版本号,你可以使用一个简单的递增整数,或者你可以使用模式的散列。
#### 动态生成的模式 #### 动态生成的模式
与Protocol Buffers和Thrift相比Avro方法的一个优点是架构不包含任何标签号码。但为什么这很重要在模式中保留一些数字有什么问题 与Protocol Buffers和Thrift相比Avro方法的一个优点是架构不包含任何标签号码。但为什么这很重要在模式中保留一些数字有什么问题
不同之处在于Avro对动态生成的模式更友善。例如假如你有一个关系数据库你想要把它的内容转储到一个文件中并且你想使用二进制格式来避免前面提到的文本格式JSONCSVSQL的问题。如果你使用Avro你可以很容易地从关系模式生成一个Avro模式在我们之前看到的JSON表示中并使用该模式对数据库内容进行编码并将其全部转储到Avro对象容器文件[25]中。您为每个数据库表生成一个记录模式每个列成为该记录中的一个字段。数据库中的列名称映射到Avro中的字段名称。 不同之处在于Avro对动态生成的模式更友善。例如假如你有一个关系数据库你想要把它的内容转储到一个文件中并且你想使用二进制格式来避免前面提到的文本格式JSONCSVSQL的问题。如果你使用Avro你可以很容易地从关系模式生成一个Avro模式在我们之前看到的JSON表示中并使用该模式对数据库内容进行编码并将其全部转储到Avro对象容器文件【25】中。您为每个数据库表生成一个记录模式每个列成为该记录中的一个字段。数据库中的列名称映射到Avro中的字段名称。
现在如果数据库模式发生变化例如一个表中添加了一列删除了一列则可以从更新的数据库模式生成新的Avro模式并在新的Avro模式中导出数据。数据导出过程不需要注意模式的改变 - 每次运行时都可以简单地进行模式转换。任何读取新数据文件的人都会看到记录的字段已经改变,但是由于字段是通过名字来标识的,所以更新的作者的模式仍然可以与旧的读者模式匹配。 现在如果数据库模式发生变化例如一个表中添加了一列删除了一列则可以从更新的数据库模式生成新的Avro模式并在新的Avro模式中导出数据。数据导出过程不需要注意模式的改变 - 每次运行时都可以简单地进行模式转换。任何读取新数据文件的人都会看到记录的字段已经改变,但是由于字段是通过名字来标识的,所以更新的作者的模式仍然可以与旧的读者模式匹配。
@ -295,13 +294,13 @@ Thrift和Protobuf依赖于代码生成在定义了模式之后可以使用
Avro为静态类型编程语言提供了可选的代码生成功能但是它也可以在不生成任何代码的情况下使用。如果你有一个对象容器文件它嵌入了作者的模式你可以简单地使用Avro库打开它并以与查看JSON文件相同的方式查看数据。该文件是自描述的因为它包含所有必要的元数据。 Avro为静态类型编程语言提供了可选的代码生成功能但是它也可以在不生成任何代码的情况下使用。如果你有一个对象容器文件它嵌入了作者的模式你可以简单地使用Avro库打开它并以与查看JSON文件相同的方式查看数据。该文件是自描述的因为它包含所有必要的元数据。
这个属性特别适用于动态类型的数据处理语言如Apache Pig [26]。在Pig中您可以打开一些Avro文件开始分析它们并编写派生数据集以Avro格式输出文件而无需考虑模式。 这个属性特别适用于动态类型的数据处理语言如Apache Pig 【26】。在Pig中您可以打开一些Avro文件开始分析它们并编写派生数据集以Avro格式输出文件而无需考虑模式。
### 模式的优点 ### 模式的优点
正如我们所看到的Protocol BuffersThrift和Avro都使用模式来描述二进制编码格式。他们的模式语言比XML模式或者JSON模式简单得多它支持更详细的验证规则例如“该字段的字符串值必须与该正则表达式匹配”或“该字段的整数值必须在0和100之间“。由于Protocol BuffersThrift和Avro实现起来更简单使用起来也更简单所以它们已经发展到支持相当广泛的编程语言。 正如我们所看到的Protocol BuffersThrift和Avro都使用模式来描述二进制编码格式。他们的模式语言比XML模式或者JSON模式简单得多它支持更详细的验证规则例如“该字段的字符串值必须与该正则表达式匹配”或“该字段的整数值必须在0和100之间“。由于Protocol BuffersThrift和Avro实现起来更简单使用起来也更简单所以它们已经发展到支持相当广泛的编程语言。
这些编码所基于的想法绝不是新的。例如它们与ASN.1有很多相似之处它是1984年首次被标准化的模式定义语言[27]。它被用来定义各种网络协议其二进制编码DER仍然被用于编码SSL证书X.509),例如[28]。 ASN.1支持使用标签号码的模式演进类似于Protocol Buf-fers和Thrift [29]。然而这也是非常复杂和严重的文件记录所以ASN.1可能不是新应用程序的好选择。 这些编码所基于的想法绝不是新的。例如它们与ASN.1有很多相似之处它是1984年首次被标准化的模式定义语言【27】。它被用来定义各种网络协议其二进制编码DER仍然被用于编码SSL证书X.509),例如【28】。 ASN.1支持使用标签号码的模式演进类似于Protocol Buf-fers和Thrift 【29】。然而这也是非常复杂和严重的文件记录所以ASN.1可能不是新应用程序的好选择。
许多数据系统也为其数据实现某种专有的二进制编码。例如大多数关系数据库都有一个网络协议您可以通过该协议向数据库发送查询并获取响应。这些协议通常特定于特定的数据库并且数据库供应商提供将来自数据库的网络协议的响应解码为内存数据结构的驱动程序例如使用ODBC或JDBC API 许多数据系统也为其数据实现某种专有的二进制编码。例如大多数关系数据库都有一个网络协议您可以通过该协议向数据库发送查询并获取响应。这些协议通常特定于特定的数据库并且数据库供应商提供将来自数据库的网络协议的响应解码为内存数据结构的驱动程序例如使用ODBC或JDBC API
@ -323,9 +322,9 @@ Avro为静态类型编程语言提供了可选的代码生成功能但是它
这是一个相当抽象的概念 - 数据可以通过多种方式从一个流程流向另一个流程。谁编码数据,谁解码?在本章的其余部分中,我们将探讨数据如何在流程之间流动的一些最常见的方式: 这是一个相当抽象的概念 - 数据可以通过多种方式从一个流程流向另一个流程。谁编码数据,谁解码?在本章的其余部分中,我们将探讨数据如何在流程之间流动的一些最常见的方式:
* 通过数据库(参阅第129页的“通过数据库的数据流” * 通过数据库(参阅“[通过数据库的数据流](#通过数据库的数据流)
* 通过服务调用(参阅第131页的“通过服务传输数据流REST和RPC” * 通过服务调用(参阅“[通过服务传输数据流REST和RPC](#通过服务传输数据流REST和RPC)
* 通过异步消息传递(参阅第136页的“消息传递数据流” * 通过异步消息传递(参阅“[消息传递数据流](#消息传递数据流)
@ -345,8 +344,6 @@ Avro为静态类型编程语言提供了可选的代码生成功能但是它
解决这个问题不是一个难题,你只需要意识到它。 解决这个问题不是一个难题,你只需要意识到它。
![](img/fig4-7.png) ![](img/fig4-7.png)
**图4-7 当较旧版本的应用程序更新以前由较新版本的应用程序编写的数据时,如果不小心,数据可能会丢失。** **图4-7 当较旧版本的应用程序更新以前由较新版本的应用程序编写的数据时,如果不小心,数据可能会丢失。**
@ -357,21 +354,21 @@ Avro为静态类型编程语言提供了可选的代码生成功能但是它
在部署应用程序的新版本(至少是服务器端应用程序)时,您可能会在几分钟内完全用新版本替换旧版本。数据库内容也是如此:五年前的数据仍然存在于原始编码中,除非您已经明确地重写了它。这种观察有时被总结为数据超出代码。 在部署应用程序的新版本(至少是服务器端应用程序)时,您可能会在几分钟内完全用新版本替换旧版本。数据库内容也是如此:五年前的数据仍然存在于原始编码中,除非您已经明确地重写了它。这种观察有时被总结为数据超出代码。
将数据重写(迁移)到一个新的模式当然是可能的,但是在一个大数据集上执行是一个昂贵的事情,所以大多数数据库如果可能的话就避免它。大多数关系数据库都允许简单的模式更改,例如添加一个默认值为空的新列,而不重写现有数据[^v]读取旧行时,数据库将填充编码数据中缺少的任何列的空值在磁盘上。 LinkedIn的文档数据库Espresso使用Avro存储允许它使用Avro的模式演变规则[23] 将数据重写(迁移)到一个新的模式当然是可能的,但是在一个大数据集上执行是一个昂贵的事情,所以大多数数据库如果可能的话就避免它。大多数关系数据库都允许简单的模式更改,例如添加一个默认值为空的新列,而不重写现有数据[^v]读取旧行时,数据库将填充编码数据中缺少的任何列的空值在磁盘上。 LinkedIn的文档数据库Espresso使用Avro存储允许它使用Avro的模式演变规则【23】
因此,架构演变允许整个数据库看起来好像是用单个模式编码的,即使底层存储可能包含用模式的各种历史版本编码的记录。 因此,架构演变允许整个数据库看起来好像是用单个模式编码的,即使底层存储可能包含用模式的各种历史版本编码的记录。
[^v]: 除了MySQL即使并非真的必要它也经常会重写整个表正如第39页的“文档模型中的架构灵活性”中所提到的。 [^v]: 除了MySQL即使并非真的必要它也经常会重写整个表正如“[文档模型中的架构灵活性](ch3.md#文档模型中的灵活性)”中所提到的。
#### 归档存储 #### 归档存储
也许您不时为数据库创建一个快照,例如备份或加载到数据仓库(参阅第91页的“数据仓库”)。在这种情况下,即使源数据库中的原始编码包含来自不同时代的模式版本的混合,数据转储通常也将使用最新模式进行编码。既然你正在复制数据,那么你可能会一直对数据的副本进行编码。 也许您不时为数据库创建一个快照,例如备份或加载到数据仓库(参阅“[数据仓库](ch3.md#数据仓库)”)。在这种情况下,即使源数据库中的原始编码包含来自不同时代的模式版本的混合,数据转储通常也将使用最新模式进行编码。既然你正在复制数据,那么你可能会一直对数据的副本进行编码。
由于数据转储是一次写入的而且以后是不可变的所以Avro对象容器文件等格式非常适合。这也是一个很好的机会可以将数据编码为面向分析的列式格式例如Parquet请参阅第97页的“列压缩” 由于数据转储是一次写入的而且以后是不可变的所以Avro对象容器文件等格式非常适合。这也是一个很好的机会可以将数据编码为面向分析的列式格式例如Parquet请参阅第97页的“[列压缩](ch3.md#列压缩)”)。
在第10章中我们将详细讨论在档案存储中使用数据。 [第10章](ch10.md)中,我们将详细讨论在档案存储中使用数据。
@ -379,53 +376,51 @@ Avro为静态类型编程语言提供了可选的代码生成功能但是它
当您需要通过网络进行通信的进程时安排该通信的方式有几种。最常见的安排是有两个角色客户端和服务器。服务器通过网络公开API并且客户端可以连接到服务器以向该API发出请求。服务器公开的API被称为服务。 当您需要通过网络进行通信的进程时安排该通信的方式有几种。最常见的安排是有两个角色客户端和服务器。服务器通过网络公开API并且客户端可以连接到服务器以向该API发出请求。服务器公开的API被称为服务。
Web以这种方式工作客户Web浏览器向Web服务器发出请求使GET请求下载HTMLCSSJavaScript图像等并向POST请求提交数据到服务器。 API包含一组标准的协议和数据格式HTTPURLSSL / TLSHTML等。由于网络浏览器网络服务器和网站作者大多同意这些标准您可以使用任何网络浏览器访问任何网站至少在理论上 Web以这种方式工作客户Web浏览器向Web服务器发出请求使GET请求下载HTMLCSSJavaScript图像等并向POST请求提交数据到服务器。 API包含一组标准的协议和数据格式HTTPURLSSL/TLSHTML等。由于网络浏览器网络服务器和网站作者大多同意这些标准您可以使用任何网络浏览器访问任何网站至少在理论上
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 architectureSOA,最近被改进和更名为微服务体系结构[31,32] 此外服务器本身可以是另一个服务的客户端例如典型的Web应用服务器充当数据库的客户端。这种方法通常用于将大型应用程序按照功能区域分解为较小的服务这样当一个服务需要来自另一个服务的某些功能或数据时就会向另一个服务发出请求。这种构建应用程序的方式传统上被称为**面向服务的体系结构service-oriented architectureSOA**,最近被改进和更名为**微服务架构 **【31,32】
在某些方面服务类似于数据库它们通常允许客户端提交和查询数据。但是虽然数据库允许使用我们在第2章 中讨论的查询语言进行任意查询但是服务公开了一个特定于应用程序的API它只允许由服务的业务逻辑应用程序代码预定的输入和输出[33 ]。这种限制提供了一定程度的封装:服务可以对客户可以做什么和不可以做什么施加细粒度的限制。 在某些方面服务类似于数据库它们通常允许客户端提交和查询数据。但是虽然数据库允许使用我们在第2章 中讨论的查询语言进行任意查询但是服务公开了一个特定于应用程序的API它只允许由服务的业务逻辑应用程序代码预定的输入和输出【33】。这种限制提供了一定程度的封装:服务可以对客户可以做什么和不可以做什么施加细粒度的限制。
面向服务/微服务架构的一个关键设计目标是通过使服务独立部署和演化来使应用程序更易于更改和维护。例如每个服务应该由一个团队拥有并且该团队应该能够经常发布新版本的服务而不必与其他团队协调。换句话说我们应该期望服务器和客户端的旧版本和新版本同时运行因此服务器和客户端使用的数据编码必须在不同版本的服务API之间兼容 - 正是我们所做的本章一直在谈论。 面向服务/微服务架构的一个关键设计目标是通过使服务独立部署和演化来使应用程序更易于更改和维护。例如每个服务应该由一个团队拥有并且该团队应该能够经常发布新版本的服务而不必与其他团队协调。换句话说我们应该期望服务器和客户端的旧版本和新版本同时运行因此服务器和客户端使用的数据编码必须在不同版本的服务API之间兼容——正是我们所做的本章一直在谈论。
#### Web服务 #### Web服务
当HTTP被用作与服务交谈的底层协议时它被称为Web服务。这可能是一个小错误因为Web服务不仅在Web上使用而且在几个不同的环境中使用。例如 **当服务使用HTTP作为底层通信协议时可称之为Web服务**。这可能是一个小错误因为Web服务不仅在Web上使用而且在几个不同的环境中使用。例如
1.运行在用户设备上的客户端应用程序例如移动设备上的本地应用程序或使用Ajax的JavaScript web应用程序通过HTTP向服务发出请求。这些请求通常通过公共互联网进行。 1. 运行在用户设备上的客户端应用程序例如移动设备上的本地应用程序或使用Ajax的JavaScript web应用程序通过HTTP向服务发出请求。这些请求通常通过公共互联网进行。
2. 一种服务向同一组织拥有的另一项服务提出请求,这些服务通常位于同一数据中心内,作为面向服务/微型架构的一部分。 (支持这种用例的软件有时被称为**中间件middleware**。)
3. 一种服务通过互联网向不同组织所拥有的服务提出请求。这用于不同组织后端系统之间的数据交换。此类别包括由在线服务如信用卡处理系统提供的公共API或用于共享访问用户数据的OAuth。
2.一种服务向同一组织拥有的另一项服务提出请求,这些服务通常位于同一数据中心内,作为面向服务/微型架构的一部分。 (支持这种用例的软件有时被称为中间件。) 有两种流行的Web服务方法REST和SOAP。他们在哲学方面几乎是截然相反的往往是各自支持者之间的激烈辩论即使在每个阵营内也有很多争论。 例如,**HATEOAS超媒体作为应用程序状态的引擎**经常引发讨论【35】。)
3.一种服务通过互联网向不同组织所拥有的服务提出请求。这用于不同组织后端系统之间的数据交换。此类别包括由在线服务如信用卡处理系统提供的公共API或用于共享访问用户数据的OAuth REST不是一个协议而是一个基于HTTP原则的设计哲学【34,35】。它强调简单的数据格式使用URL来标识资源并使用HTTP功能进行缓存控制身份验证和内容类型协商。与SOAP相比REST已经越来越受欢迎至少在跨组织服务集成的背景下【36】并经常与微服务相关[31]。根据REST原则设计的API称为RESTful
有两种流行的Web服务方法REST和SOAP。他们在哲学方面几乎是截然相反的往往是各自支持者之间的激烈辩论即使在每个阵营内也有很多争论。 例如HATEOAS超媒体作为应用程序状态的引擎经常引发讨论[35]。) 相比之下SOAP是用于制作网络API请求的基于XML的协议 尽管首字母缩写词相似SOAP并不是SOA的要求。 SOAP是一种特殊的技术而SOA是构建系统的一般方法。。虽然它最常用于HTTP但其目的是独立于HTTP并避免使用大多数HTTP功能。相反它带有庞大而复杂的多种相关标准Web服务框架称为`WS-*`它们增加了各种功能【37】。
REST不是一个协议而是一个基于HTTP原则的设计哲学[34,35]。它强调简单的数据格式使用URL来标识资源并使用HTTP功能进行缓存控制身份验证和内容类型协商。与SOAP相比REST已经越来越受欢迎至少在跨组织服务集成的背景下[36],并经常与微服务相关[31]。根据REST原则设计的API称为RESTful SOAP Web服务的API使用称为Web服务描述语言WSDL的基于XML的语言来描述。 WSDL支持代码生成客户端可以使用本地类和方法调用编码为XML消息并由框架再次解码访问远程服务。这在静态类型编程语言中非常有用但在动态类型编程语言中很少参阅“[代码生成和动态类型化语言](#代码生成和动态类型化语言)”)
相比之下SOAP是用于制作网络API请求的基于XML的协议 尽管首字母缩写词相似SOAP并不是SOA的要求。 SOAP是一种特殊的技术而SOA是构建系统的一般方法。。虽然它最常用于HTTP但其目的是独立于HTTP并避免使用大多数HTTP功能。相反它带有庞大而复杂的多种相关标准Web服务框架称为WS- *),它们增加了各种功能[37] 由于WSDL的设计不是人类可读的而且由于SOAP消息通常是手动构建的过于复杂所以SOAP的用户在很大程度上依赖于工具支持代码生成和IDE【38】。对于SOAP供应商不支持的编程语言的用户来说与SOAP服务的集成是困难的
SOAP Web服务的API使用称为Web服务描述语言WSDL的基于XML的语言来描述。 WSDL支持代码生成客户端可以使用本地类和方法调用编码为XML消息并由框架再次解码访问远程服务。这在静态类型编程语言中非常有用但在动态类型编程语言中很少请参阅“代码生成和动态类型化语言”第125页 尽管SOAP及其各种扩展表面上是标准化的但是不同厂商的实现之间的互操作性往往会造成问题【39】。由于所有这些原因尽管许多大型企业仍然使用SOAP但在大多数小公司中已经不再受到青睐
由于WSDL的设计不是人类可读的而且由于SOAP消息通常是手动构建的过于复杂所以SOAP的用户在很大程度上依赖于工具支持代码生成和IDE [38]。对于SOAP供应商不支持的编程语言的用户来说与SOAP服务的集成是困难的。 REST风格的API倾向于更简单的方法通常涉及较少的代码生成和自动化工具。定义格式如OpenAPI也称为Swagger 【40】可用于描述RESTful API并生成文档。
尽管SOAP及其各种扩展表面上是标准化的但是不同厂商的实现之间的互操作性往往会造成问题[39]。由于所有这些原因尽管许多大型企业仍然使用SOAP但在大多数小公司中已经不再受到青睐。
REST风格的API倾向于更简单的方法通常涉及较少的代码生成和自动化工具。定义格式如OpenAPI也称为Swagger [40]可用于描述RESTful API并生成文档。
#### 远程过程调用RPC的问题 #### 远程过程调用RPC的问题
Web服务仅仅是通过网络进行API请求的一系列技术的最新版本其中许多技术受到了大量的炒作但是存在严重的问题。 Enterprise JavaBeansEJB和Java的远程方法调用RMI仅限于Java。分布式组件对象模型DCOM仅限于Microsoft平台。公共对象请求代理体系结构CORBA过于复杂不提供前向或后向兼容性[41]。 Web服务仅仅是通过网络进行API请求的一系列技术的最新版本其中许多技术受到了大量的炒作但是存在严重的问题。 Enterprise JavaBeansEJB和Java的**远程方法调用RMI**仅限于Java。**分布式组件对象模型DCOM**仅限于Microsoft平台。**公共对象请求代理体系结构CORBA**过于复杂,不提供前向或后向兼容性[41]。
所有这些都是基于远程过程调用RPC的思想该过程调用自20世纪70年代以来一直存在[42]。 RPC模型试图向远程网络服务发出请求看起来与在同一进程中调用编程语言中的函数或方法相同这种抽象称为位置透明。尽管RPC起初看起来很方便但这种方法根本上是有缺陷的[43,44]。网络请求与本地函数调用非常不同: 所有这些都是基于**远程过程调用RPC**的思想该过程调用自20世纪70年代以来一直存在【42】。 RPC模型试图向远程网络服务发出请求看起来与在同一进程中调用编程语言中的函数或方法相同这种抽象称为位置透明。尽管RPC起初看起来很方便但这种方法根本上是有缺陷的【43,44】。网络请求与本地函数调用非常不同:
* 本地函数调用是可预测的,并且成功或失败,这仅取决于受您控制的参数。网络请求是不可预知的:由于网络问题,请求或响应可能会丢失,或者远程计算机可能很慢或不可用,这些问题完全不在您的控制范围之内。网络问题是常见的,所以你必须预测他们,例如通过重试失败的请求。 * 本地函数调用是可预测的,并且成功或失败,这仅取决于受您控制的参数。网络请求是不可预知的:由于网络问题,请求或响应可能会丢失,或者远程计算机可能很慢或不可用,这些问题完全不在您的控制范围之内。网络问题是常见的,所以你必须预测他们,例如通过重试失败的请求。
* 本地函数调用要么返回结果,要么抛出异常,或者永远不返回(因为进入无限循环或进程崩溃)。网络请求有另一个可能的结果:由于超时,它可能会返回没有结果。在这种情况下,你根本不知道发生了什么:如果你没有得到来自远程服务的响应,你无法知道请求是否通过。 我们将在第8章更详细地讨论这个问题。 * 本地函数调用要么返回结果,要么抛出异常,或者永远不返回(因为进入无限循环或进程崩溃)。网络请求有另一个可能的结果:由于超时,它可能会返回没有结果。在这种情况下,你根本不知道发生了什么:如果你没有得到来自远程服务的响应,你无法知道请求是否通过。 (我们将在[第8章](ch8.md)更详细地讨论这个问题。)
* 如果您重试失败的网络请求,可能会发生请求实际上正在通过,只有响应丢失。在这种情况下,重试将导致该操作被执行多次,除非您在协议中引入除重(*幂等 idempotence*)机制。本地函数调用没有这个问题。 我们在第十一章更详细地讨论幂等性) * 如果您重试失败的网络请求,可能会发生请求实际上正在通过,只有响应丢失。在这种情况下,重试将导致该操作被执行多次,除非您在协议中引入除重(**幂等idempotence**)机制。本地函数调用没有这个问题。 (在[第十一章](ch11.md)更详细地讨论幂等性)
* 每次调用本地功能时,通常需要大致相同的时间来执行。网络请求比函数调用要慢得多,而且其延迟也是非常可变的:在不到一毫秒的时间内它可能会完成,但是当网络拥塞或者远程服务超载时,可能需要几秒钟的时间完全一样的东西。 * 每次调用本地功能时,通常需要大致相同的时间来执行。网络请求比函数调用要慢得多,而且其延迟也是非常可变的:在不到一毫秒的时间内它可能会完成,但是当网络拥塞或者远程服务超载时,可能需要几秒钟的时间完全一样的东西。
* 调用本地函数时,可以高效地将引用(指针)传递给本地内存中的对象。当你发出一个网络请求时,所有这些参数都需要被编码成可以通过网络发送的一系列字节。没关系,如果参数是像数字或字符串这样的基本类型,但是对于较大的对象很快就会变成问题。 * 调用本地函数时,可以高效地将引用(指针)传递给本地内存中的对象。当你发出一个网络请求时,所有这些参数都需要被编码成可以通过网络发送的一系列字节。没关系,如果参数是像数字或字符串这样的基本类型,但是对于较大的对象很快就会变成问题。
客户端和服务可以用不同的编程语言实现所以RPC框架必须将数据类型从一种语言翻译成另一种语言。这可能会捅出大篓子因为不是所有的语言都具有相同的类型 - 例如回想一下JavaScript的数字大于$2^{53}$的问题(参阅第114页上的“JSONXML和二进制变体”。用单一语言编写的单个进程中不存在此问题。 客户端和服务可以用不同的编程语言实现所以RPC框架必须将数据类型从一种语言翻译成另一种语言。这可能会捅出大篓子因为不是所有的语言都具有相同的类型 - 例如回想一下JavaScript的数字大于$2^{53}$的问题(参阅“[JSONXML和二进制变体](#JSONXML和二进制变体)”)。用单一语言编写的单个进程中不存在此问题。
所有这些因素意味着尝试使远程服务看起来像编程语言中的本地对象一样毫无意义,因为这是一个根本不同的事情。 REST的部分吸引力在于它并不试图隐藏它是一个网络协议的事实尽管这似乎并没有阻止人们在REST之上构建RPC库 所有这些因素意味着尝试使远程服务看起来像编程语言中的本地对象一样毫无意义,因为这是一个根本不同的事情。 REST的部分吸引力在于它并不试图隐藏它是一个网络协议的事实尽管这似乎并没有阻止人们在REST之上构建RPC库
@ -433,9 +428,9 @@ Web服务仅仅是通过网络进行API请求的一系列技术的最新版本
尽管有这样那样的问题RPC不会消失。在本章提到的所有编码的基础上构建了各种RPC框架例如Thrift和Avro带有RPC支持gRPC是使用Protocol Buffers的RPC实现Finagle也使用ThriftRest.li使用JSON over HTTP。 尽管有这样那样的问题RPC不会消失。在本章提到的所有编码的基础上构建了各种RPC框架例如Thrift和Avro带有RPC支持gRPC是使用Protocol Buffers的RPC实现Finagle也使用ThriftRest.li使用JSON over HTTP。
这种新一代的RPC框架更加明确的是远程请求与本地函数调用不同。例如Finagle和Rest.li使用futurespromises来封装可能失败的异步操作。`Futures`还可以简化需要并行发出多项服务的情况,并将其结果合并[45]。 gRPC支持流其中一个调用不仅包括一个请求和一个响应还包括一系列的请求和响应[46] 这种新一代的RPC框架更加明确的是远程请求与本地函数调用不同。例如Finagle和Rest.li使用futurespromises来封装可能失败的异步操作。`Futures`还可以简化需要并行发出多项服务的情况,并将其结果合并【45】。 gRPC支持流其中一个调用不仅包括一个请求和一个响应还包括一系列的请求和响应【46】
其中一些框架还提供服务发现即允许客户端找出在哪个IP地址和端口号上可以找到特定的服务。我们将在第214页的“请求路由”中回到这个主题。 其中一些框架还提供服务发现即允许客户端找出在哪个IP地址和端口号上可以找到特定的服务。我们将在“[请求路由](ch6.md#请求路由)”中回到这个主题。
使用二进制编码格式的自定义RPC协议可以实现比通用的JSON over REST更好的性能。但是RESTful API还有其他一些显着的优点对于实验和调试只需使用Web浏览器或命令行工具curl无需任何代码生成或软件安装即可向其请求它是受支持的所有的主流编程语言和平台还有大量可用的工具服务器缓存负载平衡器代理防火墙监控调试工具测试工具等的生态系统。由于这些原因REST似乎是公共API的主要风格。 RPC框架的主要重点在于同一组织拥有的服务之间的请求通常在同一数据中心内。 使用二进制编码格式的自定义RPC协议可以实现比通用的JSON over REST更好的性能。但是RESTful API还有其他一些显着的优点对于实验和调试只需使用Web浏览器或命令行工具curl无需任何代码生成或软件安装即可向其请求它是受支持的所有的主流编程语言和平台还有大量可用的工具服务器缓存负载平衡器代理防火墙监控调试工具测试工具等的生态系统。由于这些原因REST似乎是公共API的主要风格。 RPC框架的主要重点在于同一组织拥有的服务之间的请求通常在同一数据中心内。
@ -446,12 +441,12 @@ Web服务仅仅是通过网络进行API请求的一系列技术的最新版本
RPC方案的前后向兼容性属性从它使用的编码方式中继承 RPC方案的前后向兼容性属性从它使用的编码方式中继承
* ThriftgRPC协议缓冲区和Avro RPC可以根据相应编码格式的兼容性规则进行演变。 * ThriftgRPC协议缓冲区和Avro RPC可以根据相应编码格式的兼容性规则进行演变。
* 在SOAP中请求和响应是使用XML模式指定的。这些可以演变但有一些微妙的陷阱[47] * 在SOAP中请求和响应是使用XML模式指定的。这些可以演变但有一些微妙的陷阱【47】
* RESTful API通常使用JSON没有正式指定的模式用于响应以及用于请求的JSON或URI编码/表单编码的请求参数。添加可选的请求参数并向响应对象添加新的字段通常被认为是保持兼容性的改变。 * 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】
### 消息传递中的数据流 ### 消息传递中的数据流
@ -471,15 +466,15 @@ RPC方案的前后向兼容性属性从它使用的编码方式中继承
#### 消息掮客 #### 消息掮客
过去信息掮客主要是TIBCOIBM WebSphere和webMethods等公司的商业软件的秀场。最近像RabbitMQActiveMQHornetQNATS和Apache Kafka这样的开源实现已经流行起来。我们将在第11章中对它们进行更详细的比较。 过去信息掮客主要是TIBCOIBM WebSphere和webMethods等公司的商业软件的秀场。最近像RabbitMQActiveMQHornetQNATS和Apache Kafka这样的开源实现已经流行起来。我们将在[第11章](ch11.md)中对它们进行更详细的比较。
详细的交付语义因实现和配置而异,但通常情况下,消息代理的使用方式如下:一个进程将消息发送到指定的队列或主题,代理确保将消息传递给一个或多个消费者或订阅者到那个队列或主题。在同一主题上可以有许多生产者和许多消费者。 详细的交付语义因实现和配置而异,但通常情况下,消息代理的使用方式如下:一个进程将消息发送到指定的队列或主题,代理确保将消息传递给一个或多个消费者或订阅者到那个队列或主题。在同一主题上可以有许多生产者和许多消费者。
一个主题只提供单向数据流。但是消费者本身可能会将消息发布到另一个主题上因此可以将它们链接在一起就像我们将在第11章中看到的那样或者发送给原始消息的发送者使用的回复队列允许请求/响应数据流类似于RPC 一个主题只提供单向数据流。但是,消费者本身可能会将消息发布到另一个主题上(因此,可以将它们链接在一起,就像我们将在[第11章](ch11.md)中看到的那样),或者发送给原始消息的发送者使用的回复队列(允许请求/响应数据流类似于RPC
消息代理通常不会执行任何特定的数据模型 - 消息只是包含一些元数据的字节序列,因此您可以使用任何编码格式。如果编码是向后兼容的,则您可以灵活地更改发行商和消费者的独立编码,并以任意顺序进行部署。 消息代理通常不会执行任何特定的数据模型 - 消息只是包含一些元数据的字节序列,因此您可以使用任何编码格式。如果编码是向后兼容的,则您可以灵活地更改发行商和消费者的独立编码,并以任意顺序进行部署。
如果消费者重新发布消息到另一个主题则可能需要小心保留未知字段以防止前面在数据库环境中描述的问题图4-7 如果消费者重新发布消息到另一个主题,则可能需要小心保留未知字段,以防止前面在数据库环境中描述的问题([图4-7](img/fig4-7.png))。
#### 分布式的Actor框架 #### 分布式的Actor框架
@ -493,9 +488,9 @@ actor模型是单个进程中并发的编程模型。逻辑被封装在角色中
三个流行的分布式actor框架处理消息编码如下 三个流行的分布式actor框架处理消息编码如下
* 默认情况下Akka使用Java的内置序列化不提供前向或后向兼容性。 但是,你可以用类似缓冲区的东西替代它,从而获得滚动升级的能力[50] * 默认情况下Akka使用Java的内置序列化不提供前向或后向兼容性。 但是,你可以用类似缓冲区的东西替代它,从而获得滚动升级的能力【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】
@ -533,53 +528,36 @@ actor模型是单个进程中并发的编程模型。逻辑被封装在角色中
1. “[The Python 3.4.3 Standard Library Reference Manual](https://docs.python.org/3/library/pickle.html),” *docs.python.org*, February 2015. 1. “[The Python 3.4.3 Standard Library Reference Manual](https://docs.python.org/3/library/pickle.html),” *docs.python.org*, February 2015.
1. “[EsotericSoftware/kryo](https://github.com/EsotericSoftware/kryo),” 1. “[EsotericSoftware/kryo](https://github.com/EsotericSoftware/kryo),” *github.com*, October 2014.
*github.com*, October 2014.
1. “[CWE-502: Deserialization of Untrusted Data](http://cwe.mitre.org/data/definitions/502.html),” Common Weakness Enumeration, *cwe.mitre.org*, 1. “[CWE-502: Deserialization of Untrusted Data](http://cwe.mitre.org/data/definitions/502.html),” Common Weakness Enumeration, *cwe.mitre.org*,
July 30, 2014. July 30, 2014.
1. Steve Breen: 1. Steve Breen: “[What Do WebLogic, WebSphere, JBoss, Jenkins, OpenNMS, and Your Application Have in Common? This Vulnerability](http://foxglovesecurity.com/2015/11/06/what-do-weblogic-websphere-jboss-jenkins-opennms-and-your-application-have-in-common-this-vulnerability/),” *foxglovesecurity.com*, November 6, 2015.
“[What Do WebLogic, WebSphere, JBoss, Jenkins, OpenNMS, and Your Application Have in Common? This Vulnerability](http://foxglovesecurity.com/2015/11/06/what-do-weblogic-websphere-jboss-jenkins-opennms-and-your-application-have-in-common-this-vulnerability/),” *foxglovesecurity.com*, November 6, 2015.
1. Patrick McKenzie: 1. Patrick McKenzie: “[What the Rails Security Issue Means for Your Startup](http://www.kalzumeus.com/2013/01/31/what-the-rails-security-issue-means-for-your-startup/),” *kalzumeus.com*, January 31, 2013.
“[What the Rails Security Issue Means for Your Startup](http://www.kalzumeus.com/2013/01/31/what-the-rails-security-issue-means-for-your-startup/),” *kalzumeus.com*, January 31, 2013.
1. Eishay Smith: 1. Eishay Smith: “[jvm-serializers wiki](https://github.com/eishay/jvm-serializers/wiki),” *github.com*, November 2014.
“[jvm-serializers wiki](https://github.com/eishay/jvm-serializers/wiki),”
*github.com*, November 2014.
1. “[XML Is a Poor Copy of S-Expressions](http://c2.com/cgi/wiki?XmlIsaPoorCopyOfEssExpressions),” *c2.com* wiki. 1. “[XML Is a Poor Copy of S-Expressions](http://c2.com/cgi/wiki?XmlIsaPoorCopyOfEssExpressions),” *c2.com* wiki.
1. Matt Harris: 1. Matt Harris: “[Snowflake: An Update and Some Very Important Information](https://groups.google.com/forum/#!topic/twitter-development-talk/ahbvo3VTIYI),” email to *Twitter Development Talk* mailing list, October 19, 2010.
“[Snowflake: An Update and Some Very Important Information](https://groups.google.com/forum/#!topic/twitter-development-talk/ahbvo3VTIYI),” email to *Twitter Development
Talk* mailing list, October 19, 2010.
1. Shudi (Sandy) Gao, C. M. Sperberg-McQueen, and 1. Shudi (Sandy) Gao, C. M. Sperberg-McQueen, and Henry S. Thompson: “[XML Schema 1.1](http://www.w3.org/XML/Schema),” W3C Recommendation, May 2001.
Henry S. Thompson: “[XML Schema 1.1](http://www.w3.org/XML/Schema),” W3C Recommendation,
May 2001.
1. Francis Galiegue, Kris Zyp, and Gary Court: 1. Francis Galiegue, Kris Zyp, and Gary Court: “[JSON Schema](http://json-schema.org/),” IETF Internet-Draft, February 2013.
“[JSON Schema](http://json-schema.org/),” IETF Internet-Draft, February 2013.
1. Yakov Shafranovich: 1. Yakov Shafranovich: “[RFC 4180: Common Format and MIME Type for Comma-Separated Values (CSV) Files](https://tools.ietf.org/html/rfc4180),” October 2005.
“[RFC 4180: Common Format and MIME Type for Comma-Separated Values (CSV) Files](https://tools.ietf.org/html/rfc4180),” October 2005.
1. “[MessagePack Specification](http://msgpack.org/),” *msgpack.org*. 1. “[MessagePack Specification](http://msgpack.org/),” *msgpack.org*. Mark Slee, Aditya Agarwal, and Marc Kwiatkowski: “[Thrift: Scalable Cross-Language Services Implementation](http://thrift.apache.org/static/files/thrift-20070401.pdf),” Facebook technical report, April 2007.
1. Mark Slee, Aditya Agarwal, and Marc Kwiatkowski:
“[Thrift: Scalable Cross-Language Services Implementation](http://thrift.apache.org/static/files/thrift-20070401.pdf),” Facebook technical report, April 2007.
1. “[Protocol Buffers Developer Guide](https://developers.google.com/protocol-buffers/docs/overview),” Google, Inc., *developers.google.com*. 1. “[Protocol Buffers Developer Guide](https://developers.google.com/protocol-buffers/docs/overview),” Google, Inc., *developers.google.com*.
1. Igor Anishchenko: 1. Igor Anishchenko: “[Thrift vs Protocol Buffers vs Avro - Biased Comparison](http://www.slideshare.net/IgorAnishchenko/pb-vs-thrift-vs-avro),” *slideshare.net*, September 17, 2012.
“[Thrift vs Protocol Buffers vs Avro - Biased Comparison](http://www.slideshare.net/IgorAnishchenko/pb-vs-thrift-vs-avro),” *slideshare.net*, September 17, 2012.
1. “[A Matrix of the Features Each Individual Language Library Supports](http://wiki.apache.org/thrift/LibraryFeatures),” 1. “[A Matrix of the Features Each Individual Language Library Supports](http://wiki.apache.org/thrift/LibraryFeatures),” *wiki.apache.org*.
*wiki.apache.org*.
1. Martin Kleppmann: 1. Martin Kleppmann: “[Schema Evolution in Avro, Protocol Buffers and Thrift](http://martin.kleppmann.com/2012/12/05/schema-evolution-in-avro-protocol-buffers-thrift.html),” *martin.kleppmann.com*, December 5, 2012.
“[Schema Evolution in Avro, Protocol Buffers and Thrift](http://martin.kleppmann.com/2012/12/05/schema-evolution-in-avro-protocol-buffers-thrift.html),” *martin.kleppmann.com*, December 5, 2012.
1. “[Apache Avro 1.7.7 Documentation](http://avro.apache.org/docs/1.7.7/),” *avro.apache.org*, July 2014. 1. “[Apache Avro 1.7.7 Documentation](http://avro.apache.org/docs/1.7.7/),” *avro.apache.org*, July 2014.
@ -587,116 +565,73 @@ actor模型是单个进程中并发的编程模型。逻辑被封装在角色中
“[&#91;PROPOSAL&#93; New Subproject: Avro](http://mail-archives.apache.org/mod_mbox/hadoop-general/200904.mbox/%3C49D53694.1050906@apache.org%3E),” email thread on *hadoop-general* mailing list, “[&#91;PROPOSAL&#93; New Subproject: Avro](http://mail-archives.apache.org/mod_mbox/hadoop-general/200904.mbox/%3C49D53694.1050906@apache.org%3E),” email thread on *hadoop-general* mailing list,
*mail-archives.apache.org*, April 2009. *mail-archives.apache.org*, April 2009.
1. Tony Hoare: 1. Tony Hoare: “[Null References: The Billion Dollar Mistake](http://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare),” at *QCon London*, March 2009.
“[Null References: The Billion Dollar Mistake](http://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare),” at *QCon London*,
March 2009.
1. Aditya Auradkar and Tom Quiggle: 1. Aditya Auradkar and Tom Quiggle: “[Introducing Espresso—LinkedIn's Hot New Distributed Document Store](https://engineering.linkedin.com/espresso/introducing-espresso-linkedins-hot-new-distributed-document-store),” *engineering.linkedin.com*, January 21, 2015.
“[Introducing Espresso—LinkedIn's Hot New Distributed Document Store](https://engineering.linkedin.com/espresso/introducing-espresso-linkedins-hot-new-distributed-document-store),” *engineering.linkedin.com*, January 21, 2015.
1. Jay Kreps: 1. Jay Kreps: “[Putting Apache Kafka to Use: A Practical Guide to Building a Stream Data Platform (Part 2)](http://blog.confluent.io/2015/02/25/stream-data-platform-2/),” *blog.confluent.io*, February 25, 2015.
“[Putting Apache Kafka to Use: A Practical Guide to Building a Stream Data Platform (Part 2)](http://blog.confluent.io/2015/02/25/stream-data-platform-2/),” *blog.confluent.io*,
February 25, 2015.
1. Gwen Shapira: 1. Gwen Shapira: “[The Problem of Managing Schemas](http://radar.oreilly.com/2014/11/the-problem-of-managing-schemas.html),” *radar.oreilly.com*, November 4, 2014.
“[The Problem of Managing Schemas](http://radar.oreilly.com/2014/11/the-problem-of-managing-schemas.html),” *radar.oreilly.com*, November 4, 2014.
1. “[Apache Pig 0.14.0 Documentation](http://pig.apache.org/docs/r0.14.0/),” *pig.apache.org*, November 2014. 1. “[Apache Pig 0.14.0 Documentation](http://pig.apache.org/docs/r0.14.0/),” *pig.apache.org*, November 2014.
1. John Larmouth: 1. John Larmouth: [*ASN.1Complete*](http://www.oss.com/asn1/resources/books-whitepapers-pubs/larmouth-asn1-book.pdf). Morgan Kaufmann, 1999. ISBN: 978-0-122-33435-1
<a href="http://www.oss.com/asn1/resources/books-whitepapers-pubs/larmouth-asn1-book.pdf">*ASN.1
Complete*</a>. Morgan Kaufmann, 1999. ISBN: 978-0-122-33435-1
1. Russell Housley, Warwick Ford, Tim Polk, and David Solo: 1. Russell Housley, Warwick Ford, Tim Polk, and David Solo: “[RFC 2459: Internet X.509 Public Key Infrastructure: Certificate and CRL Profile](https://www.ietf.org/rfc/rfc2459.txt),” IETF Network Working Group, Standards Track,
“[RFC 2459: Internet X.509 Public Key Infrastructure: Certificate and CRL Profile](https://www.ietf.org/rfc/rfc2459.txt),” IETF Network Working Group, Standards Track,
January 1999. January 1999.
1. Lev Walkin: 1. Lev Walkin: “[Question: Extensibility and Dropping Fields](http://lionet.info/asn1c/blog/2010/09/21/question-extensibility-removing-fields/),” *lionet.info*, September 21, 2010.
“[Question: Extensibility and Dropping Fields](http://lionet.info/asn1c/blog/2010/09/21/question-extensibility-removing-fields/),” *lionet.info*, September 21, 2010.
1. Jesse James Garrett: 1. Jesse James Garrett: “[Ajax: A New Approach to Web Applications](http://www.adaptivepath.com/ideas/ajax-new-approach-web-applications/),” *adaptivepath.com*, February 18, 2005.
“[Ajax: A New Approach to Web Applications](http://www.adaptivepath.com/ideas/ajax-new-approach-web-applications/),” *adaptivepath.com*, February 18, 2005.
1. Sam Newman: *Building Microservices*. 1. Sam Newman: *Building Microservices*. O'Reilly Media, 2015. ISBN: 978-1-491-95035-7
O'Reilly Media, 2015. ISBN: 978-1-491-95035-7
1. Chris Richardson: 1. Chris Richardson: “[Microservices: Decomposing Applications for Deployability and Scalability](http://www.infoq.com/articles/microservices-intro),” *infoq.com*, May 25, 2014.
“[Microservices: Decomposing Applications for Deployability and Scalability](http://www.infoq.com/articles/microservices-intro),” *infoq.com*, May 25, 2014.
1. Pat Helland: 1. Pat Helland: “[Data on the Outside Versus Data on the Inside](http://cidrdb.org/cidr2005/papers/P12.pdf),” at *2nd Biennial Conference on Innovative Data Systems Research* (CIDR), January 2005.
“[Data on the Outside Versus Data on the Inside](http://cidrdb.org/cidr2005/papers/P12.pdf),” at *2nd Biennial Conference on Innovative Data Systems Research* (CIDR),
January 2005.
1. Roy Thomas Fielding: 1. Roy Thomas Fielding: “[Architectural Styles and the Design of Network-Based Software Architectures](https://www.ics.uci.edu/~fielding/pubs/dissertation/fielding_dissertation.pdf),” PhD Thesis, University of California, Irvine, 2000.
“[Architectural Styles and the Design of Network-Based Software Architectures](https://www.ics.uci.edu/~fielding/pubs/dissertation/fielding_dissertation.pdf),” PhD Thesis, University of
California, Irvine, 2000.
1. Roy Thomas Fielding: 1. Roy Thomas Fielding: “[REST APIs Must Be Hypertext-Driven](http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven),” *roy.gbiv.com*, October 20 2008.
“[REST APIs Must Be Hypertext-Driven](http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven),” *roy.gbiv.com*, October 20 2008.
1. “[REST in Peace, SOAP](http://royal.pingdom.com/2010/10/15/rest-in-peace-soap/),” *royal.pingdom.com*, October 15, 2010. 1. “[REST in Peace, SOAP](http://royal.pingdom.com/2010/10/15/rest-in-peace-soap/),” *royal.pingdom.com*, October 15, 2010.
1. “[Web Services Standards as of Q1 2007](https://www.innoq.com/resources/ws-standards-poster/),” *innoq.com*, February 2007. 1. “[Web Services Standards as of Q1 2007](https://www.innoq.com/resources/ws-standards-poster/),” *innoq.com*, February 2007.
1. Pete Lacey: 1. Pete Lacey: “[The S Stands for Simple](http://harmful.cat-v.org/software/xml/soap/simple),” *harmful.cat-v.org*, November 15, 2006.
“[The S Stands for Simple](http://harmful.cat-v.org/software/xml/soap/simple),” *harmful.cat-v.org*, November 15, 2006.
1. Stefan Tilkov: 1. Stefan Tilkov: “[Interview: Pete Lacey Criticizes Web Services](http://www.infoq.com/articles/pete-lacey-ws-criticism),” *infoq.com*, December 12, 2006.
“[Interview: Pete Lacey Criticizes Web Services](http://www.infoq.com/articles/pete-lacey-ws-criticism),” *infoq.com*, December 12, 2006.
1. “[OpenAPI Specification (fka Swagger RESTful API Documentation Specification) Version 2.0](http://swagger.io/specification/),” 1. “[OpenAPI Specification (fka Swagger RESTful API Documentation Specification) Version 2.0](http://swagger.io/specification/),” *swagger.io*, September 8, 2014.
*swagger.io*, September 8, 2014.
1. Michi Henning: 1. Michi Henning: “[The Rise and Fall of CORBA](http://queue.acm.org/detail.cfm?id=1142044),” *ACM Queue*, volume 4, number 5, pages 2834, June 2006.
“[The Rise and Fall of CORBA](http://queue.acm.org/detail.cfm?id=1142044),”
*ACM Queue*, volume 4, number 5, pages 2834, June 2006.
[doi:10.1145/1142031.1142044](http://dx.doi.org/10.1145/1142031.1142044) [doi:10.1145/1142031.1142044](http://dx.doi.org/10.1145/1142031.1142044)
1. Andrew D. Birrell and Bruce Jay Nelson: 1. Andrew D. Birrell and Bruce Jay Nelson: “[Implementing Remote Procedure Calls](http://www.cs.princeton.edu/courses/archive/fall03/cs518/papers/rpc.pdf),” *ACM Transactions on Computer Systems* (TOCS), volume 2, number 1, pages 3959, February 1984. [doi:10.1145/2080.357392](http://dx.doi.org/10.1145/2080.357392)
“[Implementing Remote Procedure Calls](http://www.cs.princeton.edu/courses/archive/fall03/cs518/papers/rpc.pdf),” *ACM Transactions on Computer Systems* (TOCS),
volume 2, number 1, pages 3959, February 1984.
[doi:10.1145/2080.357392](http://dx.doi.org/10.1145/2080.357392)
1. Jim Waldo, Geoff Wyant, Ann Wollrath, and Sam Kendall: 1. Jim Waldo, Geoff Wyant, Ann Wollrath, and Sam Kendall: “[A Note on Distributed Computing](http://m.mirror.facebook.net/kde/devel/smli_tr-94-29.pdf),” Sun Microsystems Laboratories, Inc., Technical Report TR-94-29, November 1994.
“[A Note on Distributed Computing](http://m.mirror.facebook.net/kde/devel/smli_tr-94-29.pdf),”
Sun Microsystems Laboratories, Inc., Technical Report TR-94-29, November 1994.
1. Steve Vinoski: 1. Steve Vinoski: “[Convenience over Correctness](http://steve.vinoski.net/pdf/IEEE-Convenience_Over_Correctness.pdf),” *IEEE Internet Computing*, volume 12, number 4, pages 8992, July 2008. [doi:10.1109/MIC.2008.75](http://dx.doi.org/10.1109/MIC.2008.75)
“[Convenience over Correctness](http://steve.vinoski.net/pdf/IEEE-Convenience_Over_Correctness.pdf),” *IEEE Internet Computing*, volume 12, number 4, pages 8992, July 2008.
[doi:10.1109/MIC.2008.75](http://dx.doi.org/10.1109/MIC.2008.75)
1. Marius Eriksen: 1. Marius Eriksen: “[Your Server as a Function](http://monkey.org/~marius/funsrv.pdf),” at *7th Workshop on Programming Languages and Operating Systems* (PLOS), November 2013. [doi:10.1145/2525528.2525538](http://dx.doi.org/10.1145/2525528.2525538)
“[Your Server as a Function](http://monkey.org/~marius/funsrv.pdf),” at
*7th Workshop on Programming Languages and Operating Systems* (PLOS), November 2013.
[doi:10.1145/2525528.2525538](http://dx.doi.org/10.1145/2525528.2525538)
1. “[grpc-common Documentation](https://github.com/grpc/grpc-common),” Google, Inc., *github.com*, February 2015. 1. “[grpc-common Documentation](https://github.com/grpc/grpc-common),” Google, Inc., *github.com*, February 2015.
1. Aditya Narayan and Irina Singh: 1. Aditya Narayan and Irina Singh: “[Designing and Versioning Compatible Web Services](http://www.ibm.com/developerworks/websphere/library/techarticles/0705_narayan/0705_narayan.html),” *ibm.com*, March 28, 2007.
“[Designing and Versioning Compatible Web Services](http://www.ibm.com/developerworks/websphere/library/techarticles/0705_narayan/0705_narayan.html),” *ibm.com*, March 28, 2007.
1. Troy Hunt: 1. Troy Hunt: “[Your API Versioning Is Wrong, Which Is Why I Decided to Do It 3 Different Wrong Ways](http://www.troyhunt.com/2014/02/your-api-versioning-is-wrong-which-is.html),” *troyhunt.com*, February 10, 2014.
“[Your API Versioning Is Wrong, Which Is Why I Decided to Do It 3 Different Wrong Ways](http://www.troyhunt.com/2014/02/your-api-versioning-is-wrong-which-is.html),” *troyhunt.com*,
February 10, 2014.
1. “[API Upgrades](https://stripe.com/docs/upgrades),” Stripe, Inc., April 2015. 1. “[API Upgrades](https://stripe.com/docs/upgrades),” Stripe, Inc., April 2015.
1. Jonas Bonér: 1. Jonas Bonér: “[Upgrade in an Akka Cluster](http://grokbase.com/t/gg/akka-user/138wd8j9e3/upgrade-in-an-akka-cluster),” email to *akka-user* mailing list, *grokbase.com*, August 28, 2013.
“[Upgrade in an Akka Cluster](http://grokbase.com/t/gg/akka-user/138wd8j9e3/upgrade-in-an-akka-cluster),” email to *akka-user* mailing list, *grokbase.com*, August 28, 2013.
1. Philip A. Bernstein, Sergey Bykov, Alan Geller, et al.: 1. Philip A. Bernstein, Sergey Bykov, Alan Geller, et al.: “[Orleans: Distributed Virtual Actors for Programmability and Scalability](http://research.microsoft.com/pubs/210931/Orleans-MSR-TR-2014-41.pdf),” Microsoft Research Technical Report MSR-TR-2014-41, March 2014.
“[Orleans: Distributed Virtual Actors for Programmability and Scalability](http://research.microsoft.com/pubs/210931/Orleans-MSR-TR-2014-41.pdf),” Microsoft Research
Technical Report MSR-TR-2014-41, March 2014.
1. “[Microsoft Project Orleans Documentation](http://dotnet.github.io/orleans/),” Microsoft Research, *dotnet.github.io*, 2015. 1. “[Microsoft Project Orleans Documentation](http://dotnet.github.io/orleans/),” Microsoft Research, *dotnet.github.io*, 2015.
1. David Mercer, Sean Hinde, Yinso Chen, and Richard A O'Keefe: 1. David Mercer, Sean Hinde, Yinso Chen, and Richard A O'Keefe: “[beginner: Updating Data Structures](http://erlang.org/pipermail/erlang-questions/2007-October/030318.html),” email thread on *erlang-questions* mailing list, *erlang.com*, October 29, 2007.
“[beginner: Updating Data Structures](http://erlang.org/pipermail/erlang-questions/2007-October/030318.html),” email thread on *erlang-questions* mailing list, *erlang.com*,
October 29, 2007.
1. Fred Hebert: 1. Fred Hebert: “[Postscript: Maps](http://learnyousomeerlang.com/maps),” *learnyousomeerlang.com*, April 9, 2014.
“[Postscript: Maps](http://learnyousomeerlang.com/maps),” *learnyousomeerlang.com*,
April 9, 2014.
------ ------

View File

@ -22,7 +22,7 @@
在复制时需要进行许多权衡:例如,使用同步复制还是异步复制?如何处理失败的副本?。这些通常是数据库中的配置项,细节因数据库而异,但原理在许多不同的实现中都是相似的。本章将讨论这些决策的后果。 在复制时需要进行许多权衡:例如,使用同步复制还是异步复制?如何处理失败的副本?。这些通常是数据库中的配置项,细节因数据库而异,但原理在许多不同的实现中都是相似的。本章将讨论这些决策的后果。
数据库的复制是一个老话题 ——自1970年代研究以来这些原则并没有太大的改变[1],因为网络的基本约束保持不变。然而在研究之外,许多开发人员仍然假设一个数据库只有一个节点。分布式数据库变为主流只是最近的事。由于许多程序员都是这方面的新手,对**最终一致性eventual consistency**等问题存在诸多误解。在“复制滞后问题”一节,我们将更加精确地了解最终的一致性,并讨论诸如**读己之写read-your-writes**和**单调读monotonic read**保证等内容。 数据库的复制是一个老话题 ——自1970年代研究以来这些原则并没有太大的改变【1】,因为网络的基本约束保持不变。然而在研究之外,许多开发人员仍然假设一个数据库只有一个节点。分布式数据库变为主流只是最近的事。由于许多程序员都是这方面的新手,对**最终一致性eventual consistency**等问题存在诸多误解。在“复制滞后问题”一节,我们将更加精确地了解最终的一致性,并讨论诸如**读己之写read-your-writes**和**单调读monotonic read**保证等内容。
@ -41,7 +41,7 @@
![](img/fig5-1.png) ![](img/fig5-1.png)
**图5-1 基于领导者(主-从)的复制** **图5-1 基于领导者(主-从)的复制**
这种复制模式是许多关系数据库的内置功能如PostgreSQL从9.0版本开始MySQLOracle Data Guard [2]和SQL Server的AlwaysOn可用性组[3]。 它也用于一些非关系数据库包括MongoDBRethinkDB和Espresso [4]。 最后基于领导者的复制并不仅限于数据库像Kafka [5]和RabbitMQ高可用队列[6]这样的分布式消息代理也使用它。 某些网络文件系统例如DRBD这样的块复制设备也与之类似。 这种复制模式是许多关系数据库的内置功能如PostgreSQL从9.0版本开始MySQLOracle Data Guard 【2】和SQL Server的AlwaysOn可用性组【3】。 它也用于一些非关系数据库包括MongoDBRethinkDB和Espresso [4]。 最后基于领导者的复制并不仅限于数据库像Kafka [5]和RabbitMQ高可用队列【6】这样的分布式消息代理也使用它。 某些网络文件系统例如DRBD这样的块复制设备也与之类似。
@ -62,7 +62,7 @@
同步复制的优点是,跟随者保证有与领导者一致的最新数据副本。如果领导者突然失败,我们可以肯定的是,这些数据仍然可以在追随者身上找到。缺点是如果同步跟随器没有响应(因为它已经崩溃,或者出现网络故障,或者出于任何其他原因),写入不能被处理。领导者必须阻止所有写入,并等待同步副本再次可用。 同步复制的优点是,跟随者保证有与领导者一致的最新数据副本。如果领导者突然失败,我们可以肯定的是,这些数据仍然可以在追随者身上找到。缺点是如果同步跟随器没有响应(因为它已经崩溃,或者出现网络故障,或者出于任何其他原因),写入不能被处理。领导者必须阻止所有写入,并等待同步副本再次可用。
因此,所有追随者都是同步的是不切实际的:任何一个节点的中断都会导致整个系统停滞不前。实际上,如果在数据库上启用同步复制,通常意味着其中**一个**跟随者是同步的,而其他的则是异步的。如果同步跟随器变得不可用或缓慢,则使一个异步跟随器同步。这保证您至少在两个节点上拥有最新的数据副本:领导者和同步追随者。 这种配置有时也被称为**半同步semi-synchronous**[7] 因此,所有追随者都是同步的是不切实际的:任何一个节点的中断都会导致整个系统停滞不前。实际上,如果在数据库上启用同步复制,通常意味着其中**一个**跟随者是同步的,而其他的则是异步的。如果同步跟随器变得不可用或缓慢,则使一个异步跟随器同步。这保证您至少在两个节点上拥有最新的数据副本:领导者和同步追随者。 这种配置有时也被称为**半同步semi-synchronous**【7】
通常情况下,基于领导者的复制都配置为完全异步。 在这种情况下,如果领导失败并且不可恢复,则任何尚未复制给追随者的写入都将丢失。 这意味着即使已经向客户端确认成功,写入也不能保证**持久Durable**。 然而,一个完全异步的配置也有优点:即使所有的追随者都落后了,领导者也可以继续处理写入。 通常情况下,基于领导者的复制都配置为完全异步。 在这种情况下,如果领导失败并且不可恢复,则任何尚未复制给追随者的写入都将丢失。 这意味着即使已经向客户端确认成功,写入也不能保证**持久Durable**。 然而,一个完全异步的配置也有优点:即使所有的追随者都落后了,领导者也可以继续处理写入。

View File

@ -22,11 +22,11 @@
通常情况下,分区是这样定义的,即每条数据(每条记录,每行或每个文档)属于且仅属于一个分区。有很多方法可以实现这一点,本章将深入讨论。实际上,每个分区都是自己的小型数据库,尽管数据库可能支持同时触及多个分区的操作。 通常情况下,分区是这样定义的,即每条数据(每条记录,每行或每个文档)属于且仅属于一个分区。有很多方法可以实现这一点,本章将深入讨论。实际上,每个分区都是自己的小型数据库,尽管数据库可能支持同时触及多个分区的操作。
需要分区数据的主要原因是**可扩展性**。不同的分区可以放在不共享的集群中的不同节点上(参阅[第二部分](part-ii.md)关于[无共享架构](part-ii.md#无共享架构)的定义)。因此,大数据集可以分布在多个磁盘上,并且查询负载可以分布在多个处理器上。 需要分区数据的主要原因是**可扩展性**。不同的分区可以放在不共享的集群中的不同节点上(参阅[第二部分](part-ii.md)关于[无共享架构](part-ii.md#无共享架构)的定义)。因此,大数据集可以分布在多个磁盘上,并且查询负载可以分布在多个处理器上。
对于在单个分区上运行的查询,每个节点可以独立执行对其自己的分区的查询,因此可以通过添加更多的节点来缩放查询吞吐量。大型,复杂的查询可能会跨越多个节点进行并行处理,尽管这会变得非常困难。 对于在单个分区上运行的查询,每个节点可以独立执行对其自己的分区的查询,因此可以通过添加更多的节点来缩放查询吞吐量。大型,复杂的查询可能会跨越多个节点进行并行处理,尽管这会变得非常困难。
分区数据库在20世纪80年代由Teradata和NonStop SQL[【1】][1]等产品率先推出最近又被NoSQL数据库和基于Hadoop的数据仓库重新发明。有些系统是为事务性工作负载设计的其他系统则用于分析参阅“[事务处理或分析]()?”):这种差异会影响系统的调整方式,但是分区的基本原理适用于这两种工作负载。 分区数据库在20世纪80年代由Teradata和NonStop SQL【1】等产品率先推出最近又被NoSQL数据库和基于Hadoop的数据仓库重新发明。有些系统是为事务性工作负载设计的其他系统则用于分析参阅“[事务处理或分析]()?”):这种差异会影响系统的调整方式,但是分区的基本原理适用于这两种工作负载。
在本章中,我们将首先介绍分割大型数据集的不同方法,并观察索引如何与分区配合。然后,我们将讨论[再平衡](),如果想要添加或删除群集中的节点,则必须进行再平衡。最后,我们将概述数据库如何将请求路由到正确的分区并执行查询。 在本章中,我们将首先介绍分割大型数据集的不同方法,并观察索引如何与分区配合。然后,我们将讨论[再平衡](),如果想要添加或删除群集中的节点,则必须进行再平衡。最后,我们将概述数据库如何将请求路由到正确的分区并执行查询。
@ -64,11 +64,11 @@
键的范围不一定均匀分布,因为您的数据可能不均匀分布。例如,在[图6-2]()中第1卷包含以A和B开头的单词但第12卷则包含以TUVXY和Z开头的单词。每个字母的两个字母只有一个音量导致一些卷比其他卷更大。为了均匀分配数据分区边界需要适应数据。 键的范围不一定均匀分布,因为您的数据可能不均匀分布。例如,在[图6-2]()中第1卷包含以A和B开头的单词但第12卷则包含以TUVXY和Z开头的单词。每个字母的两个字母只有一个音量导致一些卷比其他卷更大。为了均匀分配数据分区边界需要适应数据。
分区边界可以由管理员手动选择,也可以由数据库自动选择(我们将在第209页的“[重新平衡分区]()”中更详细地讨论分区边界的选择)。 Bigtable使用了这种分区策略以及其开源等价物HBase [23]RethinkDB和2.4版本之前的MongoDB [4][4] 分区边界可以由管理员手动选择,也可以由数据库自动选择(将在“[重新平衡分区]()”中更详细地讨论分区边界的选择)。 Bigtable使用了这种分区策略以及其开源等价物HBase 【2, 3】RethinkDB和2.4版本之前的MongoDB 【4】
在每个分区中,我们可以按照排序的顺序保存键(参见“[SSTables和LSM-树]()”)。这具有范围扫描非常简单的优点,您可以将键作为连接索引来处理,以便在一个查询中获取多个相关记录(参阅第79页的“[多列索引]()”)。例如,考虑存储来自传感器网络的数据的应用程序,其中关键是测量的时间戳(年月日时分秒)。范围扫描在这种情况下非常有用,因为它们让您轻松获取某个月份的所有读数。 在每个分区中,我们可以按照排序的顺序保存键(参见“[SSTables和LSM-树]()”)。这具有范围扫描非常简单的优点,您可以将键作为连接索引来处理,以便在一个查询中获取多个相关记录(参阅“[多列索引](#ch2.md#多列索引)”)。例如,考虑存储来自传感器网络的数据的应用程序,其中关键是测量的时间戳(年月日时分秒)。范围扫描在这种情况下非常有用,因为它们让您轻松获取某个月份的所有读数。
然而Key Range分区的缺点是某些访问模式会导致热点。 如果Key是时间戳则分区对应于时间范围例如每天一个分区。 不幸的是,由于我们在测量发生时将数据从传感器写入数据库,因此所有写入操作都会转到同一个分区(即今天的分区),这样分区可能会因写入而过载,而其他分区则处于空闲状态[5] 然而Key Range分区的缺点是某些访问模式会导致热点。 如果Key是时间戳则分区对应于时间范围例如每天一个分区。 不幸的是,由于我们在测量发生时将数据从传感器写入数据库,因此所有写入操作都会转到同一个分区(即今天的分区),这样分区可能会因写入而过载,而其他分区则处于空闲状态【5】
为了避免传感器数据库中的这个问题需要使用除了时间戳以外的其他东西作为Key的第一个部分。 例如,可以在每个时间戳前添加传感器名称,以便分区首先按传感器名称,然后按时间。 假设同时有许多传感器处于活动状态,则写入负载将最终均匀分布在分区上。 现在,当您想要在一个时间范围内获取多个传感器的值时,您需要为每个传感器名称执行一个单独的范围查询。 为了避免传感器数据库中的这个问题需要使用除了时间戳以外的其他东西作为Key的第一个部分。 例如,可以在每个时间戳前添加传感器名称,以便分区首先按传感器名称,然后按时间。 假设同时有许多传感器处于活动状态,则写入负载将最终均匀分布在分区上。 现在,当您想要在一个时间范围内获取多个传感器的值时,您需要为每个传感器名称执行一个单独的范围查询。
@ -78,9 +78,9 @@
一个好的散列函数可以将接受偏斜的数据并使其均匀分布。假设你有一个带有字符串的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】
一旦你有一个合适的键散列函数你可以为每个分区分配一个散列范围而不是键的范围每个散列落在分区范围内的键将被存储在该分区中。如图6-3所示。 一旦你有一个合适的键散列函数,你可以为每个分区分配一个散列范围(而不是键的范围),每个散列落在分区范围内的键将被存储在该分区中。如[图6-3](img/fig6-3.png)所示。
![](img/fig6-3.png) ![](img/fig6-3.png)
@ -90,21 +90,21 @@
> #### 一致性哈希 > #### 一致性哈希
> >
> 一致性哈希由Karger等人定义。[7] 用于跨互联网级别的缓存系统例如CDN中是一种能均匀分配负载的方法。它使用随机选择的**分区边界partition boundaries**来避免中央控制或分布式共识的需要。 请注意这里的一致性与复制一致性请参阅第5章或ACID一致性参阅第7章无关而是描述了重新平衡的特定方法。 > 一致性哈希由Karger等人定义。【7】 用于跨互联网级别的缓存系统例如CDN中是一种能均匀分配负载的方法。它使用随机选择的**分区边界partition boundaries**来避免中央控制或分布式共识的需要。 请注意这里的一致性与复制一致性请参阅第5章或ACID一致性参阅[第7章](ch7.md))无关,而是描述了重新平衡的特定方法。
> >
> 正如我们将在第209页的“重新平衡分区”中所看到的,这种特殊的方法对于数据库实际上并不是很好,所以在实际中很少使用(某些数据库的文档仍然指的是一致性哈希,但是它 往往是不准确的)。 因为这太混乱了,所以最好避免使用一致性哈希这个术语,而只是把它称为**散列分区hash partitioning**。 > 正如我们将在“[重新平衡分区](#重新平衡分区)”中所看到的,这种特殊的方法对于数据库实际上并不是很好,所以在实际中很少使用(某些数据库的文档仍然指的是一致性哈希,但是它 往往是不准确的)。 因为这太混乱了,所以最好避免使用一致性哈希这个术语,而只是把它称为**散列分区hash partitioning**。
不幸的是通过使用Key散列进行分区我们失去了键范围分区的一个很好的属性高效执行范围查询的能力。曾经相邻的密钥现在分散在所有分区中所以它们之间的顺序就丢失了。在MongoDB中如果您使用了基于散列的分片模式则任何范围查询都必须发送到所有分区[4]。主键上的范围查询不受Riak [9]Couchbase [10]或Voldemort的支持。 不幸的是通过使用Key散列进行分区我们失去了键范围分区的一个很好的属性高效执行范围查询的能力。曾经相邻的密钥现在分散在所有分区中所以它们之间的顺序就丢失了。在MongoDB中如果您使用了基于散列的分片模式则任何范围查询都必须发送到所有分区【4】。主键上的范围查询不受Riak 【9】Couchbase 【10】或Voldemort的支持。
Cassandra在两种分区策略之间达成了一个折衷[11,12,13]。 Cassandra中的表可以使用由多个列组成的复合主键来声明。键中只有第一列会作为散列的依据而其他列则被用作Casssandra的SSTables中排序数据的连接索引。尽管查询无法在复合主键的第一列中按范围扫表但如果第一列已经指定了固定值则可以对该键的其他列执行有效的范围扫描。 Cassandra在两种分区策略之间达成了一个折衷【11, 12, 13】。 Cassandra中的表可以使用由多个列组成的复合主键来声明。键中只有第一列会作为散列的依据而其他列则被用作Casssandra的SSTables中排序数据的连接索引。尽管查询无法在复合主键的第一列中按范围扫表但如果第一列已经指定了固定值则可以对该键的其他列执行有效的范围扫描。
串联索引方法为一对多关系提供了一个优雅的数据模型。例如,在社交媒体网站上,一个用户可能会发布很多更新。如果更新的主键被选择为`user_idupdate_timestamp`,那么您可以有效地检索特定用户在某个时间间隔内按时间戳排序的所有更新。不同的用户可以存储在不同的分区上,但是在每个用户中,更新按时间戳顺序存储在单个分区上。 串联索引方法为一对多关系提供了一个优雅的数据模型。例如,在社交媒体网站上,一个用户可能会发布很多更新。如果更新的主键被选择为`(user_id, update_timestamp)`,那么您可以有效地检索特定用户在某个时间间隔内按时间戳排序的所有更新。不同的用户可以存储在不同的分区上,但是在每个用户中,更新按时间戳顺序存储在单个分区上。
### 负载倾斜与消除热点 ### 负载倾斜与消除热点
如前所述,哈希键确定其分区可以帮助减少热点。但是,它不能完全避免它们:在极端情况下,所有的读写操作都是针对同一个键的,所有的请求都会被路由到同一个分区。 如前所述,哈希键确定其分区可以帮助减少热点。但是,它不能完全避免它们:在极端情况下,所有的读写操作都是针对同一个键的,所有的请求都会被路由到同一个分区。
这种工作量也许并不常见,但并非闻所未闻:例如,在社交媒体网站上,一个拥有数百万追随者的名人用户在做某事时可能会引发一场风暴[14]。这个事件可能导致大量写入同一个键键可能是名人的用户ID或者人们正在评论的动作的ID。哈希键不起作用因为两个相同ID的哈希值仍然是相同的。 这种工作量也许并不常见,但并非闻所未闻:例如,在社交媒体网站上,一个拥有数百万追随者的名人用户在做某事时可能会引发一场风暴【14】。这个事件可能导致大量写入同一个键键可能是名人的用户ID或者人们正在评论的动作的ID。哈希键不起作用因为两个相同ID的哈希值仍然是相同的。
如今大多数数据系统无法自动补偿这种高度偏斜的工作负载因此应用程序有责任减少偏斜。例如如果一个密钥被认为是非常热的一个简单的方法是在密钥的开始或结尾添加一个随机数。只要一个两位数的十进制随机数就可以将写入密钥分散到100个不同的密钥中从而允许这些密钥分配到不同的分区。 如今大多数数据系统无法自动补偿这种高度偏斜的工作负载因此应用程序有责任减少偏斜。例如如果一个密钥被认为是非常热的一个简单的方法是在密钥的开始或结尾添加一个随机数。只要一个两位数的十进制随机数就可以将写入密钥分散到100个不同的密钥中从而允许这些密钥分配到不同的分区。
@ -140,7 +140,7 @@ Cassandra在两种分区策略之间达成了一个折衷[11,12,13]。 Cassandra
但是从文档分区索引中读取需要注意除非您对文档ID做了特别的处理否则没有理由将所有具有特定颜色或特定品牌的汽车放在同一个分区中。在图6-4中红色汽车出现在分区0和分区1中。因此如果要搜索红色汽车则需要将查询发送到所有分区并合并所有返回的结果。 但是从文档分区索引中读取需要注意除非您对文档ID做了特别的处理否则没有理由将所有具有特定颜色或特定品牌的汽车放在同一个分区中。在图6-4中红色汽车出现在分区0和分区1中。因此如果要搜索红色汽车则需要将查询发送到所有分区并合并所有返回的结果。
这种查询分区数据库的方法有时被称为**分散/聚集scatter/gather**,并且可能会使二级索引上的读取查询相当昂贵。即使您并行查询分区,分散/聚集也容易导致尾部延迟放大请参阅第16页的“实践中的百分比”。然而它被广泛使用MonDBDBRiak [15]Cassandra [16]Elasticsearch [17]SolrCloud [18]和VoltDB [19]都使用文档分区二级索引。大多数数据库供应商建议您构建一个能从单个分区提供二级索引查询的分区方案,但这并不总是可行,尤其是当在单个查询中使用多个二级索引时(例如同时需要按颜色和制造商查询)。 这种查询分区数据库的方法有时被称为**分散/聚集scatter/gather**,并且可能会使二级索引上的读取查询相当昂贵。即使您并行查询分区,分散/聚集也容易导致尾部延迟放大请参阅第16页的“实践中的百分比”。然而它被广泛使用MonDBDBRiak 【15】Cassandra 【16】Elasticsearch 【17】SolrCloud 【18】和VoltDB 【19】都使用文档分区二级索引。大多数数据库供应商建议您构建一个能从单个分区提供二级索引查询的分区方案,但这并不总是可行,尤其是当在单个查询中使用多个二级索引时(例如同时需要按颜色和制造商查询)。
@ -148,7 +148,7 @@ Cassandra在两种分区策略之间达成了一个折衷[11,12,13]。 Cassandra
我们可以构建一个覆盖所有分区数据的**全局索引**,而不是每个分区都有自己的次级索引(本地索引)。但是,我们不能只把这个索引存储在一个节点上,因为它可能会成为一个瓶颈,打破了分区的目的。全局索引也必须进行分区,但索引可以采用与主键不同的分区方式。 我们可以构建一个覆盖所有分区数据的**全局索引**,而不是每个分区都有自己的次级索引(本地索引)。但是,我们不能只把这个索引存储在一个节点上,因为它可能会成为一个瓶颈,打破了分区的目的。全局索引也必须进行分区,但索引可以采用与主键不同的分区方式。
图6-5说明了这可能是什么情况来自所有分区的红色汽车在索引中显示为红色索引中的红色但索引是分区的以便从字母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) ![](img/fig6-5.png)
@ -160,15 +160,15 @@ Cassandra在两种分区策略之间达成了一个折衷[11,12,13]。 Cassandra
全局(关键词分区)索引优于文档分区索引的优点是它可以使读取更有效率:而不是**分散/收集**所有分区,客户端只需要向包含关键词的分区发出请求它想要的。但是,全局索引的缺点在于写入速度较慢且较为复杂,因为写入单个文档现在可能会影响索引的多个分区(文档中的每个术语可能位于不同的分区上,位于不同的节点上) 。 全局(关键词分区)索引优于文档分区索引的优点是它可以使读取更有效率:而不是**分散/收集**所有分区,客户端只需要向包含关键词的分区发出请求它想要的。但是,全局索引的缺点在于写入速度较慢且较为复杂,因为写入单个文档现在可能会影响索引的多个分区(文档中的每个术语可能位于不同的分区上,位于不同的节点上) 。
在理想的世界里索引总是最新的写入数据库的每个文档都会立即反映在索引中。但是在分区索引中这会需要跨库分布式事务跨越所有被写入影响的分片这在所有数据库中都不受支持请参阅第7章和第9章 在理想的世界里,索引总是最新的,写入数据库的每个文档都会立即反映在索引中。但是,在分区索引中,这会需要跨库分布式事务,跨越所有被写入影响的分片,这在所有数据库中都不受支持(请参阅[第7章](ch7.md)[第9章](ch9.md))。
在实践中,对全局二级索引的更新通常是**异步**的也就是说如果在写入之后不久读取索引刚才所做的更改可能尚未反映在索引中。例如Amazon DynamoDB指出在正常情况下其全局次级索引会在不到一秒的时间内更新但在基础架构出现故障的情况下可能会经历更长的传播延迟[20] 在实践中,对全局二级索引的更新通常是**异步**的也就是说如果在写入之后不久读取索引刚才所做的更改可能尚未反映在索引中。例如Amazon DynamoDB指出在正常情况下其全局次级索引会在不到一秒的时间内更新但在基础架构出现故障的情况下可能会经历更长的传播延迟【20】
全局术语分区索引的其他用途包括Riak的搜索功能[21]和Oracle数据仓库它允许您在本地索引和全局索引之间进行选择[22]。我们将回到第12章中回到实现关键字二级索引的主题。 全局术语分区索引的其他用途包括Riak的搜索功能【21】和Oracle数据仓库它允许您在本地索引和全局索引之间进行选择【22】。我们将回到[第12章](ch12.md)中回到实现关键字二级索引的主题。
## 平衡分区 ## 平衡分区
在数据库中,随着时间的推移,事情也在起变化。 在数据库中,随着时间的推移,事情也在起变化。
@ -182,19 +182,19 @@ Cassandra在两种分区策略之间达成了一个折衷[11,12,13]。 Cassandra
* 再平衡之后,负载(数据存储,读取和写入请求)应该在集群中的节点之间公平地共享。 * 再平衡之后,负载(数据存储,读取和写入请求)应该在集群中的节点之间公平地共享。
* 再平衡正在发生时,数据库应该继续接受读取和写入。 * 再平衡正在发生时,数据库应该继续接受读取和写入。
* 节点之间不应移动超过所需的数据以便快速再平衡并尽量减少网络和磁盘I / O负载。 * 节点之间不应移动超过所需的数据以便快速再平衡并尽量减少网络和磁盘I/O负载。
### 平衡策略 ### 平衡策略
有几种不同的分区分配方式[23]。让我们依次简要讨论一下。 有几种不同的分区分配方式【23】。让我们依次简要讨论一下。
#### 反面教材hash mod N #### 反面教材hash mod N
我们在前面说过图6-3最好将可能的散列分成不同的范围并将每个范围分配给一个分区例如如果$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方法的问题是如果节点数量N发生变化大多数密钥将需要从一个节点移动到另一个节点。例如假设$hash(key)=123456$。如果最初有10个节点那么这个键一开始放在节点6上因为$123456 mod 10 = 6$。当您增长到11个节点时密钥需要移动到节点3$123456 mod 11 = 3$当您增长到12个节点时需要移动到节点0$123456 mod 12 = 0$)。这种频繁的举动使得再平衡过于昂贵。 模N方法的问题是如果节点数量N发生变化大多数密钥将需要从一个节点移动到另一个节点。例如假设$hash(key)=123456$。如果最初有10个节点那么这个键一开始放在节点6上因为$123456\ mod\ 10 = 6$。当您增长到11个节点时密钥需要移动到节点3$123456\ mod\ 11 = 3$当您增长到12个节点时需要移动到节点0$123456\ mod\ 12 = 0$)。这种频繁的举动使得再平衡过于昂贵。
我们需要一种不需要移动数据的方法。 我们需要一种不需要移动数据的方法。
@ -202,7 +202,7 @@ Cassandra在两种分区策略之间达成了一个折衷[11,12,13]。 Cassandra
幸运的是有一个相当简单的解决方案创建比节点更多的分区并为每个节点分配多个分区。例如运行在10个节点的集群上的数据库可能会从一开始就被拆分为1,000个分区因此大约有100个分区被分配给每个节点。 幸运的是有一个相当简单的解决方案创建比节点更多的分区并为每个节点分配多个分区。例如运行在10个节点的集群上的数据库可能会从一开始就被拆分为1,000个分区因此大约有100个分区被分配给每个节点。
现在,如果一个节点被添加到集群中,新节点可以从每个现有节点中**窃取**几个分区直到分区再次公平分配。这个过程如图6-6所示。如果从集群中删除一个节点则会发生相反的情况。 现在,如果一个节点被添加到集群中,新节点可以从每个现有节点中**窃取**几个分区,直到分区再次公平分配。这个过程如[图6-6](img/fig6-6.png)所示。如果从集群中删除一个节点,则会发生相反的情况。
只有整个分区在节点之间移动。分区的数量不会改变,键所指定的分区也不会改变。唯一改变的是分区所指派的节点。这种指派变更并不是即时的——在网络上传输大量的数据需要一些时间——所以在传输过程中,旧的分区会接受传输过程中发生的读写操作。 只有整个分区在节点之间移动。分区的数量不会改变,键所指定的分区也不会改变。唯一改变的是分区所指派的节点。这种指派变更并不是即时的——在网络上传输大量的数据需要一些时间——所以在传输过程中,旧的分区会接受传输过程中发生的读写操作。
@ -210,9 +210,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]中使用了这种重新平衡的方法。
在这种配置中,分区的数量通常在数据库第一次建立时是固定的,之后不会改变。虽然原则上可以分割和合并分区(请参阅下一节),但固定数量的分区在操作上更简单,因此许多固定分区数据库选择不实施分区分割。因此,一开始配置的分区数就是您可以拥有的最大节点数量,所以您需要选择足够高的分区以适应未来的增长。但是,每个分区也有管理开销,所以选择太高的数字是适得其反的。 在这种配置中,分区的数量通常在数据库第一次建立时是固定的,之后不会改变。虽然原则上可以分割和合并分区(请参阅下一节),但固定数量的分区在操作上更简单,因此许多固定分区数据库选择不实施分区分割。因此,一开始配置的分区数就是您可以拥有的最大节点数量,所以您需要选择足够高的分区以适应未来的增长。但是,每个分区也有管理开销,所以选择太高的数字是适得其反的。
@ -220,15 +218,15 @@ Cassandra在两种分区策略之间达成了一个折衷[11,12,13]。 Cassandra
#### 动态分区 #### 动态分区
对于使用键范围分区的数据库(参阅第202页的“按键范围分区”),具有固定边界的固定数量的分区将非常不方便:如果出现边界错误,则可能会导致所有一个分区中的数据和所有其他分区中的数据为空。手动重新配置分区边界将非常繁琐。 对于使用键范围分区的数据库(参阅“[按键范围分区](#按键范围分区)”),具有固定边界的固定数量的分区将非常不方便:如果出现边界错误,则可能会导致所有一个分区中的数据和所有其他分区中的数据为空。手动重新配置分区边界将非常繁琐。
出于这个原因按键的范围进行分区的数据库如HBase和RethinkDB会动态创建分区。当分区增长到超过配置的大小时在HBase上默认值是10GB它被分成两个分区大约每个分区各占一半的数据[26]。相反如果大量数据被删除并且分区缩小到某个阈值以下则可以将其与相邻分区合并。此过程与B树顶层发生的过程类似请参阅“B-树”)。 出于这个原因按键的范围进行分区的数据库如HBase和RethinkDB会动态创建分区。当分区增长到超过配置的大小时在HBase上默认值是10GB它被分成两个分区大约每个分区各占一半的数据【26】。相反如果大量数据被删除并且分区缩小到某个阈值以下则可以将其与相邻分区合并。此过程与B树顶层发生的过程类似参阅“[B树](ch2.md#B树)”)。
每个分区指派给一个节点每个节点可以处理多个分区就像固定数量的分区一样。大型分区拆分后可以将其中的一半转移到另一个节点以平衡负载。在HBase的情况下分区文件的传输通过HDFS底层分布式文件系统来实现[3] 每个分区指派给一个节点每个节点可以处理多个分区就像固定数量的分区一样。大型分区拆分后可以将其中的一半转移到另一个节点以平衡负载。在HBase的情况下分区文件的传输通过HDFS底层分布式文件系统来实现【3】
动态分区的一个优点是分区数量适应总数据量。如果只有少量的数据,少量的分区就足够了,所以开销很小;如果有大量的数据,每个分区的大小被限制在一个可配置的最大值[23] 动态分区的一个优点是分区数量适应总数据量。如果只有少量的数据,少量的分区就足够了,所以开销很小;如果有大量的数据,每个分区的大小被限制在一个可配置的最大值【23】
但是需要注意的是一个空的数据库从一个分区开始因为没有关于在哪里绘制分区边界的先验信息。虽然数据集很小直到达到第一个分区的分割点时所有写入操作都必须由单个节点处理而其他节点则处于空闲状态。为了解决这个问题HBase和MongoDB允许在一个空的数据库上配置一组初始分区这被称为**预分割pre-splitting**)。在键范围分区的情况下,预分割要求已经知道键分布的样子[4,26] 但是需要注意的是一个空的数据库从一个分区开始因为没有关于在哪里绘制分区边界的先验信息。虽然数据集很小直到达到第一个分区的分割点时所有写入操作都必须由单个节点处理而其他节点则处于空闲状态。为了解决这个问题HBase和MongoDB允许在一个空的数据库上配置一组初始分区这被称为**预分割pre-splitting**)。在键范围分区的情况下,预分割要求已经知道键分布的样子【4,26】
动态分区不仅适用于关键的范围分区数据而且也适用于散列分区数据。从版本2.4开始MongoDB同时支持键范围和哈希分区并且在任何情况下动态分割分区。 动态分区不仅适用于关键的范围分区数据而且也适用于散列分区数据。从版本2.4开始MongoDB同时支持键范围和哈希分区并且在任何情况下动态分割分区。
@ -236,10 +234,11 @@ Cassandra在两种分区策略之间达成了一个折衷[11,12,13]。 Cassandra
通过动态分区,分区的数量与数据集的大小成正比,因为拆分和合并过程将每个分区的大小保持在固定的最小值和最大值之间。另一方面,对于固定数量的分区,每个分区的大小与数据集的大小成正比。在这两种情况下,分区的数量都与节点的数量无关。 通过动态分区,分区的数量与数据集的大小成正比,因为拆分和合并过程将每个分区的大小保持在固定的最小值和最大值之间。另一方面,对于固定数量的分区,每个分区的大小与数据集的大小成正比。在这两种情况下,分区的数量都与节点的数量无关。
Cassandra和Ketama使用的第三种方法是使分区数与节点数成比例 - 换句话说,每个节点具有固定数量的分区[23,27,28]。在这种情况下,每个分区的大小与数据集大小成比例地增长,而节点数量保持不变,但是当增加节点数时,分区将再次变小。由于较大的数据量通常需要较大数量的节点进行存储,因此这种方法也使每个分区的大小相当稳定。 Cassandra和Ketama使用的第三种方法是使分区数与节点数成比例 - 换句话说,每个节点具有固定数量的分区【23, 27, 28】。在这种情况下,每个分区的大小与数据集大小成比例地增长,而节点数量保持不变,但是当增加节点数时,分区将再次变小。由于较大的数据量通常需要较大数量的节点进行存储,因此这种方法也使每个分区的大小相当稳定。
当一个新节点加入集群时它随机选择固定数量的现有分区进行拆分然后占有这些拆分分区中每个分区的一半同时将每个分区的另一半留在原地。随机化可能会产生不公平的分裂但是当在更大数量的分区上进行平均时在Cassandra中默认情况下每个节点有256个分区新节点最终从现有节点获得公平的负载份额。 Cassandra 3.0引入了另一种可重用的算法来避免不公平的分裂[29]。 当一个新节点加入集群时它随机选择固定数量的现有分区进行拆分然后占有这些拆分分区中每个分区的一半同时将每个分区的另一半留在原地。随机化可能会产生不公平的分裂但是当在更大数量的分区上进行平均时在Cassandra中默认情况下每个节点有256个分区新节点最终从现有节点获得公平的负载份额。 Cassandra 3.0引入了另一种可重用的算法来避免不公平的分裂【29】。
随机选择分区边界要求使用基于散列的分区(所以可以从散列函数产生的数字范围中挑选边界)。实际上,这种方法最符合一致性散列的原始定义[7]请参阅第204页的“一致性散列”。较新的哈希函数可以在降低元数据开销的情况下达到类似的效果[8]。
随机选择分区边界要求使用基于散列的分区所以可以从散列函数产生的数字范围中挑选边界。实际上这种方法最符合一致性散列的原始定义【7】参阅“[一致性散列](#一致性散列)”。较新的哈希函数可以在降低元数据开销的情况下达到类似的效果【8】。
### 运维:手动还是自动平衡 ### 运维:手动还是自动平衡
@ -259,12 +258,12 @@ 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. 首先将所有来自客户端的请求发送到路由选择层,它决定了应该处理请求的节点,并相应地转发。此路由层本身不处理任何请求;它仅充当分区感知负载平衡器。 2. 首先将所有来自客户端的请求发送到路由选择层,它决定了应该处理请求的节点,并相应地转发。此路由层本身不处理任何请求它仅充当分区感知负载平衡器。
3. 要求客户端知道分区和节点分配。在这种情况下,客户端可以直接连接到适当的节点,而不需要任何中介。 3. 要求客户端知道分区和节点分配。在这种情况下,客户端可以直接连接到适当的节点,而不需要任何中介。
在所有情况下,关键问题是:作出路由决策的组件(可能是节点之一,还是路由层或客户端)如何了解分区-节点之间的分配关系变化? 在所有情况下,关键问题是:作出路由决策的组件(可能是节点之一,还是路由层或客户端)如何了解分区-节点之间的分配关系变化?
@ -273,19 +272,19 @@ Cassandra和Ketama使用的第三种方法是使分区数与节点数成比例 -
**图6-7 将请求路由到正确节点的三种不同方式。** **图6-7 将请求路由到正确节点的三种不同方式。**
这是一个具有挑战性的问题,因为重要的是所有参与者都同意 - 否则请求将被发送到错误的节点,而不是正确处理。 在分布式系统中有达成共识的协议但很难正确地实现见第9章 这是一个具有挑战性的问题,因为重要的是所有参与者都同意 - 否则请求将被发送到错误的节点,而不是正确处理。 在分布式系统中有达成共识的协议,但很难正确地实现(见[第9章](ch9.md))。
许多分布式数据系统都依赖于一个独立的协调服务比如Zoo-Keeper来跟踪集群元数据如图6-8所示。 每个节点在ZooKeeper中注册自己ZooKeeper维护分区到节点的权威映射。 其他参与者如路由层或分区感知客户端可以在ZooKeeper中订阅此信息。 只要分区改变了所有权或者添加或删除了一个节点ZooKeeper就会通知路由层以使路由信息保持最新状态。 许多分布式数据系统都依赖于一个独立的协调服务比如ZooKeeper来跟踪集群元数据[图6-8](img/fig6-8.png)所示。 每个节点在ZooKeeper中注册自己ZooKeeper维护分区到节点的权威映射。 其他参与者如路由层或分区感知客户端可以在ZooKeeper中订阅此信息。 只要分区改变了所有权或者添加或删除了一个节点ZooKeeper就会通知路由层以使路由信息保持最新状态。
![](img/fig6-8.png) ![](img/fig6-8.png)
**图6-8 使用ZooKeeper跟踪分区分配给节点。** **图6-8 使用ZooKeeper跟踪分区分配给节点。**
例如LinkedIn的Espresso使用Helix [31]进行集群管理依靠ZooKeeper实现了一个路由层如图6-8所示。 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 protocal**来传播群集状态的变化。请求可以发送到任意节点该节点会转发到包含所请求的分区的适当节点图6-7中的方法1。这个模型在数据库节点中增加了更多的复杂性但是避免了对像ZooKeeper这样的外部协调服务的依赖。 Cassandra和Riak采取不同的方法他们在节点之间使用**八卦协议gossip protocal**来传播群集状态的变化。请求可以发送到任意节点,该节点会转发到包含所请求的分区的适当节点([图6-7]()中的方法1。这个模型在数据库节点中增加了更多的复杂性但是避免了对像ZooKeeper这样的外部协调服务的依赖。
Couchbase不会自动重新平衡这简化了设计。通常情况下它配置了一个名为moxi的路由选择层它会从集群节点了解路由变化[32] Couchbase不会自动重新平衡这简化了设计。通常情况下它配置了一个名为moxi的路由选择层它会从集群节点了解路由变化【32】
当使用路由层或向随机节点发送请求时客户端仍然需要找到要连接的IP地址。这些分区并不像分配给节点那么快所以为此使用DNS通常就足够了。 当使用路由层或向随机节点发送请求时客户端仍然需要找到要连接的IP地址。这些分区并不像分配给节点那么快所以为此使用DNS通常就足够了。
@ -295,7 +294,7 @@ Couchbase不会自动重新平衡这简化了设计。通常情况下
然而,通常用于分析的**大规模并行处理MPP, Massively parallel processing**关系数据库产品在其支持的查询类型方面要复杂得多。一个典型的数据仓库查询包含多个连接,过滤,分组和聚合操作。 MPP查询优化器将这个复杂的查询分解成许多执行阶段和分区其中许多可以在数据库集群的不同节点上并行执行。涉及扫描大部分数据集的查询尤其受益于这种并行执行。 然而,通常用于分析的**大规模并行处理MPP, Massively parallel processing**关系数据库产品在其支持的查询类型方面要复杂得多。一个典型的数据仓库查询包含多个连接,过滤,分组和聚合操作。 MPP查询优化器将这个复杂的查询分解成许多执行阶段和分区其中许多可以在数据库集群的不同节点上并行执行。涉及扫描大部分数据集的查询尤其受益于这种并行执行。
数据仓库查询的快速并行执行是一个专门的话题由于分析有很强的商业重要性它收到了很多商业利益。我们将在第10章讨论并行查询执行的一些技巧。有关并行数据库中使用的技术的更详细的概述请参阅参考文献[1,33] 数据仓库查询的快速并行执行是一个专门的话题由于分析有很强的商业重要性它收到了很多商业利益。我们将在第10章讨论并行查询执行的一些技巧。有关并行数据库中使用的技术的更详细的概述请参阅参考文献【1,33】
@ -331,10 +330,6 @@ Couchbase不会自动重新平衡这简化了设计。通常情况下
参考文献 参考文献
-------------------- --------------------
[1]: http://www.cs.cmu.edu/~pavlo/courses/fall2013/static/papers/dewittgray92.pdf "David J. DeWitt and Jim N. Gray: “Parallel Database Systems: The Future of High Performance Database Systems,” Communications of the ACM, volume 35, number 6, pages 8598, June 1992. doi:10.1145/129888.129894"
1. David J. DeWitt and Jim N. Gray: “[Parallel Database Systems: The Future of High Performance Database Systems](),” 1. David J. DeWitt and Jim N. Gray: “[Parallel Database Systems: The Future of High Performance Database Systems](),”
*Communications of the ACM*, volume 35, number 6, pages 8598, June 1992. [doi:10.1145/129888.129894](http://dx.doi.org/10.1145/129888.129894) *Communications of the ACM*, volume 35, number 6, pages 8598, June 1992. [doi:10.1145/129888.129894](http://dx.doi.org/10.1145/129888.129894)