mirror of
https://github.com/Vonng/ddia.git
synced 2024-12-06 15:20:12 +08:00
修复3,4,5,6章的格式
This commit is contained in:
parent
854474e9bc
commit
8811de70b2
@ -83,8 +83,8 @@
|
||||
| 序言 | [preface.md](preface.md) | 机翻 |
|
||||
| 第一部分:数据系统基础 ——概览 | [part-i.md](part-i.md) | 初翻 |
|
||||
| 第一章:可靠性、可扩展性、可维护性 | [ch1.md](ch1.md) | **精翻** |
|
||||
| 第二章:数据模型与查询语言 | [ch2.md](ch2.md) | 初翻 |
|
||||
| 第三章:存储与检索 | [ch3.md](ch3.md) | 初翻 |
|
||||
| 第二章:数据模型与查询语言 | [ch2.md](ch2.md) | 初翻 50% |
|
||||
| 第三章:存储与检索 | [ch3.md](ch3.md) | 初翻 60% |
|
||||
| 第四章:编码与演化 | [ch4.md](ch4.md) | 初翻 |
|
||||
| 第二部分:分布式数据——概览 | [part-ii.md](part-ii.md) | 初翻 |
|
||||
| 第五章:复制 | [ch5.md](ch5.md) | 初翻 |
|
||||
|
357
ddia/ch2.md
357
ddia/ch2.md
@ -11,15 +11,13 @@
|
||||
|
||||
[TOC]
|
||||
|
||||
|
||||
|
||||
数据模型可能是开发软件最重要的部分,因为它们有着深远的影响:不仅在于软件的编写方式,而且在于它如何影响对问题解决方案的思考。
|
||||
数据模型可能是开发软件最重要的部分,因为它们有着深远的影响:不仅影响软件的编写方式,而且会影响我们的解题思路。
|
||||
|
||||
大多数应用程序是通过将一个数据模型叠加在另一个之上来构建的。对于每一层,关键问题是:它是如何用下一层来表示的?例如:
|
||||
|
||||
1. 作为一名应用程序开发人员,您将看到现实世界(包括人员,组织,货物,行为,资金流向,传感器等),并根据对象或数据结构以及API进行建模,操纵这些数据结构。这些结构通常是应用程序特定的。
|
||||
2. 如果要存储这些数据结构,可以使用通用数据模型(如JSON或XML文档,关系数据库中的表、或图模型)来表示它们。
|
||||
3. 构建数据库软件的工程师决定以内存,磁盘或网络上的字节表示JSON / XML /关系/图形数据。该表示可以允许以各种方式查询,搜索,操纵和处理数据。
|
||||
3. 构建数据库软件的工程师决定以内存,磁盘或网络上的字节表示JSON/XML/关系/图数据。该表示可以允许以各种方式查询,搜索,操纵和处理数据。
|
||||
4. 在更低的层面上,硬件工程师已经计算出如何用电流,光脉冲,磁场等来表示字节。
|
||||
|
||||
在一个复杂的应用程序中,可能会有更多的中间层次,比如基于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的诞生
|
||||
|
||||
现在,在2010年代,NoSQL是推翻关系模式主导地位的最新尝试。 “NoSQL”这个名字是不幸的,因为它实际上并没有涉及到任何特定的技术,它最初只是作为一个吸引人的Twitter标签在2009年的开源,分布式,非关系数据库上聚会。无论如何,这个术语触动了一个神经,并迅速通过网络启动社区和更远的地方传播开来。一些有趣的数据库系统现在与#NoSQL#标签相关联,并被追溯性地重新解释为不仅是SQL [4]。
|
||||
现在在2010年代,NoSQL是推翻关系模式主导地位的最新尝试。 “NoSQL”这个名字非常不幸,因为它实际上并没有涉及到任何特定的技术,它最初只是作为一个吸引人的Twitter标签在2009年的一个关于分布式,非关系数据库上的开源聚会。无论如何,这个术语触动了某些神经,并迅速通过网络启动社区和更远的地方传播开来。一些有趣的数据库系统现在与*#NoSQL#*标签相关联,并被追溯性地重新解释为不仅是SQL 【4】。
|
||||
|
||||
采用NoSQL数据库有几个驱动力,其中包括:
|
||||
|
||||
@ -65,19 +63,19 @@
|
||||
|
||||
[^i]: 从电子学借用一个术语。每个电路的输入和输出都有一定的阻抗(交流电阻)。当您将一个电路的输出连接到另一个电路的输入时,如果两个电路的输出和输入阻抗匹配,则连接上的功率传输将被最大化。阻抗不匹配可能导致信号反射和其他问题
|
||||
|
||||
像ActiveRecord和Hibernate这样的对象关系映射(ORM)框架减少了这个翻译层需要的样板代码的数量,但是它们不能完全隐藏这两个模型之间的差异。
|
||||
像ActiveRecord和Hibernate这样的**对象关系映射(object-relational mapping, ORM)**框架减少了这个翻译层需要的样板代码的数量,但是它们不能完全隐藏这两个模型之间的差异。
|
||||
|
||||
![](img/fig2-1.png)
|
||||
|
||||
**图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模型(SQL:1999之前)中,最常见的规范化表示形式是将职位,培训和联系信息放在单独的表中,对用户表提供外键引用,如图2-1所示。
|
||||
* 更高版本的SQL标准增加了对结构化数据类型和XML数据的支持;这允许将多值数据存储在单行内,支持在这些文档内查询和索引。这些功能在Oracle,IBM DB2,MS SQL Server和PostgreSQL中都有不同程度的支持[6,7]。 JSON数据类型也受到几个数据库的支持,包括IBM DB2,MySQL和PostgreSQL [8]。
|
||||
* 第三种选择是将作业,教育和联系信息编码为JSON或XML文档,将其存储在数据库的文本列中,并让应用程序解释其结构和内容。在此设置中,通常不能使用数据库查询该编码列中的值。
|
||||
* 在传统SQL模型(SQL:1999之前)中,最常见的规范化表示形式是将职位,培训和联系信息放在单独的表中,对用户表提供外键引用,如[图2-1](img/fig2-1.png)所示。
|
||||
* 更高版本的SQL标准增加了对结构化数据类型和XML数据的支持;这允许将多值数据存储在单行内,支持在这些文档内查询和索引。这些功能在Oracle,IBM DB2,MS SQL Server和PostgreSQL中都有不同程度的支持【6,7】。 JSON数据类型也受到几个数据库的支持,包括IBM DB2,MySQL和PostgreSQL 【8】。
|
||||
* 第三种选择是将职业,教育和联系信息编码为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
|
||||
{
|
||||
@ -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)
|
||||
|
||||
@ -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时,对人类有意义的信息(比如慈善词)只存储在一个地方,引用它的所有信息都使用一个ID(ID只在数据库中有意义)。当你直接存储文本时,每个使用它的记录中,都存储的是有意义的信息。
|
||||
无论是存储一个ID还是一个文本字符串,都是一个关于**重复**的问题。当你使用一个ID时,对人类有意义的信息(比如单词:慈善)只存储在一个地方,引用它的所有信息都使用一个ID(ID只在数据库中有意义)。当你直接存储文本时,每个使用它的记录中,都存储的是有意义的信息。
|
||||
|
||||
使用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-4 阐明了这些新功能怎样使用多对多关系。 每个虚线矩形内的数据可以分组成一个文档,但是对单位,学校和其他用户的引用需要表示为引用,并且在查询时需要连接。
|
||||
[图2-4](img/fig2-4.png)阐明了这些新功能怎样使用多对多关系。 每个虚线矩形内的数据可以分组成一个文档,但是对单位,学校和其他用户的引用需要表示为引用,并且在查询时需要连接。
|
||||
|
||||
![](img/fig2-4.png)
|
||||
|
||||
@ -171,79 +175,79 @@ JSON表示比图2-1中的多表模式具有更好的局部性。如果要在关
|
||||
|
||||
虽然关系数据库中经常使用多对多的关系和连接,但文档数据库和NoSQL重新讨论了如何最好地在数据库中表示这种关系的争论。这个辩论比NoSQL早得多,事实上,它可以追溯到最早的计算机化数据库系统。
|
||||
|
||||
20世纪70年代最受欢迎的业务数据处理数据库是IBM的信息管理系统(IMS),最初是为了在阿波罗太空计划中进行存货而开发的,并于1968年首次商业发布[13]。目前它仍在使用和维护,在IBM大型机的OS / 390上运行[14]。
|
||||
IMS的设计使用了一个相当简单的数据模型,称为分层模型,它与文档数据库使用的JSON模型有一些显着的相似之处[2]。它将所有数据表示为嵌套在记录中的记录树,就像图2-2的JSON结构一样。
|
||||
20世纪70年代最受欢迎的业务数据处理数据库是IBM的信息管理系统(IMS),最初是为了在阿波罗太空计划中进行库存管理而开发的,并于1968年首次商业发布【13】。目前它仍在使用和维护,在IBM大型机的OS/390上运行【14】。
|
||||
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委员会成员也承认,这就像在一个n维数据空间中进行导航[17]。
|
||||
CODASYL中的查询是通过遍历记录列表和访问路径后,通过在数据库中移动游标来执行的。如果记录有多个父母(即来自其他记录的多个传入指针),则应用程序代码必须跟踪所有的各种关系。甚至CODASYL委员会成员也承认,这就像在一个n维数据空间中进行导航【17】。
|
||||
|
||||
尽管手动访问路径选择能够最有效地利用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支持通常带有可选的模式验证。没有模式意味着可以将任意的键和值添加到文档中,并且在读取时,客户端对于文档可能包含的字段没有保证。
|
||||
|
||||
文档数据库有时称为无模式,但这是误导性的,因为读取数据的代码通常采用某种结构 - 即存在隐式模式,但不由数据库强制执行[20]。一个更精确的术语是 读时模式(schema-on-read数据的结构是隐含的,只有在数据被读取时才被解释),相应的是写时模式(schema-on-write 传统的关系数据库方法,模式是明确,数据库确保所有的数据都符合它的形式)[21]。
|
||||
文档数据库有时称为**无模式(schemaless)**,但这是误导性的,因为读取数据的代码通常采用某种结构——即存在隐式模式,但不由数据库强制执行【20】。一个更精确的术语是**读时模式(schema-on-read)**(数据的结构是隐含的,只有在数据被读取时才被解释),相应的是**写时模式(schema-on-write)**(传统的关系数据库方法,模式是明确,数据库确保所有的数据都符合它的形式)【21】。
|
||||
|
||||
读取模式类似于编程语言中的动态(运行时)类型检查,而模式写入类似于静态(编译时)类型检查。就像静态和动态类型检查的倡导者对于它们的相对优点有很大的争议[22],数据库中模式的执行是一个有争议的话题,一般来说没有正确或错误的答案。
|
||||
读取模式类似于编程语言中的动态(运行时)类型检查,而模式写入类似于静态(编译时)类型检查。就像静态和动态类型检查的倡导者对于它们的相对优点有很大的争议【22】,数据库中模式的执行是一个有争议的话题,一般来说没有正确或错误的答案。
|
||||
|
||||
在应用程序想要改变其数据格式的情况下,这些方法之间的区别特别明显。例如,假设你正在将每个用户的全名存储在一个字段中,而你想分别存储名字和姓氏[23]。在文档数据库中,只需开始使用新字段写入新文档,并在应用程序中使用代码来处理读取旧文档时的情况。例如:
|
||||
在应用程序想要改变其数据格式的情况下,这些方法之间的区别特别明显。例如,假设你正在将每个用户的全名存储在一个字段中,而你想分别存储名字和姓氏【23】。在文档数据库中,只需开始使用新字段写入新文档,并在应用程序中使用代码来处理读取旧文档时的情况。例如:
|
||||
|
||||
```go
|
||||
if (user && user.name && !user.first_name) {
|
||||
// Documents written before Dec 8, 2013 don't have first_name
|
||||
user.first_name = user.name.split(" ")[0];
|
||||
// Documents written before Dec 8, 2013 don't have first_name
|
||||
user.first_name = user.name.split(" ")[0];
|
||||
}
|
||||
```
|
||||
|
||||
@ -251,13 +255,13 @@ if (user && user.name && !user.first_name) {
|
||||
|
||||
```sql
|
||||
ALTER TABLE users ADD COLUMN first_name text;
|
||||
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 = split_part(name, ' ', 1); -- PostgreSQL
|
||||
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
|
||||
|
||||
#### 查询的数据局部性
|
||||
|
||||
文档通常以单个连续字符串形式存储,编码为JSON,XML或其二进制变体(如MongoDB的BSON)。如果您的应用程序经常需要访问整个文档(例如,将其渲染至网页),则此存储局部性会带来性能优势。如果将数据分割到多个表中(如图2-1所示),则需要进行多次索引查找才能将其全部检索出来,这可能需要更多的磁盘查找并花费更多的时间。
|
||||
文档通常以单个连续字符串形式存储,编码为JSON,XML或其二进制变体(如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文档中进行索引和查询的功能,这允许应用程序使用与使用文档数据库时所做的非常相似的数据模型。
|
||||
|
||||
从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使用命令式的代码来查询数据库。那是什么意思?
|
||||
|
||||
许多常用的编程语言是命令式的。例如,如果你有一个动物物种的列表,返回列表中的鲨鱼可以这样写:
|
||||
|
||||
```js
|
||||
@ -312,7 +318,7 @@ function getSharks() {
|
||||
$$
|
||||
sharks = σ_{family = "sharks"}(animals)
|
||||
$$
|
||||
σ(希腊字母西格玛)是选择操作符,只返回符合条件的动物,family=“shark”。
|
||||
σ(希腊字母西格玛)是选择操作符,只返回符合条件的动物,`family="shark"`。
|
||||
|
||||
在定义SQL时,它紧密地遵循关系代数的结构:
|
||||
|
||||
@ -327,9 +333,10 @@ SELECT * FROM animals WHERE family ='Sharks';
|
||||
声明式查询语言是有吸引力的,因为它通常比命令式API更加简洁和容易。但更重要的是,它还隐藏了数据库引擎的实现细节,这使得数据库系统可以在不需要对查询进行任何更改的情况下提高性能。
|
||||
|
||||
例如,在本节开头所示的命令代码中,动物列表以特定顺序出现。如果数据库想要在场景后面回收未使用的磁盘空间,则可能需要移动记录,改变动物出现的顺序。数据库能否安全地执行,而不会中断查询?
|
||||
|
||||
SQL示例不保证任何特定的顺序,所以它不介意顺序是否改变。但是如果查询被写为命令式的代码,那么数据库就永远不能确定代码是否依赖于排序。 SQL在功能上更加有限的事实为数据库提供了更多自动优化的空间。
|
||||
|
||||
最后,声明性语言往往适合于并行执行。现在,通过增加更多的内核,CPU的速度会更快,而不是以比以前更高的时钟速度运行[31]。命令代码很难在多个内核和多个机器之间并行化,因为它指定了必须以特定顺序执行的指令。声明性语言在并行执行中获得更快的机会,因为它们仅指定结果的模式,而不是用于确定结果的算法。如果合适,数据库可以自由使用查询语言的并行实现[32]。
|
||||
最后,声明性语言往往适合于并行执行。现在,通过增加更多的内核,CPU的速度会更快,而不是以比以前更高的时钟速度运行【31】。命令代码很难在多个内核和多个机器之间并行化,因为它指定了必须以特定顺序执行的指令。声明性语言在并行执行中获得更快的机会,因为它们仅指定结果的模式,而不是用于确定结果的算法。如果合适,数据库可以自由使用查询语言的并行实现【32】。
|
||||
|
||||
### Web上的声明式查询
|
||||
|
||||
@ -361,7 +368,7 @@ SQL示例不保证任何特定的顺序,所以它不介意顺序是否改变
|
||||
|
||||
```css
|
||||
li.selected > p {
|
||||
background-color: blue;
|
||||
background-color: blue;
|
||||
}
|
||||
```
|
||||
|
||||
@ -379,7 +386,7 @@ li.selected > p {
|
||||
|
||||
在这里,XPath表达式`li[@class='selected']/p`相当于上例中的CSS选择器`li.selected> p`。 CSS和XSL的共同之处在于,它们都是用于指定文档样式的声明性语言。
|
||||
|
||||
想象一下,如果你必须使用一命令式的方法,生活会是什么样子。在Javascript中,使用文档对象模型(DOM)API,结果可能如下所示:
|
||||
想象一下,如果你必须使用一命令式的方法,生活会是什么样子。在Javascript中,使用**文档对象模型(DOM)**API,结果可能如下所示:
|
||||
|
||||
```js
|
||||
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的性能。
|
||||
|
||||
在Web浏览器中,使用声明式CSS样式比在JavaScript中强制操作样式好得多。类似地,在数据库中,像SQL这样的声明式查询语言比命令式查询API(IMS与CODASYL都是命令式的,应用程序通常通过COBOL代码来一次一行迭代数据库中的记录)要好得多
|
||||
在Web浏览器中,使用声明式CSS样式比在JavaScript中强制操作样式好得多。类似地,在数据库中,像SQL这样的声明式查询语言比命令式查询API(IMS与CODASYL都是命令式的,应用程序通常通过COBOL代码来一次一行迭代数据库中的记录)要好得多[^vi]。
|
||||
|
||||
[^vi]: vi IMS和CODASYL都使用命令式API。 应用程序通常使用COBOL代码遍历数据库中的记录,一次一条记录【2,16】。
|
||||
|
||||
### MapReduce查询
|
||||
|
||||
MapReduce是一个编程模型,用于在许多机器上批量处理大量的数据,由Google推广[33]。一些NoSQL数据存储(包括MongoDB和CouchDB)支持有限形式的MapReduce,作为在多个文档中执行只读查询的机制。
|
||||
总的来说,MapReduce在第10章中有更详细的描述。现在我们将简要讨论一下MongoDB对这一模型的应用。
|
||||
MapReduce是一个编程模型,用于在许多机器上批量处理大量的数据,由Google推广【33】。一些NoSQL数据存储(包括MongoDB和CouchDB)支持有限形式的MapReduce,作为在多个文档中执行只读查询的机制。
|
||||
总的来说,MapReduce在[第10章](ch10.md)中有更详细的描述。现在我们将简要讨论一下MongoDB对这一模型的应用。
|
||||
|
||||
MapReduce既不是一个声明性的查询语言,也不是一个完全强制性的查询API,而是位于两者之间的地方:查询的逻辑用代码片断来表示,这些代码片段被处理框架重复地调用。它基于许多函数式编程语言中存在的map(也称为collect)和reduce(也称为fold或inject)函数。
|
||||
|
||||
@ -418,8 +427,8 @@ MapReduce既不是一个声明性的查询语言,也不是一个完全强制
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
date_trunc('month', observation_timestamp) AS observation_month,
|
||||
sum(num_animals) AS total_animals
|
||||
date_trunc('month', observation_timestamp) AS observation_month,
|
||||
sum(num_animals) AS total_animals
|
||||
FROM observations
|
||||
WHERE family = 'Sharks'
|
||||
GROUP BY observation_month;
|
||||
@ -450,9 +459,9 @@ db.observations.mapReduce(function map() {
|
||||
|
||||
* 可以声明性地指定只考虑鲨鱼种类的过滤器(这是对MapReduce的特定于MongoDB的扩展)。
|
||||
* 每个匹配查询的文档都会调用一次JavaScript函数`map`,将`this`设置为文档对象。
|
||||
* map函数发出一个键(包括年份和月份的字符串,如“2013-12”或“2014-1”)和一个值(该观察中的动物数量)。
|
||||
* map发出的键值对按键组合。 对于具有相同键(即,相同的月份和年份)的所有键值对,`reduce`函数被调用一次。
|
||||
* reduce函数将特定月份内所有观测值的动物数量相加。
|
||||
* `map`函数发出一个键(包括年份和月份的字符串,如`"2013-12"`或`"2014-1"`)和一个值(该观察中的动物数量)。
|
||||
* `map`发出的键值对按键组合。 对于具有相同键(即,相同的月份和年份)的所有键值对,`reduce`函数被调用一次。
|
||||
* `reduce`函数将特定月份内所有观测值的动物数量相加。
|
||||
* 最终的输出被写入到`monthlySharkReport`集合中。
|
||||
|
||||
例如假设`observations`集合包含这两个文档:
|
||||
@ -476,11 +485,11 @@ db.observations.mapReduce(function map() {
|
||||
|
||||
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
|
||||
db.observations.aggregate([
|
||||
@ -504,13 +513,13 @@ db.observations.aggregate([
|
||||
|
||||
但是,如果多对多的关系在您的数据中很常见呢?关系模型可以处理多对多关系的简单情况,但是随着数据之间的连接变得更加复杂,开始将数据建模为图形变得更加自然。
|
||||
|
||||
一个图由两种对象组成:顶点(vertices 也称为节点nodes 或实体 entities)和边(edges 也称为关系relationships或弧 arcs )。许多种数据可以被模拟为一个图形。典型的例子包括:
|
||||
一个图由两种对象组成:**顶点(vertices)**(也称为**节点(nodes)** 或**实体(entities)**),和**边(edges)**( 也称为**关系(relationships)**或**弧 (arcs)** )。许多种数据可以被模拟为一个图形。典型的例子包括:
|
||||
|
||||
***社交图表***
|
||||
***社交图谱***
|
||||
|
||||
顶点是人,边指示哪些人彼此认识。
|
||||
|
||||
***网络图***
|
||||
***网络图谱***
|
||||
|
||||
顶点是网页,边缘表示到其他页面的HTML链接。
|
||||
|
||||
@ -520,35 +529,36 @@ db.observations.aggregate([
|
||||
|
||||
众所周知的算法可以在这些图上进行操作:例如,汽车导航系统搜索道路网络中两点之间的最短路径,PageRank可以用在网络图上来确定网页的流行程度,从而其在搜索结果中的排名。
|
||||
|
||||
在刚刚给出的例子中,图中的所有顶点代表了相同类型的东西(人,网页或交叉路口)。然而,图并不局限于这样的同类数据:图的同样强大的用途是提供一种在单个数据存储中存储完全不同类型的对象的一致方式。例如,Facebook维护一个包含许多不同类型的顶点和边的单个图:顶点表示用户所做的人,地点,事件,签到和评论;边缘表示哪些人是彼此的朋友,哪个位置发生了检查,谁评论了哪个职位,谁参加了哪个事件,等等[35]。
|
||||
在刚刚给出的例子中,图中的所有顶点代表了相同类型的东西(人,网页或交叉路口)。然而,图并不局限于这样的同类数据:图的同样强大的用途是提供一种在单个数据存储中存储完全不同类型的对象的一致方式。例如,Facebook维护一个包含许多不同类型的顶点和边的单个图:顶点表示用户所做的人,地点,事件,签到和评论;边缘表示哪些人是彼此的朋友,哪个位置发生了检查,谁评论了哪个职位,谁参加了哪个事件,等等【35】。
|
||||
|
||||
在本节中,我们将使用图2-5所示的示例。它可以从社交网络或系谱数据库中获得:它显示了两个人,来自爱达荷州的Lucy和来自法国Beaune的Alain。他们已婚,住在伦敦。
|
||||
在本节中,我们将使用[图2-5](img/fig2-5.png)所示的示例。它可以从社交网络或系谱数据库中获得:它显示了两个人,来自爱达荷州的Lucy和来自法国Beaune的Alain。他们已婚,住在伦敦。
|
||||
|
||||
![](img/fig2-5.png)
|
||||
|
||||
**图2-5 图数据结构示例(框代表顶点,箭头代表边)**
|
||||
|
||||
有几种不同但相关的方法来构建和查询图表中的数据。 在本节中,我们将讨论属性图模型(由Neo4j,Titan和InfiniteGraph实现)和三存储(triple-store)模型(由Datomic,AllegroGraph等实现)。 我们将看图的三种声明性查询语言:Cypher,SPARQL和Datalog。 除此之外,还有像Gremlin [36]这样的图形查询语言和像Pregel这样的图形处理框架(见第10章)。
|
||||
有几种不同但相关的方法来构建和查询图表中的数据。 在本节中,我们将讨论属性图模型(由Neo4j,Titan和InfiniteGraph实现)和三元组存储(triple-store)模型(由Datomic,AllegroGraph等实现)。 我们将看图的三种声明性查询语言:Cypher,SPARQL和Datalog。 除此之外,还有像Gremlin 【36】这样的图形查询语言和像Pregel这样的图形处理框架(见[第10章](ch10.md))。
|
||||
|
||||
### 属性图
|
||||
|
||||
在属性图模型中,每个顶点(vertex)包括:
|
||||
在属性图模型中,每个**顶点(vertex)**包括:
|
||||
|
||||
* 唯一的标识符
|
||||
* 出向边的集合(outgoing edges)
|
||||
* 入向边的集合(ingoing edges)
|
||||
* **出向边集合(outgoing edges)**
|
||||
* **入向边集合(ingoing edges)**
|
||||
* 一组属性(键值对)
|
||||
|
||||
每条边(edge)包括:
|
||||
每条**边(edge)**包括:
|
||||
|
||||
* 唯一标识符
|
||||
* 边的起点(tail vertex 箭头出发的点)
|
||||
* 边的终点(head vertex 箭头指向的点)
|
||||
* **边的起点/尾点(tail vertex)**
|
||||
* **边的终点/头点(head vertex)**
|
||||
* 描述两个顶点之间关系类型的标签
|
||||
* 一组属性(键值对)
|
||||
|
||||
可以将图存储看作两个关系表:一个存储顶点,另一个存储边,如例2-2所示(该模式使用PostgreSQL json数据类型来存储每个顶点或边的属性)。头部和尾部顶点存储为每个边缘。如果您想要一组顶点的输入或输出边,您可以分别通过head_vertex或tail_vertex来查询边表。
|
||||
例2-2 使用关系模式来表示属性图
|
||||
可以将图存储看作两个关系表:一个存储顶点,另一个存储边,如[例2-2]()所示(该模式使用PostgreSQL json数据类型来存储每个顶点或边的属性)。头部和尾部顶点存储为每个边缘。如果您想要一组顶点的输入或输出边,您可以分别通过`head_vertex`或`tail_vertex`来查询边表。
|
||||
|
||||
**例2-2 使用关系模式来表示属性图**
|
||||
|
||||
```sql
|
||||
CREATE TABLE vertices (
|
||||
@ -571,64 +581,67 @@ CREATE INDEX edges_heads ON edges (head_vertex);
|
||||
这个模型的一些重要方面是:
|
||||
|
||||
1. 任何顶点都可以有一个边连接到任何其他顶点。没有哪种事物可不可以关联的模式限制。
|
||||
2. 给定任何顶点,您可以高效地找到它的入边和出边,从而遍历图,即沿着一系列顶点的路径前后移动。 (这就是为什么示例2-2在tail_vertex和head_vertex列上都有索引的原因。)
|
||||
2. 给定任何顶点,您可以高效地找到它的入边和出边,从而遍历图,即沿着一系列顶点的路径前后移动。 (这就是为什么[例2-2]()在`tail_vertex`和`head_vertex`列上都有索引的原因。)
|
||||
3. 通过对不同类型的关系使用不同的标签,可以在一个图中存储几种不同的信息,同时仍然保持一个干净的数据模型。
|
||||
|
||||
这些特性为数据建模提供了很大的灵活性,如图2-5所示。图中显示了一些传统关系模式难以表达的东西,例如不同国家的不同地区结构(法国有省和州,美国有不同的州和州),国中国的怪事(先忽略主权国家和国家错综复杂的烂摊子),不同的数据粒度(露西现在的住所被指定为一个城市,而她的出生地点只是在一个州的级别)。
|
||||
这些特性为数据建模提供了很大的灵活性,如[图2-5](img/fig2-5.png)所示。图中显示了一些传统关系模式难以表达的东西,例如不同国家的不同地区结构(法国有省和州,美国有不同的州和州),国中国的怪事(先忽略主权国家和国家错综复杂的烂摊子),不同的数据粒度(露西现在的住所被指定为一个城市,而她的出生地点只是在一个州的级别)。
|
||||
|
||||
你可以想象延伸图还包括许多关于露西和阿兰,或其他人的其他事实。例如,您可以用它来表示食物过敏(通过为每个过敏原引入一个顶点,以及人与过敏原之间的边缘来指示过敏),并将过敏原与一组过敏原显示哪些食物含有哪些物质的顶点。然后,你可以写一个查询,找出每个人吃什么是安全的。图表对于可演化性是有利的:当您向应用程序添加功能时,可以轻松扩展图形以适应应用程序数据结构的变化。
|
||||
|
||||
### 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
|
||||
CREATE
|
||||
(NAmerica:Location {name:'North America', type:'continent'}),
|
||||
(USA:Location {name:'United States', type:'country' }),
|
||||
(Idaho:Location {name:'Idaho', type:'state' }),
|
||||
(Lucy:Person {name:'Lucy' }),
|
||||
(Idaho) -[:WITHIN]-> (USA) -[:WITHIN]-> (NAmerica),
|
||||
(Lucy) -[:BORN_IN]-> (Idaho)
|
||||
(NAmerica:Location {name:'North America', type:'continent'}),
|
||||
(USA:Location {name:'United States', type:'country' }),
|
||||
(Idaho:Location {name:'Idaho', type:'state' }),
|
||||
(Lucy:Person {name:'Lucy' }),
|
||||
(Idaho) -[:WITHIN]-> (USA) -[:WITHIN]-> (NAmerica),
|
||||
(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
|
||||
MATCH
|
||||
(person) -[:BORN_IN]-> () -[:WITHIN*0..]-> (us:Location {name:'United States'}),
|
||||
(person) -[:LIVES_IN]-> () -[:WITHIN*0..]-> (eu:Location {name:'Europe'})
|
||||
(person) -[:BORN_IN]-> () -[:WITHIN*0..]-> (us:Location {name:'United States'}),
|
||||
(person) -[:LIVES_IN]-> () -[:WITHIN*0..]-> (eu:Location {name:'Europe'})
|
||||
RETURN person.name
|
||||
```
|
||||
|
||||
查询可以被读取如下:
|
||||
|
||||
* 找到满足以下两者的所有顶点(称之为person):
|
||||
* 有一条到某个顶点`BORN_IN`类型的出边。从那个顶点开始可以沿着一系列`WITHIN`出边最终到达类型为`Location`,`name=United States`的顶点
|
||||
* 也有一条到某个顶点`LIVES_IN`类型的出边。沿着这条边,可以通过一系列`WITHIN`出边最终到达类型为`Location`,`name=Europe`的顶点
|
||||
* 对于这样的`Person`类型顶点,返回其name属性。
|
||||
> 找到满足以下两者的所有顶点(称之为person):
|
||||
> 1. 有一条到某个顶点`BORN_IN`类型的出边。从那个顶点开始可以沿着一系列`WITHIN`出边最终到达类型为`Location`,`name=United States`的顶点
|
||||
>
|
||||
>
|
||||
> 2. 也有一条到某个顶点`LIVES_IN`类型的出边。沿着这条边,可以通过一系列`WITHIN`出边最终到达类型为`Location`,`name=Europe`的顶点
|
||||
>
|
||||
> 对于这样的`Person`类型顶点,返回其`name`属性。
|
||||
|
||||
这条查询有几种可行的查询路径。这里给出的描述建议你首先扫描数据库中的所有人,检查每个人的出生地和居住地,然后只返回符合条件的人。
|
||||
|
||||
等价地,也可以从两个位置顶点开始并向后查找。如果名称属性上有一个索引,则可以高效地找到代表美国和欧洲的两个顶点。然后,您可以继续查找所有在WITHIN边缘中的位置(美国和欧洲的所有位置(州,地区,城市等))。最后,您可以查找可以通过在某个位置顶点处传入的BORN_IN或LIVES_IN边缘找到的人员。
|
||||
等价地,也可以从两个位置顶点开始并向后查找。如果名称属性上有一个索引,则可以高效地找到代表美国和欧洲的两个顶点。然后,您可以继续查找所有在`WITHIN`边中的位置(美国和欧洲的所有位置(州,地区,城市等))。最后,您可以查找可以通过在某个位置顶点处传入的`BORN_IN`或`LIVES_IN`边找到的人员。
|
||||
|
||||
对于声明性查询语言来说,典型的情况是,在编写查询语句时,您不需要指定执行细节:查询优化程序会自动选择预测效率最高的策略,因此您可以继续编写其余的应用程序。
|
||||
|
||||
### SQL中的图表查询
|
||||
|
||||
例2-2建议可以在关系数据库中表示图形数据。但是,如果我们把图形数据放入关系结构中,我们是否也可以使用SQL查询它?答案是肯定的,但有些困难。在关系数据库中,您通常会事先知道在查询中需要哪些连接。在图表查询中,您可能需要在找到要查找的顶点之前,遍历可变数量的边。也就是说,JOIN的数量事先并不确定。
|
||||
[例2-2]()建议可以在关系数据库中表示图形数据。但是,如果我们把图形数据放入关系结构中,我们是否也可以使用SQL查询它?答案是肯定的,但有些困难。在关系数据库中,您通常会事先知道在查询中需要哪些连接。在图表查询中,您可能需要在找到要查找的顶点之前,遍历可变数量的边。也就是说,连接的数量事先并不确定。
|
||||
|
||||
在我们的例子中,这发生在Cypher查询中的`() - [:WITHIN * 0 ..] - >()`规则中。一个人的LIVES_IN边缘可以指向任何类型的位置:街道,城市,地区,地区,国家等。城市可以在一个地区,在一个州内的一个地区,在一个国家内的一个州等等.LIVES_IN边缘可以直接指向你正在查找的位置,或者可以在位置层次结构中删除几个级别。
|
||||
在Cypher中,`WITHIN * 0`表示这个事实非常简洁:意思是“沿着一个WITHIN边,零次或多次”。它就像正则表达式中的*运算符。
|
||||
由于SQL:1999,查询中可变长度遍历路径的思想可以使用称为递归公用表表达式(`WITH RECURSIVE`语法)的东西来表示。例2-5显示了同样的查询 - 查找使用这种技术(PostgreSQL,IBM DB2,Oracle和SQL Server支持)中从美国移民到欧洲的人的姓名。但是,与Cypher相比,语法非常笨拙。
|
||||
在我们的例子中,这发生在Cypher查询中的`() -[:WITHIN*0..]-> ()`规则中。一个人的`LIVES_IN`边缘可以指向任何类型的位置:街道,城市,地区,地区,国家等。城市可以在一个地区,在一个州内的一个地区,在一个国家内的一个州等等。`LIVES_IN`边可以直接指向你正在查找的位置,或者可以在位置层次结构中删除几个级别。
|
||||
在Cypher中,`WITHIN * 0`表示这个事实非常简洁:意思是“沿着一个`WITHIN`边,零次或多次”。它就像正则表达式中的`*`运算符。
|
||||
由于SQL:1999,查询中可变长度遍历路径的思想可以使用称为递归公用表表达式(`WITH RECURSIVE`语法)的东西来表示。[例2-5]()显示了同样的查询 - 查找使用这种技术(PostgreSQL,IBM DB2,Oracle和SQL Server支持)中从美国移民到欧洲的人的姓名。但是,与Cypher相比,语法非常笨拙。
|
||||
|
||||
例2-5 与示例2-4相同的查询,使用递归公用表表达式在SQL中表示
|
||||
**例2-5 与示例2-4相同的查询,使用递归CTE表达式在SQL中表示**
|
||||
|
||||
```sql
|
||||
WITH RECURSIVE
|
||||
@ -666,10 +679,10 @@ WITH RECURSIVE
|
||||
JOIN lives_in_europe ON vertices.vertex_id = lives_in_europe.vertex_id;
|
||||
```
|
||||
|
||||
* 集合in_usa以(Location name=United States)作为种子,沿着边表,从种子集作为起点,将所有具有`with_in`边的终点加入种子集,不断递归直到边表内的所有条目都被访问完毕。
|
||||
* 同理建立点集in_europe,获取欧洲的地点列表。
|
||||
* 对于in_usa集合中的每个顶点,按照传入的born_in边缘来查找出生在美国某个地方的人。
|
||||
* 同样,对于in_europe集合中的每个顶点,请按照传入的lives_in边缘来查找居住在欧洲的人。
|
||||
* 集合`in_usa`以(`Location name=United States`)作为种子,沿着边表,从种子集作为起点,将所有具有`with_in`边的终点加入种子集,不断递归直到边表内的所有条目都被访问完毕。
|
||||
* 同理建立点集`in_europe`,获取欧洲的地点列表。
|
||||
* 对于`in_usa`集合中的每个顶点,按照传入的`born_in`边缘来查找出生在美国某个地方的人。
|
||||
* 同样,对于`in_europe`集合中的每个顶点,请按照传入的`lives_in`边来查找居住在欧洲的人。
|
||||
* 最后,把在美国出生的人与在欧洲居住的人相交,获取他们的名称。
|
||||
|
||||
同一个查询,可以用一个查询语言写成4行,而另一个查询需要29行,这说明了不同的数据模型是为不同的应用场景设计的。选择适合应用程序的数据模型非常重要。
|
||||
@ -682,11 +695,12 @@ WITH RECURSIVE
|
||||
|
||||
三元组的主体相当于图中的一个顶点。而客体是两件事情之一:
|
||||
|
||||
1. 原始数据类型中的值,例如字符串或数字。在这种情况下,三元组的谓词和对象相当于主题顶点上的属性的键和值。例如,(lucy,age,33)就像属性{“age”:33}的顶点lucy。
|
||||
2. 图中的另一个顶点。在这种情况下,谓词是图中的边,主体是尾部顶点,而对象是顶点。例如,在(lucy,marriedTo,alain)主语和宾语lucy和alain都是顶点,并且谓词marriedTo是连接他们的边的标签。
|
||||
1. 原始数据类型中的值,例如字符串或数字。在这种情况下,三元组的谓词和对象相当于主题顶点上的属性的键和值。例如,`(lucy, age, 33)`就像属性`{“age”:33}`的顶点lucy。
|
||||
2. 图中的另一个顶点。在这种情况下,谓词是图中的边,主体是尾部顶点,而对象是顶点。例如,在`(lucy, marriedTo, alain)`主语和宾语`lucy`和`alain`都是顶点,并且谓词`marriedTo`是连接他们的边的标签。
|
||||
|
||||
示例2-6显示了与示例2-3相同的数据,以称为Turtle的格式(Notation3(N3)[39])的一个子集形式写成三元组。
|
||||
例2-6。图2-5中的数据子集,表示为Turtle三元组
|
||||
[例2-6]()显示了与[例2-3]()相同的数据,以称为Turtle的格式(Notation3(N3)【39】)的一个子集形式写成三元组。
|
||||
|
||||
**例2-6 图2-5中的数据子集,表示为Turtle三元组**
|
||||
|
||||
```reStructuredText
|
||||
@prefix : <urn:example:>.
|
||||
@ -706,9 +720,11 @@ _:namerica :name "North America"
|
||||
_:namerica :type :"continent"
|
||||
```
|
||||
|
||||
在这个例子中,图的顶点被写为:`_:someName`。这个名字并不意味着这个文件以外的任何东西。它的存在只是帮助我们明确三元组之间的相互引用。当谓词表示边时,该对象是一个顶点,如_:idaho:在_:usa内。当谓词是一个属性时,该对象是一个字符串,如_:usa:name“United States”。
|
||||
一遍又一遍地重复相同的主题是相当重复的,但幸运的是,您可以使用分号来说明关于同一主题的多个事情。这使得Turtle格式相当不错,可读性强:参见例2-7。
|
||||
例2-7。在示例2-6 @prefix:<urn:example:>中写入数据的更简洁的方法。
|
||||
在这个例子中,图的顶点被写为:`_:someName`。这个名字并不意味着这个文件以外的任何东西。它的存在只是帮助我们明确三元组之间的相互引用。当谓词表示边时,该对象是一个顶点,如:`_:idaho :within _:usa.`。当谓词是一个属性时,该对象是一个字符串,如:`_:usa :name "United States"`
|
||||
|
||||
一遍又一遍地重复相同的主题是相当重复的,但幸运的是,您可以使用分号来说明关于同一主题的多个事情。这使得Turtle格式相当不错,可读性强:参见[例2-7]()。
|
||||
|
||||
**例2-7 在示例2-6 `@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数据模型
|
||||
|
||||
例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
|
||||
<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的角度来看,URL <http://my-company.com/namespace> 不一定需要能解析成什么东西,它只是一个命名空间。为避免与`http://URL`混淆,本节中的示例使用不可解析的URI,如urn:example:within。幸运的是,您只需在文件顶部指定一个前缀,然后忘记它。
|
||||
|
||||
从RDF的角度来看,URL `<http://my-company.com/namespace>` 不一定需要能解析成什么东西,它只是一个命名空间。为避免与`http://URL`混淆,本节中的示例使用不可解析的URI,如`urn:example:within`。幸运的是,您只需在文件顶部指定一个前缀,然后忘记它。
|
||||
|
||||
### SPARQL查询语言
|
||||
|
||||
SPARQL是 RDF数据模型三元组存储的查询语言[43]。 (它是SPARQL协议和RDF查询语言的缩写,发音为“sparkle”。)它早于Cypher,并且由于Cypher的模式匹配是从SPARQL中借用的,所以它们看起来非常相似[37]。
|
||||
与从前从美国转移到欧洲的人相同的查询 - 在SPARQL中比在Cypher中更加简洁(请参见示例2-9)。
|
||||
例2-9。与示例2-4相同的查询,用SPARQL表示
|
||||
SPARQL是RDF数据模型三元组存储的查询语言【43】。 (它是SPARQL协议和RDF查询语言的缩写,发音为“sparkle”。)它早于Cypher,并且由于Cypher的模式匹配是从SPARQL中借用的,所以它们看起来非常相似【37】。
|
||||
与从前从美国转移到欧洲的人相同的查询——在SPARQL中比在Cypher中更加简洁(参见[例2-9]())。
|
||||
|
||||
**例2-9 与示例2-4相同的查询,用SPARQL表示**
|
||||
|
||||
```sparql
|
||||
PREFIX : <urn:example:>
|
||||
@ -785,36 +805,39 @@ SELECT ?personName WHERE {
|
||||
?person :bornIn / :within* ?location. # SPARQL
|
||||
```
|
||||
|
||||
由于RDF不区分属性和边,而只是使用两个属性,所以可以使用相同的语法来匹配属性。在下面的表达式中,变量usa被绑定到任何具有值为字符串“United States”的name属性的顶点:
|
||||
由于RDF不区分属性和边,而只是使用两个属性,所以可以使用相同的语法来匹配属性。在下面的表达式中,变量usa被绑定到任何具有值为字符串`"United States"`的`name`属性的顶点:
|
||||
|
||||
```
|
||||
(usa {name:'United States'}) # Cypher
|
||||
?usa :name "United States". # SPARQL
|
||||
```
|
||||
|
||||
SPARQL是一种很好的查询语言 - 即使语义网从来没有出现,它也可以成为应用程序内部使用的强大工具。
|
||||
|
||||
#### 图形数据库与网络模型相比较
|
||||
|
||||
在第36页的“文档数据库是否重复历史?”中,我们讨论了CODASYL和关系模型如何竞争解决IMS中的多对多关系问题。乍一看,CODASYL的网络模型看起来与图模型相似。 CODASYL是否是图形数据库的第二个变种?
|
||||
不,他们在几个重要方面有所不同:
|
||||
|
||||
* 在CODASYL中,数据库有一个模式,指定哪种记录类型可以嵌套在其他记录类型中。在图形数据库中,不存在这样的限制:任何顶点都可以具有到其他任何顶点的边。这为应用程序适应不断变化的需求提供了更大的灵活性。
|
||||
* 在CODASYL中,达到特定记录的唯一方法是遍历其中的一个访问路径。在图形数据库中,可以通过其唯一ID直接引用任何顶点,也可以使用索引来查找具有特定值的顶点。
|
||||
* 在CODASYL,记录的孩子们的有序集合,所以数据库的人不得不维持排序(其中有用于存储布局的后果),并且插入新记录到数据库的应用程序不得不担心的新记录的位置在这些集合中。在图形数据库中,顶点和边不是有序的(您只能在查询时对结果进行排序)。
|
||||
* 在CODASYL中,所有查询都是必要的,难以编写,并且很容易被架构中的变化破坏。在图形数据库中,如果需要,可以在命令式代码中编写遍历,但大多数图形数据库也支持高级声明式查询语言,如Cypher或SPARQL。
|
||||
|
||||
SPARQL是一种很好的查询语言——即使语义网从来没有出现,它也可以成为应用程序内部使用的强大工具。
|
||||
|
||||
> #### 图形数据库与网络模型相比较
|
||||
>
|
||||
> 在“[文档数据库是否在重蹈覆辙?](#文档数据库是否在重蹈覆辙?)”中,我们讨论了CODASYL和关系模型如何竞争解决IMS中的多对多关系问题。乍一看,CODASYL的网络模型看起来与图模型相似。 CODASYL是否是图形数据库的第二个变种?
|
||||
>
|
||||
> 不,他们在几个重要方面有所不同:
|
||||
>
|
||||
> * 在CODASYL中,数据库有一个模式,指定哪种记录类型可以嵌套在其他记录类型中。在图形数据库中,不存在这样的限制:任何顶点都可以具有到其他任何顶点的边。这为应用程序适应不断变化的需求提供了更大的灵活性。
|
||||
> * 在CODASYL中,达到特定记录的唯一方法是遍历其中的一个访问路径。在图形数据库中,可以通过其唯一ID直接引用任何顶点,也可以使用索引来查找具有特定值的顶点。
|
||||
> * 在CODASYL,记录的孩子们的有序集合,所以数据库的人不得不维持排序(其中有用于存储布局的后果),并且插入新记录到数据库的应用程序不得不担心的新记录的位置在这些集合中。在图形数据库中,顶点和边不是有序的(您只能在查询时对结果进行排序)。
|
||||
> * 在CODASYL中,所有查询都是必要的,难以编写,并且很容易被架构中的变化破坏。在图形数据库中,如果需要,可以在命令式代码中编写遍历,但大多数图形数据库也支持高级声明式查询语言,如Cypher或SPARQL。
|
||||
>
|
||||
>
|
||||
|
||||
### 基础: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
|
||||
name(namerica, 'North America').
|
||||
@ -832,9 +855,9 @@ name(lucy, 'Lucy').
|
||||
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 */
|
||||
@ -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。例如,name(Location,Name)与可变绑定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_recursive(namerica,'北美')。
|
||||
2. 数据库中存在(usa,namerica),并在之前的步骤中生成(namerica,'North America'),所以适用规则2。它会产生within_recursive(美国,“北美”)。
|
||||
3. 在(爱达荷州,美国)存在于数据库和上一步生成within_recursive(美国,“北美”),所以规则2适用。它产生within_recursive(爱达荷州,“北美”)。
|
||||
1. 名称`(namerica, '北美')`存在于数据库中,故规则1适用。它生成`within_recursive(namerica, '北美')`。
|
||||
2. 数据库中存在`(usa, namerica)`,并在之前的步骤中生成`(namerica, 'North America')`,所以适用规则2。它会产生`within_recursive(美国, “北美”)`。
|
||||
3. 在`(爱达荷州, 美国)`存在于数据库和上一步生成`within_recursive(美国, “北美”)`,所以规则2适用。它产生`within_recursive(爱达荷州, "北美")`。
|
||||
|
||||
通过重复应用规则1和2,within_recursive谓词可以告诉我们在我们的数据库中包含的北美(或任何其他位置名称)的所有位置。这个过程如图2-6所示。
|
||||
通过重复应用规则1和2,`within_recursive`谓词可以告诉我们在我们的数据库中包含的北美(或任何其他位置名称)的所有位置。这个过程如[图2-6](img/fig2-6.png)所示。
|
||||
|
||||
![](img/fig2-6.png)
|
||||
|
||||
**图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方法需要对本章讨论的其他查询语言采取不同的思维方式,但这是一种非常强大的方法,因为规则可以在不同的查询中进行组合和重用。简单的一次性查询不太方便,但是如果数据很复杂,它可以更好地处理。
|
||||
|
||||
@ -889,12 +912,12 @@ Datalog方法需要对本章讨论的其他查询语言采取不同的思维方
|
||||
|
||||
文档数据库和图数据库有一个共同点,那就是它们通常不会为存储的数据强制实施一个模式,这可以使应用程序更容易适应不断变化的需求。但是应用程序很可能仍假定数据具有一定的结构:这只是模式是明确的(强制写入)还是隐含的(在读取时处理)的问题。
|
||||
|
||||
每个数据模型都带有自己的查询语言或框架,我们讨论了几个例子:SQL,MapReduce,MongoDB的聚合管道,Cypher,SPARQL和Datalog。我们也谈到了CSS和XSL / XPath,它们不是数据库查询语言,而包含有趣的相似之处。
|
||||
每个数据模型都带有自己的查询语言或框架,我们讨论了几个例子:SQL,MapReduce,MongoDB的聚合管道,Cypher,SPARQL和Datalog。我们也谈到了CSS和XSL/XPath,它们不是数据库查询语言,而包含有趣的相似之处。
|
||||
|
||||
虽然我们已经覆盖了很多地方,但仍然有许多数据模型没有提到。举几个简单的例子:
|
||||
|
||||
* 研究人员使用基因组数据通常需要执行序列相似性搜索,这意味着需要一个很长的字符串(代表一个DNA分子),并将其与一个类似但不完全相同的大型字符串数据库进行匹配。这里所描述的数据库都不能处理这种用法,这就是为什么研究人员编写了像GenBank这样的专门的基因组数据库软件的原因[48]。
|
||||
* 粒子物理学家数十年来一直在进行大数据类型的大规模数据分析,像大型强子对撞机(LHC)这样的项目现在可以工作在数百亿兆字节的范围内!在这样的规模下,需要定制解决方案来阻止硬件成本从失控中解脱出来[49]。
|
||||
* 研究人员使用基因组数据通常需要执行序列相似性搜索,这意味着需要一个很长的字符串(代表一个DNA分子),并将其与一个类似但不完全相同的大型字符串数据库进行匹配。这里所描述的数据库都不能处理这种用法,这就是为什么研究人员编写了像GenBank这样的专门的基因组数据库软件的原因【48】。
|
||||
* 粒子物理学家数十年来一直在进行大数据类型的大规模数据分析,像大型强子对撞机(LHC)这样的项目现在可以工作在数百亿兆字节的范围内!在这样的规模下,需要定制解决方案来阻止硬件成本从失控中解脱出来【49】。
|
||||
* 全文搜索可以说是一种经常与数据库一起使用的数据模型。信息检索是一个大的专业课题,在本书中我们不会详细介绍,但是我们将在第三章和第三章中介绍搜索指标。
|
||||
|
||||
在下一章中,我们将讨论在实现本章描述的数据模型时会发挥的一些权衡。
|
||||
|
333
ddia/ch4.md
333
ddia/ch4.md
@ -11,74 +11,77 @@
|
||||
|
||||
[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*),一次将新版本部署到少数几个节点,检查新版本是否运行正常,然后逐渐部完所有的节点。这样无需中断服务即可部署新版本,为频繁发布提供了可行性,从而带来更好的可演化性。
|
||||
* 对于客户端(client-side)应用程序,升不升级就要看用户的心情了。用户可能相当长一段时间里都不会去升级软件。
|
||||
* 对于**服务端(server-side)**应用程序,可能需要执行**滚动升级 (rolling upgrade)**(也称为**阶段发布(staged rollout)**),一次将新版本部署到少数几个节点,检查新版本是否运行正常,然后逐渐部完所有的节点。这样无需中断服务即可部署新版本,为频繁发布提供了可行性,从而带来更好的可演化性。
|
||||
* 对于**客户端(client-side)**应用程序,升不升级就要看用户的心情了。用户可能相当长一段时间里都不会去升级软件。
|
||||
|
||||
这意味着,新旧版本的代码,以及新旧数据格式可能会在系统中同时共处。系统想要继续顺利运行,就需要保持*双向兼容性*:
|
||||
这意味着,新旧版本的代码,以及新旧数据格式可能会在系统中同时共处。系统想要继续顺利运行,就需要保持**双向兼容性**:
|
||||
|
||||
* **向后兼容** (backward compatibility)
|
||||
***向后兼容 (backward compatibility)***
|
||||
|
||||
新代码可以读旧数据。
|
||||
新代码可以读旧数据。
|
||||
|
||||
* **向前兼容** (forward compatibility)
|
||||
***向前兼容 (forward compatibility)***
|
||||
|
||||
旧代码可以读新数据。
|
||||
旧代码可以读新数据。
|
||||
|
||||
向后兼容性通常并不难实现:新代码的作者当然知道由旧代码使用的数据格式,因此可以显示地处理它(最简单的办法是,保留旧代码即可读取旧数据)。
|
||||
|
||||
向前兼容性可能会更棘手,因为旧版的程序需要忽略新版数据格式中新增的部分。
|
||||
|
||||
本章中将介绍几种编码数据的格式,包括 JSON,XML,Protocol Buffers,Thrift和Avro。尤其将关注这些格式如何应对模式变化,以及它们如何对新旧代码数据需要共存的系统提供支持。然后将讨论如何使用这些格式进行数据存储和通信:在Web服务中,具象状态传输(REST)和远程过程调用(RPC),以及消息传递系统(如Actor和消息队列)。
|
||||
本章中将介绍几种编码数据的格式,包括 JSON,XML,Protocol Buffers,Thrift和Avro。尤其将关注这些格式如何应对模式变化,以及它们如何对新旧代码数据需要共存的系统提供支持。然后将讨论如何使用这些格式进行数据存储和通信:在Web服务中,**具象状态传输(REST)**和**远程过程调用(RPC)**,以及**消息传递系统**(如Actor和消息队列)。
|
||||
|
||||
## 编码数据的格式
|
||||
|
||||
程序通常(至少)使用两种形式的数据:
|
||||
|
||||
1. 在内存中,数据保存在对象,结构体,列表,数组,哈希表,树等中。 这些数据结构针对CPU的高效访问和操作进行了优化(通常使用指针)。
|
||||
2. 如果要将数据写入文件,或通过网络发送,则必须将其*编码(encode)*为某种自包含的字节序列(例如,JSON文档)。 由于每个进程都有自己独立的地址空间,一个进程中的指针对任何其他进程都没有意义,所以这个字节序列表示会与通常在内存中使用的数据结构完全不同。
|
||||
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)表达此含义。
|
||||
>
|
||||
> 译者注:Marshal与Serialization的区别:Marshal不仅传输对象的状态,而且会一起传输对象的方法(相关代码)。
|
||||
> 不幸的是,在[第七章](ch7.md):**事务(Transaction)**的上下文里,**序列化(Serialization)**这个术语也出现了,而且具有完全不同的含义。尽管序列化可能是更常见的术语,为了避免术语重载,本书中坚持使用**编码(Encoding)**表达此含义。
|
||||
|
||||
这是一个常见的问题,因而有许多库和编码格式可供选择。 首先让我们概览一下。
|
||||
|
||||
### 语言特定的格式
|
||||
|
||||
许多编程语言都内建了将内存对象编码为字节序列的支持。例如,Java有`java.io.Serializable [1]`,Ruby有`Marshal [2]`,Python有`pickle [3]`等等。许多第三方库也存在,例如`Kryo for Java [4]`。
|
||||
许多编程语言都内建了将内存对象编码为字节序列的支持。例如,Java有`java.io.Serializable` 【1】,Ruby有`Marshal`【2】,Python有`pickle`【3】等等。许多第三方库也存在,例如`Kryo for Java` 【4】。
|
||||
|
||||
这些编码库非常方便,可以用很少的额外代码实现内存对象的保存与恢复。但是它们也有一些深层次的问题:
|
||||
|
||||
* 这类编码通常与特定的编程语言深度绑定,其他语言很难读取这种数据。如果以这类编码存储或传输数据,那你就和这门语言绑死在一起了。并且很难将系统与其他组织的系统(可能用的是不同的语言)进行集成。
|
||||
* 为了恢复相同对象类型的数据,解码过程需要**实例化任意类**的能力,这通常是安全问题的一个来源[5]:如果攻击者可以让应用程序解码任意的字节序列,他们就能实例化任意的类,这会允许他们做可怕的事情,如远程执行任意代码[6 ,7]。
|
||||
* 为了恢复相同对象类型的数据,解码过程需要**实例化任意类**的能力,这通常是安全问题的一个来源【5】:如果攻击者可以让应用程序解码任意的字节序列,他们就能实例化任意的类,这会允许他们做可怕的事情,如远程执行任意代码【6,7】。
|
||||
* 在这些库中,数据版本控制通常是事后才考虑的。因为它们旨在快速简便地对数据进行编码,所以往往忽略了前向后向兼容性带来的麻烦问题。
|
||||
* 效率(编码或解码所花费的CPU时间,以及编码结构的大小)往往也是事后才考虑的。 例如,Java的内置序列化由于其糟糕的性能和臃肿的编码而臭名昭着[8]。
|
||||
* 效率(编码或解码所花费的CPU时间,以及编码结构的大小)往往也是事后才考虑的。 例如,Java的内置序列化由于其糟糕的性能和臃肿的编码而臭名昭着【8】。
|
||||
|
||||
因此,除非临时使用,采用语言内置编码通常是一个坏主意。
|
||||
|
||||
### JSON,XML和二进制变体
|
||||
|
||||
谈到可以被许多编程语言编写和读取的标准化编码,JSON和XML是显眼的竞争者。它们广为人知,广受支持,也“广受憎恶”。 XML经常被批评为过于冗长和不必要的复杂[9]。 JSON倍受欢迎,主要由于它在Web浏览器中的内置支持(通过成为JavaScript的一个子集)以及相对于XML的简单性。 CSV是另一种流行的与语言无关的格式,尽管功能较弱。
|
||||
谈到可以被许多编程语言编写和读取的标准化编码,JSON和XML是显眼的竞争者。它们广为人知,广受支持,也“广受憎恶”。 XML经常被批评为过于冗长和不必要的复杂【9】。 JSON倍受欢迎,主要由于它在Web浏览器中的内置支持(通过成为JavaScript的一个子集)以及相对于XML的简单性。 CSV是另一种流行的与语言无关的格式,尽管功能较弱。
|
||||
|
||||
JSON,XML和CSV是文本格式,因此具有人类可读性(尽管语法是一个热门辩题)。除了表面的语法问题之外,它们也有一些微妙的问题:
|
||||
|
||||
* 数字的编码多有歧义之处。XML和CSV不能区分数字和字符串(除非引用外部模式)。 JSON虽然区分字符串和数字,但不区分整数和浮点数,而且不能指定精度。
|
||||
* 当处理大量数据时,这个问题更严重了。例如,大于$2^{53}$的整数不能在IEEE 754双精度浮点数中精确表示,因此在使用浮点数(例如JavaScript)的语言进行分析时,这些数字会变得不准确。 Twitter上有一个大于$2^{53}$的数字的例子,它使用一个64位的数字来标识每条推文。 Twitter API返回的JSON包含了两次推特ID,一次是JSON数字,一次是十进制字符串,以此避免JavaScript程序无法正确解析数字的问题[10]。
|
||||
* JSON和XML对Unicode字符串(即人类可读的文本)有很好的支持,但是它们不支持二进制数据(不带字符编码(character encoding)的字节序列)。二进制串是很实用的功能,所以人们通过使用Base64将二进制数据编码为文本来绕开这个限制。模式然后用于表示该值应该被解释为Base64编码。这个工作,但它有点hacky,并增加了33%的数据大小。 XML [11]和JSON [12]都有可选的模式支持。这些模式语言相当强大,所以学习和实现起来相当复杂。 XML模式的使用相当普遍,但许多基于JSON的工具嫌麻烦才不会使用模式。由于数据的正确解释(例如数字和二进制字符串)取决于模式中的信息,因此不使用XML / JSON模式的应用程序可能需要对相应的编码/解码逻辑进行硬编码。
|
||||
* CSV没有任何模式,因此应用程序需要定义每行和每列的含义。如果应用程序更改添加新的行或列,则必须手动处理该变更。 CSV也是一个相当模糊的格式(如果一个值包含逗号或换行符,会发生什么?)。尽管其转义规则已经被正式指定[13],但并不是所有的解析器都正确的实现了标准。
|
||||
* 当处理大量数据时,这个问题更严重了。例如,大于$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模式的应用程序可能需要对相应的编码/解码逻辑进行硬编码。
|
||||
* CSV没有任何模式,因此应用程序需要定义每行和每列的含义。如果应用程序更改添加新的行或列,则必须手动处理该变更。 CSV也是一个相当模糊的格式(如果一个值包含逗号或换行符,会发生什么?)。尽管其转义规则已经被正式指定【13】,但并不是所有的解析器都正确的实现了标准。
|
||||
|
||||
尽管存在这些缺陷,但JSON,XML和CSV已经足够用于很多目的。特别是作为数据交换格式(即将数据从一个组织发送到另一个组织),它们很可能仍然很受欢迎。这种情况下,只要人们对格式是什么意见一致,格式多么美观或者高效就没有关系。让不同的组织达成一致的难度超过了其他大多数问题。
|
||||
尽管存在这些缺陷,但JSON,XML和CSV已经足够用于很多目的。特别是作为数据交换格式(即将数据从一个组织发送到另一个组织),它们很可能仍然很受欢迎。这种情况下,只要人们对格式是什么意见一致,格式多么美观或者高效就没有关系。**让不同的组织达成一致的难度超过了其他大多数问题。**
|
||||
|
||||
#### 二进制编码
|
||||
|
||||
@ -86,7 +89,7 @@ JSON,XML和CSV是文本格式,因此具有人类可读性(尽管语法是
|
||||
|
||||
JSON比XML简洁,但与二进制格式一比,还是太占地方。这一事实导致大量二进制编码版本JSON & XML的出现,JSON(MessagePack,BSON,BJSON,UBJSON,BISON和Smile等)(例如WBXML和Fast Infoset)。这些格式已经被各种各样的领域所采用,但是没有一个像JSON和XML的文本版本那样被广泛采用。
|
||||
|
||||
这些格式中的一些扩展了一组数据类型(例如,区分整数和浮点数,或者增加对二进制字符串的支持),另一方面,它们没有盖面JSON / XML的数据模型。特别是由于它们没有规定模式,所以它们需要在编码数据中包含所有的对象字段名称。也就是说,在**例4-1**中的JSON文档的二进制编码中,需要在某处包含字符串`userName`,`favoriteNumber`和`interest`。
|
||||
这些格式中的一些扩展了一组数据类型(例如,区分整数和浮点数,或者增加对二进制字符串的支持),另一方面,它们没有盖面JSON / XML的数据模型。特别是由于它们没有规定模式,所以它们需要在编码数据中包含所有的对象字段名称。也就是说,在[例4-1]()中的JSON文档的二进制编码中,需要在某处包含字符串`userName`,`favoriteNumber`和`interest`。
|
||||
|
||||
**例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)。
|
||||
3. 接下来八个字节是ASCII字符串形式的字段名称`userName`。由于之前已经指明长度,不需要任何标记来标识字符串的结束位置(或者任何转义)。
|
||||
4. 接下来的七个字节对前缀为`0xa6`的六个字母的字符串值`Martin`进行编码,依此类推。
|
||||
@ -114,12 +117,10 @@ JSON比XML简洁,但与二进制格式一比,还是太占地方。这一事
|
||||
|
||||
**图4-1 使用MessagePack编码的记录(例4-1)**
|
||||
|
||||
|
||||
|
||||
### Thrift与Protocol Buffers
|
||||
|
||||
Apache Thrift [15]和Protocol Buffers(protobuf)[16]是基于相同原理的二进制编码库。 Protocol Buffers最初是在Google开发的,Thrift最初是在Facebook开发的,并且在2007 - 2008年都是开源的[17]。
|
||||
Thrift和Protocol Buffers都需要一个模式来编码任何数据。要在Thrift的例4-1中对数据进行编码,可以使用Thrift接口定义语言(IDL)来描述模式,如下所示:
|
||||
Apache Thrift 【15】和Protocol Buffers(protobuf)【16】是基于相同原理的二进制编码库。 Protocol Buffers最初是在Google开发的,Thrift最初是在Facebook开发的,并且在2007~2008年都是开源的【17】。
|
||||
Thrift和Protocol Buffers都需要一个模式来编码任何数据。要在Thrift的[例4-1]()中对数据进行编码,可以使用Thrift**接口定义语言(IDL)**来描述模式,如下所示:
|
||||
|
||||
```c
|
||||
struct Person {
|
||||
@ -139,28 +140,26 @@ message Person {
|
||||
}
|
||||
```
|
||||
|
||||
Thrift和Protocol Buffers每一个都带有一个代码生成工具,它采用了类似于这里所示的模式定义,并且生成了以各种编程语言实现模式的类[18]。您的应用程序代码可以调用此生成的代码来对模式的记录进行编码或解码。
|
||||
用这个模式编码的数据是什么样的?令人困惑的是,Thrift有两种不同的二进制编码格式[^iii],分别称为BinaryProtocol和CompactProtocol。先来看看BinaryProtocol。这种格式的编码示例4-1需要59个字节,如图4-2 [19]所示。
|
||||
|
||||
( )
|
||||
Thrift和Protocol Buffers每一个都带有一个代码生成工具,它采用了类似于这里所示的模式定义,并且生成了以各种编程语言实现模式的类【18】。您的应用程序代码可以调用此生成的代码来对模式的记录进行编码或解码。
|
||||
用这个模式编码的数据是什么样的?令人困惑的是,Thrift有两种不同的二进制编码格式[^iii],分别称为BinaryProtocol和CompactProtocol。先来看看BinaryProtocol。使用这种格式的编码来编码[例4-1]()中的消息只需要59个字节,如[图4-2](img/fig4-2.png)所示【19】。
|
||||
|
||||
![](img/fig4-2.png)
|
||||
|
||||
**图4-2 使用Thrift二进制协议编码的记录**
|
||||
|
||||
[^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相比,最大的区别是没有字段名(userName,favoriteNumber,interest)。相反,编码数据包含字段标签,它们是数字(1,2和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)
|
||||
|
||||
**图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)
|
||||
|
||||
@ -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所做的从单值到多值的相同演变,但是它具有支持嵌套列表的优点。
|
||||
|
||||
### 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),更易于机器读取。
|
||||
|
||||
@ -218,7 +217,7 @@ record Person {
|
||||
}
|
||||
```
|
||||
|
||||
首先,请注意架构中没有标签号码。 如果我们使用这个模式编码我们的例子记录(例4-1),Avro二进制编码只有32个字节长,这是我们所见过的所有编码中最紧凑的。 编码字节序列的分解如图4-5所示。
|
||||
首先,请注意架构中没有标签号码。 如果我们使用这个模式编码我们的例子记录([例4-1]()),Avro二进制编码只有32个字节长,这是我们所见过的所有编码中最紧凑的。 编码字节序列的分解如[图4-5](img/fig4-5.png)所示。
|
||||
|
||||
如果您检查字节序列,您可以看到没有什么可以识别字段或其数据类型。 编码只是由连在一起的值组成。 一个字符串只是一个长度前缀,后跟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解决读写模式的差异**
|
||||
|
||||
|
||||
|
||||
#### 模式演变规则
|
||||
|
||||
使用Avro,向前兼容性意味着您可以将新版本的架构作为编写器,并将旧版本的架构作为读者。相反,向后兼容意味着你可以有一个作为读者的新版本的模式和作为作者的旧版本。
|
||||
|
||||
为了保持兼容性,您只能添加或删除具有默认值的字段。 (我们的Avro模式中的字段favourNumber的默认值为null)。例如,假设您添加一个默认值的字段,所以这个新的字段存在于新的模式中,而不是旧的。当使用新模式的阅读器读取使用旧模式写入的记录时,将为缺少的字段填充默认值。
|
||||
为了保持兼容性,您只能添加或删除具有默认值的字段。 (我们的Avro模式中的字段`favourNumber`的默认值为`null`)。例如,假设您添加一个默认值的字段,所以这个新的字段存在于新的模式中,而不是旧的。当使用新模式的阅读器读取使用旧模式写入的记录时,将为缺少的字段填充默认值。
|
||||
|
||||
如果你要添加一个没有默认值的字段,新的阅读器将无法读取旧作者写的数据,所以你会破坏向后兼容性。如果您要删除没有默认值的字段,旧的阅读器将无法读取新作者写入的数据,因此您会打破兼容性。在一些编程语言中,null是任何变量可以接受的默认值,但在Avro中并不是这样:如果要允许一个字段为null,则必须使用联合类型。例如,`union {null,long,string}`字段;表示该字段可以是数字或字符串,也可以是`null`。如果它是union的分支之一,那么只能使用null作为默认值.iv这比默认情况下可以为null是更加冗长的,但是通过明确什么可以和不可以是什么,有助于防止错误null [22]。
|
||||
如果你要添加一个没有默认值的字段,新的阅读器将无法读取旧作者写的数据,所以你会破坏向后兼容性。如果您要删除没有默认值的字段,旧的阅读器将无法读取新作者写入的数据,因此您会打破兼容性。在一些编程语言中,null是任何变量可以接受的默认值,但在Avro中并不是这样:如果要允许一个字段为`null`,则必须使用联合类型。例如,`union {null,long,string}`字段;表示该字段可以是数字或字符串,也可以是`null`。如果它是union的分支之一,那么只能使用null作为默认值[^iv]。这比默认情况下可以为`null`是更加冗长的,但是通过明确什么可以和不可以是什么,有助于防止错误的`null` 【22】。
|
||||
|
||||
[^iv]: 确切地说,默认值必须是联合的第一个分支的类型,尽管这是Avro的特定限制,而不是联合类型的一般特征。
|
||||
|
||||
因此,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方法的一个优点是架构不包含任何标签号码。但为什么这很重要?在模式中保留一些数字有什么问题?
|
||||
|
||||
不同之处在于Avro对动态生成的模式更友善。例如,假如你有一个关系数据库,你想要把它的内容转储到一个文件中,并且你想使用二进制格式来避免前面提到的文本格式(JSON,CSV,SQL)的问题。如果你使用Avro,你可以很容易地从关系模式生成一个Avro模式(在我们之前看到的JSON表示中),并使用该模式对数据库内容进行编码,并将其全部转储到Avro对象容器文件[25]中。您为每个数据库表生成一个记录模式,每个列成为该记录中的一个字段。数据库中的列名称映射到Avro中的字段名称。
|
||||
不同之处在于Avro对动态生成的模式更友善。例如,假如你有一个关系数据库,你想要把它的内容转储到一个文件中,并且你想使用二进制格式来避免前面提到的文本格式(JSON,CSV,SQL)的问题。如果你使用Avro,你可以很容易地从关系模式生成一个Avro模式(在我们之前看到的JSON表示中),并使用该模式对数据库内容进行编码,并将其全部转储到Avro对象容器文件【25】中。您为每个数据库表生成一个记录模式,每个列成为该记录中的一个字段。数据库中的列名称映射到Avro中的字段名称。
|
||||
|
||||
现在,如果数据库模式发生变化(例如,一个表中添加了一列,删除了一列),则可以从更新的数据库模式生成新的Avro模式,并在新的Avro模式中导出数据。数据导出过程不需要注意模式的改变 - 每次运行时都可以简单地进行模式转换。任何读取新数据文件的人都会看到记录的字段已经改变,但是由于字段是通过名字来标识的,所以更新的作者的模式仍然可以与旧的读者模式匹配。
|
||||
|
||||
@ -295,13 +294,13 @@ Thrift和Protobuf依赖于代码生成:在定义了模式之后,可以使用
|
||||
|
||||
Avro为静态类型编程语言提供了可选的代码生成功能,但是它也可以在不生成任何代码的情况下使用。如果你有一个对象容器文件(它嵌入了作者的模式),你可以简单地使用Avro库打开它,并以与查看JSON文件相同的方式查看数据。该文件是自描述的,因为它包含所有必要的元数据。
|
||||
|
||||
这个属性特别适用于动态类型的数据处理语言如Apache Pig [26]。在Pig中,您可以打开一些Avro文件,开始分析它们,并编写派生数据集以Avro格式输出文件,而无需考虑模式。
|
||||
这个属性特别适用于动态类型的数据处理语言如Apache Pig 【26】。在Pig中,您可以打开一些Avro文件,开始分析它们,并编写派生数据集以Avro格式输出文件,而无需考虑模式。
|
||||
|
||||
### 模式的优点
|
||||
|
||||
正如我们所看到的,Protocol Buffers,Thrift和Avro都使用模式来描述二进制编码格式。他们的模式语言比XML模式或者JSON模式简单得多,它支持更详细的验证规则(例如,“该字段的字符串值必须与该正则表达式匹配”或“该字段的整数值必须在0和100之间“)。由于Protocol Buffers,Thrift和Avro实现起来更简单,使用起来也更简单,所以它们已经发展到支持相当广泛的编程语言。
|
||||
|
||||
这些编码所基于的想法绝不是新的。例如,它们与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)。
|
||||
|
||||
@ -323,9 +322,9 @@ Avro为静态类型编程语言提供了可选的代码生成功能,但是它
|
||||
|
||||
这是一个相当抽象的概念 - 数据可以通过多种方式从一个流程流向另一个流程。谁编码数据,谁解码?在本章的其余部分中,我们将探讨数据如何在流程之间流动的一些最常见的方式:
|
||||
|
||||
* 通过数据库(请参阅第129页的“通过数据库的数据流”
|
||||
* 通过服务调用(请参阅第131页的“通过服务传输数据流:REST和RPC”
|
||||
* 通过异步消息传递(请参阅第136页的“消息传递数据流”
|
||||
* 通过数据库(参阅“[通过数据库的数据流](#通过数据库的数据流)”)
|
||||
* 通过服务调用(参阅“[通过服务传输数据流:REST和RPC](#通过服务传输数据流:REST和RPC)”)
|
||||
* 通过异步消息传递(参阅“[消息传递数据流](#消息传递数据流)”)
|
||||
|
||||
|
||||
|
||||
@ -345,8 +344,6 @@ Avro为静态类型编程语言提供了可选的代码生成功能,但是它
|
||||
|
||||
解决这个问题不是一个难题,你只需要意识到它。
|
||||
|
||||
|
||||
|
||||
![](img/fig4-7.png)
|
||||
|
||||
**图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被称为服务。
|
||||
|
||||
Web以这种方式工作:客户(Web浏览器)向Web服务器发出请求,使GET请求下载HTML,CSS,JavaScript,图像等,并向POST请求提交数据到服务器。 API包含一组标准的协议和数据格式(HTTP,URL,SSL / TLS,HTML等)。由于网络浏览器,网络服务器和网站作者大多同意这些标准,您可以使用任何网络浏览器访问任何网站(至少在理论上!)。
|
||||
Web以这种方式工作:客户(Web浏览器)向Web服务器发出请求,使GET请求下载HTML,CSS,JavaScript,图像等,并向POST请求提交数据到服务器。 API包含一组标准的协议和数据格式(HTTP,URL,SSL/TLS,HTML等)。由于网络浏览器,网络服务器和网站作者大多同意这些标准,您可以使用任何网络浏览器访问任何网站(至少在理论上!)。
|
||||
|
||||
Web浏览器不是唯一的客户端类型。例如,在移动设备或桌面计算机上运行的本地应用程序也可以向服务器发出网络请求,并且在Web浏览器内运行的客户端JavaScript应用程序可以使用XMLHttpRequest成为HTTP客户端(该技术被称为Ajax [30])。在这种情况下,服务器的响应通常不是用于显示给人的HTML,而是用于便于客户端应用程序代码(如JSON)进一步处理的编码数据。尽管HTTP可能被用作传输协议,但顶层实现的API是特定于应用程序的,客户端和服务器需要就该API的细节达成一致。
|
||||
Web浏览器不是唯一的客户端类型。例如,在移动设备或桌面计算机上运行的本地应用程序也可以向服务器发出网络请求,并且在Web浏览器内运行的客户端JavaScript应用程序可以使用XMLHttpRequest成为HTTP客户端(该技术被称为Ajax 【30】)。在这种情况下,服务器的响应通常不是用于显示给人的HTML,而是用于便于客户端应用程序代码(如JSON)进一步处理的编码数据。尽管HTTP可能被用作传输协议,但顶层实现的API是特定于应用程序的,客户端和服务器需要就该API的细节达成一致。
|
||||
|
||||
此外,服务器本身可以是另一个服务的客户端(例如,典型的Web应用服务器充当数据库的客户端)。这种方法通常用于将大型应用程序按照功能区域分解为较小的服务,这样当一个服务需要来自另一个服务的某些功能或数据时,就会向另一个服务发出请求。这种构建应用程序的方式传统上被称为面向服务的体系结构(service-oriented architecture,SOA),最近被改进和更名为微服务体系结构[31,32]。
|
||||
此外,服务器本身可以是另一个服务的客户端(例如,典型的Web应用服务器充当数据库的客户端)。这种方法通常用于将大型应用程序按照功能区域分解为较小的服务,这样当一个服务需要来自另一个服务的某些功能或数据时,就会向另一个服务发出请求。这种构建应用程序的方式传统上被称为**面向服务的体系结构(service-oriented architecture,SOA)**,最近被改进和更名为**微服务架构 **【31,32】。
|
||||
|
||||
在某些方面,服务类似于数据库:它们通常允许客户端提交和查询数据。但是,虽然数据库允许使用我们在第2章 中讨论的查询语言进行任意查询,但是服务公开了一个特定于应用程序的API,它只允许由服务的业务逻辑(应用程序代码)预定的输入和输出[33 ]。这种限制提供了一定程度的封装:服务可以对客户可以做什么和不可以做什么施加细粒度的限制。
|
||||
在某些方面,服务类似于数据库:它们通常允许客户端提交和查询数据。但是,虽然数据库允许使用我们在第2章 中讨论的查询语言进行任意查询,但是服务公开了一个特定于应用程序的API,它只允许由服务的业务逻辑(应用程序代码)预定的输入和输出【33】。这种限制提供了一定程度的封装:服务可以对客户可以做什么和不可以做什么施加细粒度的限制。
|
||||
|
||||
面向服务/微服务架构的一个关键设计目标是通过使服务独立部署和演化来使应用程序更易于更改和维护。例如,每个服务应该由一个团队拥有,并且该团队应该能够经常发布新版本的服务,而不必与其他团队协调。换句话说,我们应该期望服务器和客户端的旧版本和新版本同时运行,因此服务器和客户端使用的数据编码必须在不同版本的服务API之间兼容 - 正是我们所做的本章一直在谈论。
|
||||
面向服务/微服务架构的一个关键设计目标是通过使服务独立部署和演化来使应用程序更易于更改和维护。例如,每个服务应该由一个团队拥有,并且该团队应该能够经常发布新版本的服务,而不必与其他团队协调。换句话说,我们应该期望服务器和客户端的旧版本和新版本同时运行,因此服务器和客户端使用的数据编码必须在不同版本的服务API之间兼容——正是我们所做的本章一直在谈论。
|
||||
|
||||
#### 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服务的集成是困难的。
|
||||
|
||||
尽管SOAP及其各种扩展表面上是标准化的,但是不同厂商的实现之间的互操作性往往会造成问题[39]。由于所有这些原因,尽管许多大型企业仍然使用SOAP,但在大多数小公司中已经不再受到青睐。
|
||||
|
||||
REST风格的API倾向于更简单的方法,通常涉及较少的代码生成和自动化工具。定义格式(如OpenAPI,也称为Swagger [40])可用于描述RESTful API并生成文档。
|
||||
REST风格的API倾向于更简单的方法,通常涉及较少的代码生成和自动化工具。定义格式(如OpenAPI,也称为Swagger 【40】)可用于描述RESTful API并生成文档。
|
||||
|
||||
#### 远程过程调用(RPC)的问题
|
||||
|
||||
Web服务仅仅是通过网络进行API请求的一系列技术的最新版本,其中许多技术受到了大量的炒作,但是存在严重的问题。 Enterprise JavaBeans(EJB)和Java的远程方法调用(RMI)仅限于Java。分布式组件对象模型(DCOM)仅限于Microsoft平台。公共对象请求代理体系结构(CORBA)过于复杂,不提供前向或后向兼容性[41]。
|
||||
Web服务仅仅是通过网络进行API请求的一系列技术的最新版本,其中许多技术受到了大量的炒作,但是存在严重的问题。 Enterprise JavaBeans(EJB)和Java的**远程方法调用(RMI)**仅限于Java。**分布式组件对象模型(DCOM)**仅限于Microsoft平台。**公共对象请求代理体系结构(CORBA)**过于复杂,不提供前向或后向兼容性[41]。
|
||||
|
||||
所有这些都是基于远程过程调用(RPC)的思想,该过程调用自20世纪70年代以来一直存在[42]。 RPC模型试图向远程网络服务发出请求,看起来与在同一进程中调用编程语言中的函数或方法相同(这种抽象称为位置透明)。尽管RPC起初看起来很方便,但这种方法根本上是有缺陷的[43,44]。网络请求与本地函数调用非常不同:
|
||||
所有这些都是基于**远程过程调用(RPC)**的思想,该过程调用自20世纪70年代以来一直存在【42】。 RPC模型试图向远程网络服务发出请求,看起来与在同一进程中调用编程语言中的函数或方法相同(这种抽象称为位置透明)。尽管RPC起初看起来很方便,但这种方法根本上是有缺陷的【43,44】。网络请求与本地函数调用非常不同:
|
||||
|
||||
* 本地函数调用是可预测的,并且成功或失败,这仅取决于受您控制的参数。网络请求是不可预知的:由于网络问题,请求或响应可能会丢失,或者远程计算机可能很慢或不可用,这些问题完全不在您的控制范围之内。网络问题是常见的,所以你必须预测他们,例如通过重试失败的请求。
|
||||
* 本地函数调用要么返回结果,要么抛出异常,或者永远不返回(因为进入无限循环或进程崩溃)。网络请求有另一个可能的结果:由于超时,它可能会返回没有结果。在这种情况下,你根本不知道发生了什么:如果你没有得到来自远程服务的响应,你无法知道请求是否通过。 (我们将在第8章更详细地讨论这个问题。)
|
||||
* 如果您重试失败的网络请求,可能会发生请求实际上正在通过,只有响应丢失。在这种情况下,重试将导致该操作被执行多次,除非您在协议中引入除重(*幂等 idempotence*)机制。本地函数调用没有这个问题。 (我们在第十一章更详细地讨论幂等性)
|
||||
* 本地函数调用要么返回结果,要么抛出异常,或者永远不返回(因为进入无限循环或进程崩溃)。网络请求有另一个可能的结果:由于超时,它可能会返回没有结果。在这种情况下,你根本不知道发生了什么:如果你没有得到来自远程服务的响应,你无法知道请求是否通过。 (我们将在[第8章](ch8.md)更详细地讨论这个问题。)
|
||||
* 如果您重试失败的网络请求,可能会发生请求实际上正在通过,只有响应丢失。在这种情况下,重试将导致该操作被执行多次,除非您在协议中引入除重(**幂等(idempotence)**)机制。本地函数调用没有这个问题。 (在[第十一章](ch11.md)更详细地讨论幂等性)
|
||||
* 每次调用本地功能时,通常需要大致相同的时间来执行。网络请求比函数调用要慢得多,而且其延迟也是非常可变的:在不到一毫秒的时间内它可能会完成,但是当网络拥塞或者远程服务超载时,可能需要几秒钟的时间完全一样的东西。
|
||||
* 调用本地函数时,可以高效地将引用(指针)传递给本地内存中的对象。当你发出一个网络请求时,所有这些参数都需要被编码成可以通过网络发送的一系列字节。没关系,如果参数是像数字或字符串这样的基本类型,但是对于较大的对象很快就会变成问题。
|
||||
|
||||
客户端和服务可以用不同的编程语言实现,所以RPC框架必须将数据类型从一种语言翻译成另一种语言。这可能会捅出大篓子,因为不是所有的语言都具有相同的类型 - 例如回想一下JavaScript的数字大于$2^{53}$的问题(请参阅第114页上的“JSON,XML和二进制变体”)。用单一语言编写的单个进程中不存在此问题。
|
||||
客户端和服务可以用不同的编程语言实现,所以RPC框架必须将数据类型从一种语言翻译成另一种语言。这可能会捅出大篓子,因为不是所有的语言都具有相同的类型 - 例如回想一下JavaScript的数字大于$2^{53}$的问题(参阅“[JSON,XML和二进制变体](#JSON,XML和二进制变体)”)。用单一语言编写的单个进程中不存在此问题。
|
||||
|
||||
所有这些因素意味着尝试使远程服务看起来像编程语言中的本地对象一样毫无意义,因为这是一个根本不同的事情。 REST的部分吸引力在于,它并不试图隐藏它是一个网络协议的事实(尽管这似乎并没有阻止人们在REST之上构建RPC库)。
|
||||
|
||||
@ -433,9 +428,9 @@ Web服务仅仅是通过网络进行API请求的一系列技术的最新版本
|
||||
|
||||
尽管有这样那样的问题,RPC不会消失。在本章提到的所有编码的基础上构建了各种RPC框架:例如,Thrift和Avro带有RPC支持,gRPC是使用Protocol Buffers的RPC实现,Finagle也使用Thrift,Rest.li使用JSON over HTTP。
|
||||
|
||||
这种新一代的RPC框架更加明确的是,远程请求与本地函数调用不同。例如,Finagle和Rest.li使用futures(promises)来封装可能失败的异步操作。`Futures`还可以简化需要并行发出多项服务的情况,并将其结果合并[45]。 gRPC支持流,其中一个调用不仅包括一个请求和一个响应,还包括一系列的请求和响应[46]。
|
||||
这种新一代的RPC框架更加明确的是,远程请求与本地函数调用不同。例如,Finagle和Rest.li使用futures(promises)来封装可能失败的异步操作。`Futures`还可以简化需要并行发出多项服务的情况,并将其结果合并【45】。 gRPC支持流,其中一个调用不仅包括一个请求和一个响应,还包括一系列的请求和响应【46】。
|
||||
|
||||
其中一些框架还提供服务发现,即允许客户端找出在哪个IP地址和端口号上可以找到特定的服务。我们将在第214页的“请求路由”中回到这个主题。
|
||||
其中一些框架还提供服务发现,即允许客户端找出在哪个IP地址和端口号上可以找到特定的服务。我们将在“[请求路由](ch6.md#请求路由)”中回到这个主题。
|
||||
|
||||
使用二进制编码格式的自定义RPC协议可以实现比通用的JSON over REST更好的性能。但是,RESTful API还有其他一些显着的优点:对于实验和调试(只需使用Web浏览器或命令行工具curl,无需任何代码生成或软件安装即可向其请求),它是受支持的所有的主流编程语言和平台,还有大量可用的工具(服务器,缓存,负载平衡器,代理,防火墙,监控,调试工具,测试工具等)的生态系统。由于这些原因,REST似乎是公共API的主要风格。 RPC框架的主要重点在于同一组织拥有的服务之间的请求,通常在同一数据中心内。
|
||||
|
||||
@ -446,12 +441,12 @@ Web服务仅仅是通过网络进行API请求的一系列技术的最新版本
|
||||
RPC方案的前后向兼容性属性从它使用的编码方式中继承
|
||||
|
||||
* Thrift,gRPC(协议缓冲区)和Avro RPC可以根据相应编码格式的兼容性规则进行演变。
|
||||
* 在SOAP中,请求和响应是使用XML模式指定的。这些可以演变,但有一些微妙的陷阱[47]。
|
||||
* 在SOAP中,请求和响应是使用XML模式指定的。这些可以演变,但有一些微妙的陷阱【47】。
|
||||
* RESTful API通常使用JSON(没有正式指定的模式)用于响应,以及用于请求的JSON或URI编码/表单编码的请求参数。添加可选的请求参数并向响应对象添加新的字段通常被认为是保持兼容性的改变。
|
||||
|
||||
由于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方案的前后向兼容性属性从它使用的编码方式中继承
|
||||
|
||||
#### 消息掮客
|
||||
|
||||
过去,信息掮客主要是TIBCO,IBM WebSphere和webMethods等公司的商业软件的秀场。最近像RabbitMQ,ActiveMQ,HornetQ,NATS和Apache Kafka这样的开源实现已经流行起来。我们将在第11章中对它们进行更详细的比较。
|
||||
过去,信息掮客主要是TIBCO,IBM WebSphere和webMethods等公司的商业软件的秀场。最近像RabbitMQ,ActiveMQ,HornetQ,NATS和Apache Kafka这样的开源实现已经流行起来。我们将在[第11章](ch11.md)中对它们进行更详细的比较。
|
||||
|
||||
详细的交付语义因实现和配置而异,但通常情况下,消息代理的使用方式如下:一个进程将消息发送到指定的队列或主题,代理确保将消息传递给一个或多个消费者或订阅者到那个队列或主题。在同一主题上可以有许多生产者和许多消费者。
|
||||
|
||||
一个主题只提供单向数据流。但是,消费者本身可能会将消息发布到另一个主题上(因此,可以将它们链接在一起,就像我们将在第11章中看到的那样),或者发送给原始消息的发送者使用的回复队列(允许请求/响应数据流,类似于RPC)。
|
||||
一个主题只提供单向数据流。但是,消费者本身可能会将消息发布到另一个主题上(因此,可以将它们链接在一起,就像我们将在[第11章](ch11.md)中看到的那样),或者发送给原始消息的发送者使用的回复队列(允许请求/响应数据流,类似于RPC)。
|
||||
|
||||
消息代理通常不会执行任何特定的数据模型 - 消息只是包含一些元数据的字节序列,因此您可以使用任何编码格式。如果编码是向后兼容的,则您可以灵活地更改发行商和消费者的独立编码,并以任意顺序进行部署。
|
||||
|
||||
如果消费者重新发布消息到另一个主题,则可能需要小心保留未知字段,以防止前面在数据库环境中描述的问题(图4-7)。
|
||||
如果消费者重新发布消息到另一个主题,则可能需要小心保留未知字段,以防止前面在数据库环境中描述的问题([图4-7](img/fig4-7.png))。
|
||||
|
||||
#### 分布式的Actor框架
|
||||
|
||||
@ -493,9 +488,9 @@ actor模型是单个进程中并发的编程模型。逻辑被封装在角色中
|
||||
|
||||
三个流行的分布式actor框架处理消息编码如下:
|
||||
|
||||
* 默认情况下,Akka使用Java的内置序列化,不提供前向或后向兼容性。 但是,你可以用类似缓冲区的东西替代它,从而获得滚动升级的能力[50]。
|
||||
* `Orleans`默认使用不支持滚动升级部署的自定义数据编码格式; 要部署新版本的应用程序,您需要设置一个新的群集,将流量从旧群集迁移到新群集,然后关闭旧群集[51,52]。 像Akka一样,可以使用自定义序列化插件。
|
||||
* 在Erlang OTP中,对记录模式进行更改是非常困难的(尽管系统具有许多为高可用性设计的功能)。 滚动升级是可能的,但需要仔细计划[53]。 一个新的实验性的`maps`数据类型(2014年在Erlang R17中引入的类似于JSON的结构)可能使得这个数据类型在未来更容易[54]。
|
||||
* 默认情况下,Akka使用Java的内置序列化,不提供前向或后向兼容性。 但是,你可以用类似缓冲区的东西替代它,从而获得滚动升级的能力【50】。
|
||||
* `Orleans`默认使用不支持滚动升级部署的自定义数据编码格式; 要部署新版本的应用程序,您需要设置一个新的群集,将流量从旧群集迁移到新群集,然后关闭旧群集【51,52】。 像Akka一样,可以使用自定义序列化插件。
|
||||
* 在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. “[EsotericSoftware/kryo](https://github.com/EsotericSoftware/kryo),”
|
||||
*github.com*, October 2014.
|
||||
1. “[EsotericSoftware/kryo](https://github.com/EsotericSoftware/kryo),” *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*,
|
||||
July 30, 2014.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
1. Eishay Smith:
|
||||
“[jvm-serializers wiki](https://github.com/eishay/jvm-serializers/wiki),”
|
||||
*github.com*, November 2014.
|
||||
1. Eishay Smith: “[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. 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.
|
||||
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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
1. Francis Galiegue, Kris Zyp, and Gary Court:
|
||||
“[JSON Schema](http://json-schema.org/),” IETF Internet-Draft, February 2013.
|
||||
1. Francis Galiegue, Kris Zyp, and Gary Court: “[JSON Schema](http://json-schema.org/),” IETF Internet-Draft, February 2013.
|
||||
|
||||
1. Yakov Shafranovich:
|
||||
“[RFC 4180: Common Format and MIME Type for Comma-Separated Values (CSV) Files](https://tools.ietf.org/html/rfc4180),” October 2005.
|
||||
1. Yakov Shafranovich: “[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. 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. “[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. “[Protocol Buffers Developer Guide](https://developers.google.com/protocol-buffers/docs/overview),” Google, Inc., *developers.google.com*.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
1. “[A Matrix of the Features Each Individual Language Library Supports](http://wiki.apache.org/thrift/LibraryFeatures),”
|
||||
*wiki.apache.org*.
|
||||
1. “[A Matrix of the Features Each Individual Language Library Supports](http://wiki.apache.org/thrift/LibraryFeatures),” *wiki.apache.org*.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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模型是单个进程中并发的编程模型。逻辑被封装在角色中
|
||||
“[[PROPOSAL] 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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
1. “[Apache Pig 0.14.0 Documentation](http://pig.apache.org/docs/r0.14.0/),” *pig.apache.org*, November 2014.
|
||||
|
||||
1. John Larmouth:
|
||||
<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. 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
|
||||
|
||||
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,
|
||||
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,
|
||||
January 1999.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
1. Sam Newman: *Building Microservices*.
|
||||
O'Reilly Media, 2015. ISBN: 978-1-491-95035-7
|
||||
1. Sam Newman: *Building Microservices*. O'Reilly Media, 2015. ISBN: 978-1-491-95035-7
|
||||
|
||||
1. Chris Richardson:
|
||||
“[Microservices: Decomposing Applications for Deployability and Scalability](http://www.infoq.com/articles/microservices-intro),” *infoq.com*, May 25, 2014.
|
||||
1. Chris Richardson: “[Microservices: Decomposing Applications for Deployability and Scalability](http://www.infoq.com/articles/microservices-intro),” *infoq.com*, May 25, 2014.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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. Pete Lacey:
|
||||
“[The S Stands for Simple](http://harmful.cat-v.org/software/xml/soap/simple),” *harmful.cat-v.org*, November 15, 2006.
|
||||
1. Pete Lacey: “[The S Stands for Simple](http://harmful.cat-v.org/software/xml/soap/simple),” *harmful.cat-v.org*, November 15, 2006.
|
||||
|
||||
1. Stefan Tilkov:
|
||||
“[Interview: Pete Lacey Criticizes Web Services](http://www.infoq.com/articles/pete-lacey-ws-criticism),” *infoq.com*, December 12, 2006.
|
||||
1. Stefan Tilkov: “[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/),”
|
||||
*swagger.io*, September 8, 2014.
|
||||
1. “[OpenAPI Specification (fka Swagger RESTful API Documentation Specification) Version 2.0](http://swagger.io/specification/),” *swagger.io*, September 8, 2014.
|
||||
|
||||
1. Michi Henning:
|
||||
“[The Rise and Fall of CORBA](http://queue.acm.org/detail.cfm?id=1142044),”
|
||||
*ACM Queue*, volume 4, number 5, pages 28–34, June 2006.
|
||||
1. Michi Henning: “[The Rise and Fall of CORBA](http://queue.acm.org/detail.cfm?id=1142044),” *ACM Queue*, volume 4, number 5, pages 28–34, June 2006.
|
||||
[doi:10.1145/1142031.1142044](http://dx.doi.org/10.1145/1142031.1142044)
|
||||
|
||||
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 39–59, February 1984.
|
||||
[doi:10.1145/2080.357392](http://dx.doi.org/10.1145/2080.357392)
|
||||
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 39–59, 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:
|
||||
“[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. 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.
|
||||
|
||||
1. Steve Vinoski:
|
||||
“[Convenience over Correctness](http://steve.vinoski.net/pdf/IEEE-Convenience_Over_Correctness.pdf),” *IEEE Internet Computing*, volume 12, number 4, pages 89–92, July 2008.
|
||||
[doi:10.1109/MIC.2008.75](http://dx.doi.org/10.1109/MIC.2008.75)
|
||||
1. Steve Vinoski: “[Convenience over Correctness](http://steve.vinoski.net/pdf/IEEE-Convenience_Over_Correctness.pdf),” *IEEE Internet Computing*, volume 12, number 4, pages 89–92, July 2008. [doi:10.1109/MIC.2008.75](http://dx.doi.org/10.1109/MIC.2008.75)
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
1. “[grpc-common Documentation](https://github.com/grpc/grpc-common),” Google, Inc., *github.com*, February 2015.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
1. “[API Upgrades](https://stripe.com/docs/upgrades),” Stripe, Inc., April 2015.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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:
|
||||
“[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. 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.
|
||||
|
||||
1. Fred Hebert:
|
||||
“[Postscript: Maps](http://learnyousomeerlang.com/maps),” *learnyousomeerlang.com*,
|
||||
April 9, 2014.
|
||||
1. Fred Hebert: “[Postscript: Maps](http://learnyousomeerlang.com/maps),” *learnyousomeerlang.com*, April 9, 2014.
|
||||
|
||||
|
||||
------
|
||||
|
@ -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)
|
||||
**图5-1 基于领导者(主-从)的复制**
|
||||
|
||||
这种复制模式是许多关系数据库的内置功能,如PostgreSQL(从9.0版本开始),MySQL,Oracle Data Guard [2]和SQL Server的AlwaysOn可用性组[3]。 它也用于一些非关系数据库,包括MongoDB,RethinkDB和Espresso [4]。 最后,基于领导者的复制并不仅限于数据库:像Kafka [5]和RabbitMQ高可用队列[6]这样的分布式消息代理也使用它。 某些网络文件系统,例如DRBD这样的块复制设备也与之类似。
|
||||
这种复制模式是许多关系数据库的内置功能,如PostgreSQL(从9.0版本开始),MySQL,Oracle Data Guard 【2】和SQL Server的AlwaysOn可用性组【3】。 它也用于一些非关系数据库,包括MongoDB,RethinkDB和Espresso [4]。 最后,基于领导者的复制并不仅限于数据库:像Kafka [5]和RabbitMQ高可用队列【6】这样的分布式消息代理也使用它。 某些网络文件系统,例如DRBD这样的块复制设备也与之类似。
|
||||
|
||||
|
||||
|
||||
@ -62,7 +62,7 @@
|
||||
|
||||
同步复制的优点是,跟随者保证有与领导者一致的最新数据副本。如果领导者突然失败,我们可以肯定的是,这些数据仍然可以在追随者身上找到。缺点是如果同步跟随器没有响应(因为它已经崩溃,或者出现网络故障,或者出于任何其他原因),写入不能被处理。领导者必须阻止所有写入,并等待同步副本再次可用。
|
||||
|
||||
因此,所有追随者都是同步的是不切实际的:任何一个节点的中断都会导致整个系统停滞不前。实际上,如果在数据库上启用同步复制,通常意味着其中**一个**跟随者是同步的,而其他的则是异步的。如果同步跟随器变得不可用或缓慢,则使一个异步跟随器同步。这保证您至少在两个节点上拥有最新的数据副本:领导者和同步追随者。 这种配置有时也被称为**半同步(semi-synchronous)**[7]。
|
||||
因此,所有追随者都是同步的是不切实际的:任何一个节点的中断都会导致整个系统停滞不前。实际上,如果在数据库上启用同步复制,通常意味着其中**一个**跟随者是同步的,而其他的则是异步的。如果同步跟随器变得不可用或缓慢,则使一个异步跟随器同步。这保证您至少在两个节点上拥有最新的数据副本:领导者和同步追随者。 这种配置有时也被称为**半同步(semi-synchronous)**【7】。
|
||||
|
||||
通常情况下,基于领导者的复制都配置为完全异步。 在这种情况下,如果领导失败并且不可恢复,则任何尚未复制给追随者的写入都将丢失。 这意味着即使已经向客户端确认成功,写入也不能保证**持久(Durable)**。 然而,一个完全异步的配置也有优点:即使所有的追随者都落后了,领导者也可以继续处理写入。
|
||||
|
||||
|
89
ddia/ch6.md
89
ddia/ch6.md
@ -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卷则包含以T,U,V,X,Y和Z开头的单词。每个字母的两个字母只有一个音量导致一些卷比其他卷更大。为了均匀分配数据,分区边界需要适应数据。
|
||||
|
||||
分区边界可以由管理员手动选择,也可以由数据库自动选择(我们将在第209页的“[重新平衡分区]()”中更详细地讨论分区边界的选择)。 Bigtable使用了这种分区策略,以及其开源等价物HBase [2,3],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的第一个部分。 例如,可以在每个时间戳前添加传感器名称,以便分区首先按传感器名称,然后按时间。 假设同时有许多传感器处于活动状态,则写入负载将最终均匀分布在分区上。 现在,当您想要在一个时间范围内获取多个传感器的值时,您需要为每个传感器名称执行一个单独的范围查询。
|
||||
|
||||
@ -78,9 +78,9 @@
|
||||
|
||||
一个好的散列函数可以将接受偏斜的数据并使其均匀分布。假设你有一个带有字符串的32位散列函数。无论何时给它一个新的字符串,它将返回一个0到$2^{32}-1$之间的"随机"数。即使输入的字符串非常相似,它们的散列也会均匀分布在这个数字范围内。
|
||||
|
||||
出于分区的目的,散列函数不需要多么强壮的密码学安全性:例如,Cassandra和MongoDB使用MD5,Voldemort使用Fowler-Noll-Vo函数。许多编程语言都有内置的简单哈希函数(因为它们用于哈希表),但是它们可能不适合分区:例如,在Java的`Object.hashCode()`和Ruby的`Object#hash`,同一个键可能有不同的进程中不同的哈希值[6]。
|
||||
出于分区的目的,散列函数不需要多么强壮的密码学安全性:例如,Cassandra和MongoDB使用MD5,Voldemort使用Fowler-Noll-Vo函数。许多编程语言都有内置的简单哈希函数(因为它们用于哈希表),但是它们可能不适合分区:例如,在Java的`Object.hashCode()`和Ruby的`Object#hash`,同一个键可能有不同的进程中不同的哈希值【6】。
|
||||
|
||||
一旦你有一个合适的键散列函数,你可以为每个分区分配一个散列范围(而不是键的范围),每个散列落在分区范围内的键将被存储在该分区中。如图6-3所示。
|
||||
一旦你有一个合适的键散列函数,你可以为每个分区分配一个散列范围(而不是键的范围),每个散列落在分区范围内的键将被存储在该分区中。如[图6-3](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_id,update_timestamp)`,那么您可以有效地检索特定用户在某个时间间隔内按时间戳排序的所有更新。不同的用户可以存储在不同的分区上,但是在每个用户中,更新按时间戳顺序存储在单个分区上。
|
||||
串联索引方法为一对多关系提供了一个优雅的数据模型。例如,在社交媒体网站上,一个用户可能会发布很多更新。如果更新的主键被选择为`(user_id, update_timestamp)`,那么您可以有效地检索特定用户在某个时间间隔内按时间戳排序的所有更新。不同的用户可以存储在不同的分区上,但是在每个用户中,更新按时间戳顺序存储在单个分区上。
|
||||
|
||||
### 负载倾斜与消除热点
|
||||
|
||||
如前所述,哈希键确定其分区可以帮助减少热点。但是,它不能完全避免它们:在极端情况下,所有的读写操作都是针对同一个键的,所有的请求都会被路由到同一个分区。
|
||||
|
||||
这种工作量也许并不常见,但并非闻所未闻:例如,在社交媒体网站上,一个拥有数百万追随者的名人用户在做某事时可能会引发一场风暴[14]。这个事件可能导致大量写入同一个键(键可能是名人的用户ID,或者人们正在评论的动作的ID)。哈希键不起作用,因为两个相同ID的哈希值仍然是相同的。
|
||||
这种工作量也许并不常见,但并非闻所未闻:例如,在社交媒体网站上,一个拥有数百万追随者的名人用户在做某事时可能会引发一场风暴【14】。这个事件可能导致大量写入同一个键(键可能是名人的用户ID,或者人们正在评论的动作的ID)。哈希键不起作用,因为两个相同ID的哈希值仍然是相同的。
|
||||
|
||||
如今,大多数数据系统无法自动补偿这种高度偏斜的工作负载,因此应用程序有责任减少偏斜。例如,如果一个密钥被认为是非常热的,一个简单的方法是在密钥的开始或结尾添加一个随机数。只要一个两位数的十进制随机数就可以将写入密钥分散到100个不同的密钥中,从而允许这些密钥分配到不同的分区。
|
||||
|
||||
@ -140,7 +140,7 @@ Cassandra在两种分区策略之间达成了一个折衷[11,12,13]。 Cassandra
|
||||
|
||||
但是,从文档分区索引中读取需要注意:除非您对文档ID做了特别的处理,否则没有理由将所有具有特定颜色或特定品牌的汽车放在同一个分区中。在图6-4中,红色汽车出现在分区0和分区1中。因此,如果要搜索红色汽车,则需要将查询发送到所有分区,并合并所有返回的结果。
|
||||
|
||||
这种查询分区数据库的方法有时被称为**分散/聚集(scatter/gather)**,并且可能会使二级索引上的读取查询相当昂贵。即使您并行查询分区,分散/聚集也容易导致尾部延迟放大(请参阅第16页的“实践中的百分比”)。然而,它被广泛使用:MonDBDB,Riak [15],Cassandra [16],Elasticsearch [17],SolrCloud [18]和VoltDB [19]都使用文档分区二级索引。大多数数据库供应商建议您构建一个能从单个分区提供二级索引查询的分区方案,但这并不总是可行,尤其是当在单个查询中使用多个二级索引时(例如同时需要按颜色和制造商查询)。
|
||||
这种查询分区数据库的方法有时被称为**分散/聚集(scatter/gather)**,并且可能会使二级索引上的读取查询相当昂贵。即使您并行查询分区,分散/聚集也容易导致尾部延迟放大(请参阅第16页的“实践中的百分比”)。然而,它被广泛使用:MonDBDB,Riak 【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)
|
||||
|
||||
@ -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
|
||||
|
||||
我们在前面说过(图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,这似乎是将每个键分配给一个节点的简单方法。
|
||||
|
||||
模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个分区被分配给每个节点。
|
||||
|
||||
现在,如果一个节点被添加到集群中,新节点可以从每个现有节点中**窃取**几个分区,直到分区再次公平分配。这个过程如图6-6所示。如果从集群中删除一个节点,则会发生相反的情况。
|
||||
现在,如果一个节点被添加到集群中,新节点可以从每个现有节点中**窃取**几个分区,直到分区再次公平分配。这个过程如[图6-6](img/fig6-6.png)所示。如果从集群中删除一个节点,则会发生相反的情况。
|
||||
|
||||
只有整个分区在节点之间移动。分区的数量不会改变,键所指定的分区也不会改变。唯一改变的是分区所指派的节点。这种指派变更并不是即时的——在网络上传输大量的数据需要一些时间——所以在传输过程中,旧的分区会接受传输过程中发生的读写操作。
|
||||
|
||||
@ -210,9 +210,7 @@ Cassandra在两种分区策略之间达成了一个折衷[11,12,13]。 Cassandra
|
||||
|
||||
**图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同时支持键范围和哈希分区,并且在任何情况下动态分割分区。
|
||||
|
||||
@ -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]。
|
||||
随机选择分区边界要求使用基于散列的分区(所以可以从散列函数产生的数字范围中挑选边界)。实际上,这种方法最符合一致性散列的原始定义[7](请参阅第204页的“一致性散列”)。较新的哈希函数可以在降低元数据开销的情况下达到类似的效果[8]。
|
||||
当一个新节点加入集群时,它随机选择固定数量的现有分区进行拆分,然后占有这些拆分分区中每个分区的一半,同时将每个分区的另一半留在原地。随机化可能会产生不公平的分裂,但是当在更大数量的分区上进行平均时(在Cassandra中,默认情况下,每个节点有256个分区),新节点最终从现有节点获得公平的负载份额。 Cassandra 3.0引入了另一种可重用的算法来避免不公平的分裂【29】。
|
||||
|
||||
随机选择分区边界要求使用基于散列的分区(所以可以从散列函数产生的数字范围中挑选边界)。实际上,这种方法最符合一致性散列的原始定义【7】(参阅“[一致性散列](#一致性散列)”)。较新的哈希函数可以在降低元数据开销的情况下达到类似的效果【8】。
|
||||
|
||||
### 运维:手动还是自动平衡
|
||||
|
||||
@ -259,12 +258,12 @@ Cassandra和Ketama使用的第三种方法是使分区数与节点数成比例 -
|
||||
|
||||
现在我们已经将数据集分割到多个机器上运行的多个节点上。但是仍然存在一个悬而未决的问题:当客户想要提出请求时,如何知道要连接哪个节点?随着分区重新平衡,分区对节点的分配也发生变化。为了回答这个问题,有人需要停留在这些变化之上:如果我想读或写键“foo”,需要连接哪个IP地址和端口号?
|
||||
|
||||
这是一个称为**服务发现(service discovery)**的更普遍问题的实例,它不仅限于数据库。任何可通过网络访问的软件都有这个问题,特别是如果它的目标是实现高可用性(在多台机器上运行冗余配置)。许多公司已经编写了自己的内部服务发现工具,其中许多已经作为开源发布[30]。
|
||||
这是一个称为**服务发现(service discovery)**的更普遍问题的实例,它不仅限于数据库。任何可通过网络访问的软件都有这个问题,特别是如果它的目标是实现高可用性(在多台机器上运行冗余配置)。许多公司已经编写了自己的内部服务发现工具,其中许多已经作为开源发布【30】。
|
||||
|
||||
在很高的层面上,这个问题有几种不同的方法(如图6-7所示):
|
||||
|
||||
1. 允许客户联系任何节点(例如,通过**循环策略的负载均衡(Round-Robin Load Balancer)**)。如果该节点巧合地拥有请求所适用的分区,则它可以直接处理该请求;否则,它将请求转发到适当的节点,接收答复并传递给客户端。
|
||||
2. 首先将所有来自客户端的请求发送到路由选择层,它决定了应该处理请求的节点,并相应地转发。此路由层本身不处理任何请求;它仅充当分区感知负载平衡器。
|
||||
2. 首先将所有来自客户端的请求发送到路由选择层,它决定了应该处理请求的节点,并相应地转发。此路由层本身不处理任何请求;它仅充当分区感知负载平衡器。
|
||||
3. 要求客户端知道分区和节点分配。在这种情况下,客户端可以直接连接到适当的节点,而不需要任何中介。
|
||||
|
||||
在所有情况下,关键问题是:作出路由决策的组件(可能是节点之一,还是路由层或客户端)如何了解分区-节点之间的分配关系变化?
|
||||
@ -273,19 +272,19 @@ Cassandra和Ketama使用的第三种方法是使分区数与节点数成比例 -
|
||||
|
||||
**图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)
|
||||
|
||||
**图6-8 使用ZooKeeper跟踪分区分配给节点。**
|
||||
|
||||
例如,LinkedIn的Espresso使用Helix [31]进行集群管理(依靠ZooKeeper),实现了一个路由层,如图6-8所示。 HBase,SolrCloud和Kafka也使用ZooKeeper来跟踪分区分配。 MongoDB具有类似的体系结构,但它依赖于自己的**配置服务器(config server)**实现和mongos守护进程作为路由层。
|
||||
例如,LinkedIn的Espresso使用Helix 【31】进行集群管理(依靠ZooKeeper),实现了一个路由层,如[图6-8](img/fig6-8.png)所示。 HBase,SolrCloud和Kafka也使用ZooKeeper来跟踪分区分配。 MongoDB具有类似的体系结构,但它依赖于自己的**配置服务器(config server)**实现和mongos守护进程作为路由层。
|
||||
|
||||
Cassandra和Riak采取不同的方法:他们在节点之间使用**八卦协议(gossip protocal)**来传播群集状态的变化。请求可以发送到任意节点,该节点会转发到包含所请求的分区的适当节点(图6-7中的方法1)。这个模型在数据库节点中增加了更多的复杂性,但是避免了对像ZooKeeper这样的外部协调服务的依赖。
|
||||
Cassandra和Riak采取不同的方法:他们在节点之间使用**八卦协议(gossip protocal)**来传播群集状态的变化。请求可以发送到任意节点,该节点会转发到包含所请求的分区的适当节点([图6-7]()中的方法1)。这个模型在数据库节点中增加了更多的复杂性,但是避免了对像ZooKeeper这样的外部协调服务的依赖。
|
||||
|
||||
Couchbase不会自动重新平衡,这简化了设计。通常情况下,它配置了一个名为moxi的路由选择层,它会从集群节点了解路由变化[32]。
|
||||
Couchbase不会自动重新平衡,这简化了设计。通常情况下,它配置了一个名为moxi的路由选择层,它会从集群节点了解路由变化【32】。
|
||||
|
||||
当使用路由层或向随机节点发送请求时,客户端仍然需要找到要连接的IP地址。这些分区并不像分配给节点那么快,所以为此使用DNS通常就足够了。
|
||||
|
||||
@ -295,7 +294,7 @@ Couchbase不会自动重新平衡,这简化了设计。通常情况下,它
|
||||
|
||||
然而,通常用于分析的**大规模并行处理(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 85–98, 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](),”
|
||||
*Communications of the ACM*, volume 35, number 6, pages 85–98, June 1992. [doi:10.1145/129888.129894](http://dx.doi.org/10.1145/129888.129894)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user