bold format

This commit is contained in:
Vonng 2020-01-02 13:17:10 +08:00
parent 5a9f4a9315
commit 4408677a5f
9 changed files with 92 additions and 96 deletions

View File

@ -3,8 +3,8 @@
- 作者: [Martin Kleppmann](https://martin.kleppmann.com)
- 原书名称:[《Designing Data-Intensive Application》](http://shop.oreilly.com/product/0636920032175.do)
- 译者:[冯若航]( http://vonng.com/about) fengruohang@outlook.com
- Gitbook地址[ddia-cn](https://www.gitbook.com/book/vonng/ddia-cn)(需要科学上网)
- 建议使用[Typora](https://www.typora.io)或Gitbook以获取最佳阅读体验。
- Gitbook地址[ddia-cn](https://www.gitbook.com/book/vonng/ddia-cn)
- 使用[Typora](https://www.typora.io)或Gitbook以获取最佳阅读体验。
@ -27,15 +27,13 @@
##
## 前言
> 在我们的社会中,技术是一种强大的力量。数据、软件、通信可以用于坏的方面:不公平的阶级固化,损害公民权利,保护既得利益集团。但也可以用于好的方面:让底层人民发出自己的声音,让每个人都拥有机会,避免灾难。本书献给所有将技术用于善途的人们。
---------
> 计算是一种流行文化,流行文化鄙视历史。 流行文化关乎个体身份和参与感,但与合作无关。流行文化活在当下,也与过去和未来无关。 我认为大部分(为了钱)编写代码的人就是这样的, 他们不知道自己的文化来自哪里。
> 计算是一种流行文化,流行文化鄙视历史。 流行文化关乎个体身份和参与感,但与合作无关。流行文化活在当下,也与过去和未来无关。 我认为大部分(为了钱)编写代码的人就是这样的, 他们不知道自己的文化来自哪里。
>
> ——阿兰·凯接受Dobb博士的杂志采访时2012年
@ -92,6 +90,9 @@
4. 第一部分前言ch2校正 by @jiajiadebug
5. 词汇表、后记关于野猪的部分 by @[Chowss](https://github.com/Vonng/ddia/commits?author=Chowss)
https://github.com/Vonng/ddia/pulls)
感谢所有作出贡献,提出意见的朋友们:[Issues](https://github.com/Vonng/ddia/issues)[Pull Requests](https://github.com/Vonng/ddia/pulls)

36
ch1.md
View File

@ -10,17 +10,17 @@
[TOC]
现今很多应用程序都是 **数据密集型data-intensive** 的,而非 **计算密集型compute-intensive** 的。因此CPU很少成为这类应用的瓶颈更大的问题通常来自数据量、数据复杂性、以及数据的变更速度。
现今很多应用程序都是 **数据密集型data-intensive** 的,而非 **计算密集型compute-intensive** 的。因此CPU很少成为这类应用的瓶颈更大的问题通常来自数据量、数据复杂性、以及数据的变更速度。
数据密集型应用通常由标准组件构建而成,标准组件提供了很多通用的功能;例如,许多应用程序都需要:
- 存储数据,以便自己或其他应用程序之后能再次找到 ***数据库database***
- 存储数据,以便自己或其他应用程序之后能再次找到 ***数据库database***
- 记住开销昂贵操作的结果,加快读取速度(***缓存cache***
- 允许用户按关键字搜索数据,或以各种方式对数据进行过滤(***搜索索引search indexes***
- 向其他进程发送消息,进行异步处理(***流处理stream processing***
- 定期处理累积的大批量数据(***批处理batch processing***
如果这些功能听上去平淡无奇,那是因为这些 **数据系统data system** 是非常成功的抽象:我们一直不假思索地使用它们并习以为常。绝大多数工程师不会幻想从零开始编写存储引擎,因为在开发应用时,数据库已经是足够完美的工具了。
如果这些功能听上去平淡无奇,那是因为这些 **数据系统data system** 是非常成功的抽象:我们一直不假思索地使用它们并习以为常。绝大多数工程师不会幻想从零开始编写存储引擎,因为在开发应用时,数据库已经是足够完美的工具了。
但现实没有这么简单。不同的应用有着不同的需求,因而数据库系统也是百花齐放,有着各式各样的特性。实现缓存有很多种手段,创建搜索索引也有好几种方法,诸如此类。因此在开发应用前,我们依然有必要先弄清楚最适合手头工作的工具和方法。而且当单个工具解决不了你的问题时,组合使用这些工具可能还是有些难度的。
@ -34,7 +34,7 @@
我们通常认为,数据库、消息队列、缓存等工具分属于几个差异显著的类别。虽然数据库和消息队列表面上有一些相似性——它们都会存储一段时间的数据——但它们有迥然不同的访问模式,这意味着迥异的性能特征和实现手段。
那我们为什么要把这些东西放在**数据系统data system**的总称之下混为一谈呢?
那我们为什么要把这些东西放在 **数据系统data system** 的总称之下混为一谈呢?
近些年来出现了许多新的数据存储工具与数据处理工具。它们针对不同应用场景进行优化因此不再适合生硬地归入传统类别【1】。类别之间的界限变得越来越模糊例如数据存储可以被当成消息队列用Redis消息队列则带有类似数据库的持久保证Apache Kafka
@ -96,7 +96,7 @@
当想到系统失效的原因时,**硬件故障hardware faults**总会第一个进入脑海。硬盘崩溃、内存出错、机房断电、有人拔错网线……任何与大型数据中心打过交道的人都会告诉你:一旦你拥有很多机器,这些事情**总**会发生!
据报道称,硬盘的**平均无故障时间MTTF, mean time to failure**约为10到50年【5】【6】。因此从数学期望上讲在拥有10000个磁盘的存储集群上平均每天会有1个磁盘出故障。
据报道称,硬盘的 **平均无故障时间MTTF mean time to failure** 约为10到50年【5】【6】。因此从数学期望上讲在拥有10000个磁盘的存储集群上平均每天会有1个磁盘出故障。
为了减少系统的故障率第一反应通常都是增加单个硬件的冗余度例如磁盘可以组建RAID服务器可能有双路电源和热插拔CPU数据中心可能有电池和柴油发电机作为后备电源某个组件挂掉时冗余组件可以立刻接管。这种方法虽然不能完全防止由硬件问题导致的系统失效但它简单易懂通常也足以让机器不间断运行很多年。
@ -148,13 +148,13 @@
## 可扩展性
系统今天能可靠运行,并不意味未来也能可靠运行。服务**降级degradation**的一个常见原因是负载增加,例如:系统负载已经从一万个并发用户增长到十万个并发用户,或者从一百万增长到一千万。也许现在处理的数据量级要比过去大得多。
系统今天能可靠运行,并不意味未来也能可靠运行。服务 **降级degradation** 的一个常见原因是负载增加,例如:系统负载已经从一万个并发用户增长到十万个并发用户,或者从一百万增长到一千万。也许现在处理的数据量级要比过去大得多。
**可扩展性Scalability**是用来描述系统应对负载增长能力的术语。但是请注意这不是贴在系统上的一维标签说“X可扩展”或“Y不可扩展”是没有任何意义的。相反讨论可扩展性意味着考虑诸如“如果系统以特定方式增长有什么选项可以应对增长”和“如何增加计算资源来处理额外的负载”等问题。
**可扩展性Scalability** 是用来描述系统应对负载增长能力的术语。但是请注意这不是贴在系统上的一维标签说“X可扩展”或“Y不可扩展”是没有任何意义的。相反讨论可扩展性意味着考虑诸如“如果系统以特定方式增长有什么选项可以应对增长”和“如何增加计算资源来处理额外的负载”等问题。
### 描述负载
在讨论增长问题(如果负载加倍会发生什么?)前,首先要能简要描述系统的当前负载。负载可以用一些称为**负载参数load parameters**的数字来描述。参数的最佳选择取决于系统架构它可能是每秒向Web服务器发出的请求、数据库中的读写比率、聊天室中同时活跃的用户数量、缓存命中率或其他东西。除此之外也许平均情况对你很重要也许你的瓶颈是少数极端场景。
在讨论增长问题(如果负载加倍会发生什么?)前,首先要能简要描述系统的当前负载。负载可以用一些称为 **负载参数load parameters** 的数字来描述。参数的最佳选择取决于系统架构它可能是每秒向Web服务器发出的请求、数据库中的读写比率、聊天室中同时活跃的用户数量、缓存命中率或其他东西。除此之外也许平均情况对你很重要也许你的瓶颈是少数极端场景。
为了使这个概念更加具体我们以推特在2012年11月发布的数据【16】为例。推特的两个主要业务是
@ -216,8 +216,7 @@
> #### 延迟和响应时间
>
> **延迟latency**和**响应时间response time**经常用作同义词,但实际上它们并不一样。响应时间是客户所看到的,除了实际处理请求的时间(**服务时间service time**)之外,还包括网络延迟和排队延迟。延迟是某个请求等待处理的**持续时长**,在此期间它处于**休眠latent**状态并等待服务【17】。
>
> **延迟latency****响应时间response time** 经常用作同义词,但实际上它们并不一样。响应时间是客户所看到的,除了实际处理请求的时间( **服务时间service time** )之外,还包括网络延迟和排队延迟。延迟是某个请求等待处理的**持续时长**,在此期间它处于 **休眠latent** 状态并等待服务【17】。
即使不断重复发送同样的请求,每次得到的响应时间也都会略有不同。现实世界的系统会处理各式各样的请求,响应时间可能会有很大差异。因此我们需要将响应时间视为一个可以测量的数值**分布distribution**,而不是单个数值。
@ -241,7 +240,7 @@
百分位点通常用于**服务级别目标SLO, service level objectives**和**服务级别协议SLA, service level agreements**,即定义服务预期性能和可用性的合同。 SLA可能会声明如果服务响应时间的中位数小于200毫秒且99.9百分位点低于1秒则认为服务工作正常如果响应时间更长就认为服务不达标。这些指标为客户设定了期望值并允许客户在SLA未达标的情况下要求退款。
**排队延迟queueing delay**通常占了高百分位点处响应时间的很大一部分。由于服务器只能并行处理少量的事务如受其CPU核数的限制所以只要有少量缓慢的请求就能阻碍后续请求的处理这种效应有时被称为**头部阻塞head-of-line blocking**。即使后续请求在服务器上处理的非常迅速,由于需要等待先前请求完成,客户端最终看到的是缓慢的总体响应时间。因为存在这种效应,测量客户端的响应时间非常重要。
**排队延迟queueing delay** 通常占了高百分位点处响应时间的很大一部分。由于服务器只能并行处理少量的事务如受其CPU核数的限制所以只要有少量缓慢的请求就能阻碍后续请求的处理这种效应有时被称为 **头部阻塞head-of-line blocking** 。即使后续请求在服务器上处理的非常迅速,由于需要等待先前请求完成,客户端最终看到的是缓慢的总体响应时间。因为存在这种效应,测量客户端的响应时间非常重要。
为测试系统的可扩展性而人为产生负载时产生负载的客户端要独立于响应时间不断发送请求。如果客户端在发送下一个请求之前等待先前的请求完成这种行为会产生人为排队的效果使得测试时的队列比现实情况更短使测量结果产生偏差【23】。
@ -263,9 +262,9 @@
适应某个级别负载的架构不太可能应付10倍于此的负载。如果你正在开发一个快速增长的服务那么每次负载发生数量级的增长时你可能都需要重新考虑架构——或者更频繁。
人们经常讨论**纵向扩展scaling up****垂直扩展vertical scaling**,转向更强大的机器)和**横向扩展scaling out****水平扩展horizontal scaling**,将负载分布到多台小机器上)之间的对立。跨多台机器分配负载也称为“**无共享shared-nothing**”架构。可以在单台机器上运行的系统通常更简单,但高端机器可能非常贵,所以非常密集的负载通常无法避免地需要横向扩展。现实世界中的优秀架构需要将这两种方法务实地结合,因为使用几台足够强大的机器可能比使用大量的小型虚拟机更简单也更便宜。
人们经常讨论**纵向扩展scaling up****垂直扩展vertical scaling**,转向更强大的机器)和**横向扩展scaling out** **水平扩展horizontal scaling**,将负载分布到多台小机器上)之间的对立。跨多台机器分配负载也称为“**无共享shared-nothing**”架构。可以在单台机器上运行的系统通常更简单,但高端机器可能非常贵,所以非常密集的负载通常无法避免地需要横向扩展。现实世界中的优秀架构需要将这两种方法务实地结合,因为使用几台足够强大的机器可能比使用大量的小型虚拟机更简单也更便宜。
有些系统是**弹性elastic**的,这意味着可以在检测到负载增加时自动增加计算资源,而其他系统则是手动扩展(人工分析容量并决定向系统添加更多的机器)。如果负载**极难预测highly unpredictable**,则弹性系统可能很有用,但手动扩展系统更简单,并且意外操作可能会更少(参阅“[重新平衡分区](ch6.md#分区再平衡)”)。
有些系统是 **弹性elastic** 的,这意味着可以在检测到负载增加时自动增加计算资源,而其他系统则是手动扩展(人工分析容量并决定向系统添加更多的机器)。如果负载**极难预测highly unpredictable**,则弹性系统可能很有用,但手动扩展系统更简单,并且意外操作可能会更少(参阅“[重新平衡分区](ch6.md#分区再平衡)”)。
跨多台机器部署**无状态服务stateless services**非常简单,但将带状态的数据系统从单节点变为分布式配置则可能引入许多额外复杂度。出于这个原因,常识告诉我们应该将数据库放在单个节点上(纵向扩展),直到扩展成本或可用性需求迫使其改为分布式。
@ -334,13 +333,13 @@
### 简单性:管理复杂度
小型软件项目可以使用简单讨喜的、富表现力的代码,但随着项目越来越大,代码往往变得非常复杂,难以理解。这种复杂度拖慢了所有系统相关人员,进一步增加了维护成本。一个陷入复杂泥潭的软件项目有时被描述为**烂泥潭a big ball of mud**【30】。
小型软件项目可以使用简单讨喜的、富表现力的代码,但随着项目越来越大,代码往往变得非常复杂,难以理解。这种复杂度拖慢了所有系统相关人员,进一步增加了维护成本。一个陷入复杂泥潭的软件项目有时被描述为 **烂泥潭a big ball of mud** 【30】。
**复杂度complexity**有各种可能的症状例如状态空间激增、模块间紧密耦合、纠结的依赖关系、不一致的命名和术语、解决性能问题的Hack、需要绕开的特例等等现在已经有很多关于这个话题的讨论【31,32,33】。
**复杂度complexity** 有各种可能的症状例如状态空间激增、模块间紧密耦合、纠结的依赖关系、不一致的命名和术语、解决性能问题的Hack、需要绕开的特例等等现在已经有很多关于这个话题的讨论【31,32,33】。
因为复杂度导致维护困难时,预算和时间安排通常会超支。在复杂的软件中进行变更,引入错误的风险也更大:当开发人员难以理解系统时,隐藏的假设、无意的后果和意外的交互就更容易被忽略。相反,降低复杂度能极大地提高软件的可维护性,因此简单性应该是构建系统的一个关键目标。
简化系统并不一定意味着减少功能;它也可以意味着消除**额外的accidental**的复杂度。 Moseley和Marks【32】把**额外复杂度**定义为:由具体实现中涌现,而非(从用户视角看,系统所解决的)问题本身固有的复杂度。
简化系统并不一定意味着减少功能;它也可以意味着消除**额外的accidental**的复杂度。 Moseley和Marks【32】把 **额外复杂度** 定义为:由具体实现中涌现,而非(从用户视角看,系统所解决的)问题本身固有的复杂度。
用于消除**额外复杂度**的最好工具之一是**抽象abstraction**。一个好的抽象可以将大量实现细节隐藏在一个干净,简单易懂的外观下面。一个好的抽象也可以广泛用于各类不同应用。比起重复造很多轮子,重用抽象不仅更有效率,而且有助于开发高质量的软件。抽象组件的质量改进将使所有使用它的应用受益。
@ -354,11 +353,11 @@
系统的需求永远不变,基本是不可能的。更可能的情况是,它们处于常态的变化中,例如:你了解了新的事实、出现意想不到的应用场景、业务优先级发生变化、用户要求新功能、新平台取代旧平台、法律或监管要求发生变化、系统增长迫使架构变化等。
在组织流程方面,**敏捷agile**工作模式为适应变化提供了一个框架。敏捷社区还开发了对在频繁变化的环境中开发软件很有帮助的技术工具和模式,如**测试驱动开发TDD, test-driven development**和**重构refactoring**。
在组织流程方面, **敏捷agile** 工作模式为适应变化提供了一个框架。敏捷社区还开发了对在频繁变化的环境中开发软件很有帮助的技术工具和模式,如 **测试驱动开发TDD, test-driven development** **重构refactoring**
这些敏捷技术的大部分讨论都集中在相当小的规模同一个应用中的几个代码文件。本书将探索在更大数据系统层面上提高敏捷性的方法可能由几个不同的应用或服务组成。例如为了将装配主页时间线的方法从方法1变为方法2你会如何“重构”推特的架构
修改数据系统并使其适应不断变化需求的容易程度,是与**简单性**和**抽象性**密切相关的:简单易懂的系统通常比复杂系统更容易修改。但由于这是一个非常重要的概念,我们将用一个不同的词来指代数据系统层面的敏捷性:**可演化性evolvability**【34】。
修改数据系统并使其适应不断变化需求的容易程度,是与**简单性**和**抽象性**密切相关的:简单易懂的系统通常比复杂系统更容易修改。但由于这是一个非常重要的概念,我们将用一个不同的词来指代数据系统层面的敏捷性: **可演化性evolvability** 【34】。
@ -456,7 +455,6 @@
------
| 上一章 | 目录 | 下一章 |

25
ch2.md
View File

@ -63,7 +63,7 @@
[^i]: 一个从电子学借用的术语。每个电路的输入和输出都有一定的阻抗(交流电阻)。当你将一个电路的输出连接到另一个电路的输入时,如果两个电路的输出和输入阻抗匹配,则连接上的功率传输将被最大化。阻抗不匹配会导致信号反射及其他问题。
像ActiveRecord和Hibernate这样的**对象关系映射object-relational mapping, ORM**框架可以减少这个转换层所需的样板代码的数量,但是它们不能完全隐藏这两个模型之间的差异。
像ActiveRecord和Hibernate这样的 **对象关系映射(ORM object-relational mapping** 框架可以减少这个转换层所需的样板代码的数量,但是它们不能完全隐藏这两个模型之间的差异。
![](img/fig2-1.png)
@ -138,9 +138,9 @@ JSON表示比[图2-1](img/fig2-1.png)中的多表模式具有更好的**局部
* 本地化支持——当网站翻译成其他语言时,标准化的列表可以被本地化,使得地区和行业可以使用用户的语言来显示
* 更好的搜索——例如搜索华盛顿州的慈善家就会匹配这份简介因为地区列表可以编码记录西雅图在华盛顿这一事实从“Greater Seattle Area”这个字符串中看不出来
存储ID还是文本字符串这是个**副本duplication**问题。当使用ID时对人类有意义的信息比如单词Philanthropy只存储在一处所有引用它的地方使用IDID只在数据库中有意义。当直接存储文本时对人类有意义的信息会复制在每处使用记录中。
存储ID还是文本字符串这是个 **副本duplication** 问题。当使用ID时对人类有意义的信息比如单词Philanthropy只存储在一处所有引用它的地方使用IDID只在数据库中有意义。当直接存储文本时对人类有意义的信息会复制在每处使用记录中。
使用ID的好处是ID对人类没有任何意义因而永远不需要改变ID可以保持不变即使它标识的信息发生变化。任何对人类有意义的东西都可能需要在将来某个时候改变——如果这些信息被复制所有的冗余副本都需要更新。这会导致写入开销也存在不一致的风险一些副本被更新了还有些副本没有被更新。去除此类重复是数据库**规范化normalization**的关键思想。[^ii]
使用ID的好处是ID对人类没有任何意义因而永远不需要改变ID可以保持不变即使它标识的信息发生变化。任何对人类有意义的东西都可能需要在将来某个时候改变——如果这些信息被复制所有的冗余副本都需要更新。这会导致写入开销也存在不一致的风险一些副本被更新了还有些副本没有被更新。去除此类重复是数据库 **规范化normalization** 的关键思想。[^ii]
[^ii]: 关于关系模型的文献区分了几种不同的规范形式,但这些区别几乎没有实际意义。一个经验法则是,如果重复存储了可以存储在一个地方的值,则模式就不是**规范化normalized**的。
@ -201,7 +201,7 @@ CODASYL中的查询是通过利用遍历记录列和跟随访问路径表在数
#### 关系模型
相比之下,关系模型做的就是将所有的数据放在光天化日之下:一个**关系(表)**只是一个**元组(行)**的集合,仅此而已。如果你想读取数据,它没有迷宫似的嵌套结构,也没有复杂的访问路径。你可以选中符合任意条件的行,读取表中的任何或所有行。你可以通过指定某些列作为匹配关键字来读取特定行。你可以在任何表中插入一个新的行,而不必担心与其他表的外键关系[^iv]。
相比之下,关系模型做的就是将所有的数据放在光天化日之下:一个 **关系(表)** 只是一个 **元组(行)** 的集合,仅此而已。如果你想读取数据,它没有迷宫似的嵌套结构,也没有复杂的访问路径。你可以选中符合任意条件的行,读取表中的任何或所有行。你可以通过指定某些列作为匹配关键字来读取特定行。你可以在任何表中插入一个新的行,而不必担心与其他表的外键关系[^iv]。
[^iv]: 外键约束允许对修改约束但对于关系模型这并不是必选项。即使有约束外键连接在查询时执行而在CODASYL中连接在插入时高效完成。
@ -252,7 +252,7 @@ if (user && user.name && !user.first_name) {
}
```
另一方面,在“静态类型”数据库模式中,通常会执行以下**迁移migration**操作:
另一方面,在“静态类型”数据库模式中,通常会执行以下 **迁移migration** 操作:
```sql
ALTER TABLE users ADD COLUMN first_name text;
@ -277,7 +277,7 @@ UPDATE users SET first_name = substring_index(name, ' ', 1); -- MySQL
局部性仅仅适用于同时需要文档绝大部分内容的情况。数据库通常需要加载整个文档即使只访问其中的一小部分这对于大型文档来说是很浪费的。更新文档时通常需要整个重写。只有不改变文档大小的修改才可以容易地原地执行。因此通常建议保持相对小的文档并避免增加文档大小的写入【9】。这些性能限制大大减少了文档数据库的实用场景。
值得指出的是为了局部性而分组集合相关数据的想法并不局限于文档模型。例如Google的Spanner数据库在关系数据模型中提供了同样的局部性属性允许模式声明一个表的行应该交错嵌套在父表内【27】。Oracle类似地允许使用一个称为**多表索引集群表multi-table index cluster tables**的类似特性【28】。Bigtable数据模型用于Cassandra和HBase中的**列族column-family**概念与管理局部性的目的类似【29】。
值得指出的是为了局部性而分组集合相关数据的想法并不局限于文档模型。例如Google的Spanner数据库在关系数据模型中提供了同样的局部性属性允许模式声明一个表的行应该交错嵌套在父表内【27】。Oracle类似地允许使用一个称为 **多表索引集群表multi-table index cluster tables** 的类似特性【28】。Bigtable数据模型用于Cassandra和HBase中的 **列族column-family** 概念与管理局部性的目的类似【29】。
在[第3章](ch3.md)将还会看到更多关于局部性的内容。
@ -299,7 +299,7 @@ UPDATE users SET first_name = substring_index(name, ' ', 1); -- MySQL
## 数据查询语言
当引入关系模型时关系模型包含了一种查询数据的新方法SQL是一种**声明式**查询语言而IMS和CODASYL使用**命令式**代码来查询数据库。那是什么意思?
当引入关系模型时关系模型包含了一种查询数据的新方法SQL是一种 **声明式** 查询语言而IMS和CODASYL使用 **命令式** 代码来查询数据库。那是什么意思?
许多常用的编程语言是命令式的。例如,给定一个动物物种的列表,返回列表中的鲨鱼可以这样写:
@ -387,7 +387,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");
@ -545,11 +545,11 @@ db.observations.aggregate([
在属性图模型中,每个**顶点vertex**包括:
* 唯一的标识符
* 一组**出边outgoing edges**
* 一组**入边ingoing edges**
* 一组 **出边outgoing edges**
* 一组 **入边ingoing edges**
* 一组属性(键值对)
每条**边edge**包括:
每条 **边edge** 包括:
* 唯一标识符
* **边的起点/尾部顶点tail vertex**
@ -698,7 +698,7 @@ WITH RECURSIVE
三元组存储模式大体上与属性图模型相同,用不同的词来描述相同的想法。不过仍然值得讨论,因为三元组存储有很多现成的工具和语言,这些工具和语言对于构建应用程序的工具箱可能是宝贵的补充。
在三元组存储中,所有信息都以非常简单的三部分表示形式存储(**主语****谓语****宾语**)。例如,三元组**(吉姆, 喜欢 ,香蕉)**中,**吉姆**是主语,**喜欢**是谓语(动词),**香蕉**是对象。
在三元组存储中,所有信息都以非常简单的三部分表示形式存储(**主语****谓语****宾语**)。例如,三元组 **(吉姆, 喜欢 ,香蕉)** 中,**吉姆** 是主语,**喜欢** 是谓语(动词),**香蕉** 是对象。
三元组的主语相当于图中的一个顶点。而宾语是下面两者之一:
@ -1037,7 +1037,6 @@ Cypher和SPARQL使用SELECT立即跳转但是Datalog一次只进行一小步
1. Fons Rademakers: “[ROOT for Big Data Analysis](http://indico.cern.ch/getFile.py/access?contribId=13&resId=0&materialId=slides&confId=246453),” at *Workshop on the Future of Big Data Management*,
London, UK, June 2013.
------
| 上一章 | 目录 | 下一章 |

53
ch3.md
View File

@ -19,7 +19,7 @@
特别需要注意,针对**事务**性负载和**分析性**负载优化的存储引擎之间存在巨大差异。稍后我们将在 “[事务处理还是分析?](#事务处理还是分析?)” 一节中探讨这一区别,并在 “[列存储](#列存储)”中讨论一系列针对分析优化存储引擎。
但是我们将从您最可能熟悉的两大类数据库传统关系型数据库与很多所谓的“NoSQL”数据库开始通过介绍它们的**存储引擎**来开始本章的内容。我们会研究两大类存储引擎:**日志结构log-structured**的存储引擎,以及**面向页面page-oriented**的存储引擎例如B树
但是我们将从您最可能熟悉的两大类数据库传统关系型数据库与很多所谓的“NoSQL”数据库开始通过介绍它们的**存储引擎**来开始本章的内容。我们会研究两大类存储引擎:**日志结构log-structured** 的存储引擎,以及**面向页面page-oriented** 的存储引擎例如B树
## 驱动数据库的数据结构
@ -63,9 +63,9 @@ $ cat database
42,{"name":"San Francisco","attractions":["Exploratorium"]}
```
`db_set` 函数对于极其简单的场景其实有非常好的性能,因为在文件尾部追加写入通常是非常高效的。与`db_set`做的事情类似,许多数据库在内部使用了**日志log**,也就是一个**仅追加append-only**的数据文件。真正的数据库有更多的问题需要处理(如并发控制,回收磁盘空间以避免日志无限增长,处理错误与部分写入的记录),但基本原理是一样的。日志极其有用,我们还将在本书的其它部分重复见到它好几次。
`db_set` 函数对于极其简单的场景其实有非常好的性能,因为在文件尾部追加写入通常是非常高效的。与`db_set`做的事情类似,许多数据库在内部使用了**日志log**,也就是一个 **仅追加append-only** 的数据文件。真正的数据库有更多的问题需要处理(如并发控制,回收磁盘空间以避免日志无限增长,处理错误与部分写入的记录),但基本原理是一样的。日志极其有用,我们还将在本书的其它部分重复见到它好几次。
> **日志log**这个词通常指应用日志:即应用程序输出的描述发生事情的文本。本书在更普遍的意义下使用**日志**这一词:一个仅追加的记录序列。它可能压根就不是给人类看的,使用二进制格式,并仅能由其他程序读取。
> **日志log** 这个词通常指应用日志:即应用程序输出的描述发生事情的文本。本书在更普遍的意义下使用**日志**这一词:一个仅追加的记录序列。它可能压根就不是给人类看的,使用二进制格式,并仅能由其他程序读取。
另一方面,如果这个数据库中有着大量记录,则这个`db_get` 函数的性能会非常糟糕。每次你想查找一个键时,`db_get` 必须从头到尾扫描整个数据库文件来查找键的出现。用算法的语言来说,查找的开销是 `O(n)` :如果数据库记录数量 n 翻了一倍,查找时间也要翻一倍。这就不好了。
@ -79,11 +79,11 @@ $ cat database
### 哈希索引
让我们从**键值数据key-value Data**的索引开始。这不是您可以索引的唯一数据类型,但键值数据是很常见的。对于更复杂的索引来说,这是一个有用的构建模块。
让我们从 **键值数据key-value Data** 的索引开始。这不是您可以索引的唯一数据类型,但键值数据是很常见的。对于更复杂的索引来说,这是一个有用的构建模块。
键值存储与在大多数编程语言中可以找到的**字典dictionary**类型非常相似,通常字典都是用**散列映射hash map**(或**哈希表hash table**实现的。哈希映射在许多算法教科书中都有描述【1,2】所以这里我们不会讨论它的工作细节。既然我们已经有**内存中**数据结构 —— 哈希映射,为什么不使用它来索引在**磁盘上**的数据呢?
假设我们的数据存储只是一个追加写入的文件,就像前面的例子一样。那么最简单的索引策略就是:保留一个内存中的哈希映射,其中每个键都映射到一个数据文件中的字节偏移量,指明了可以找到对应值的位置,如[图3-1](img/fig3-1.png)所示。当你将新的键值对追加写入文件中时,还要更新散列映射,以反映刚刚写入的数据的偏移量(这同时适用于插入新键与更新现有键)。当你想查找一个值时,使用哈希映射来查找数据文件中的偏移量,**寻找seek**该位置并读取该值。
假设我们的数据存储只是一个追加写入的文件,就像前面的例子一样。那么最简单的索引策略就是:保留一个内存中的哈希映射,其中每个键都映射到一个数据文件中的字节偏移量,指明了可以找到对应值的位置,如[图3-1](img/fig3-1.png)所示。当你将新的键值对追加写入文件中时,还要更新散列映射,以反映刚刚写入的数据的偏移量(这同时适用于插入新键与更新现有键)。当你想查找一个值时,使用哈希映射来查找数据文件中的偏移量,**寻找seek** 该位置并读取该值。
![](img/fig3-1.png)
@ -130,7 +130,7 @@ $ cat database
乍一看,只有追加日志看起来很浪费:为什么不更新文件,用新值覆盖旧值?但是只能追加设计的原因有几个:
* 追加和分段合并是顺序写入操作,通常比随机写入快得多,尤其是在磁盘旋转硬盘上。在某种程度上,顺序写入在基于闪存的**固态硬盘SSD**上也是优选的【4】。我们将在第83页的“[比较B-树和LSM-树](#比较B-树和LSM-树)”中进一步讨论这个问题。
* 追加和分段合并是顺序写入操作,通常比随机写入快得多,尤其是在磁盘旋转硬盘上。在某种程度上,顺序写入在基于闪存的 **固态硬盘SSD** 上也是优选的【4】。我们将在第83页的“[比较B-树和LSM-树](#比较B-树和LSM-树)”中进一步讨论这个问题。
* 如果段文件是附加的或不可变的,并发和崩溃恢复就简单多了。例如,您不必担心在覆盖值时发生崩溃的情况,而将包含旧值和新值的一部分的文件保留在一起。
* 合并旧段可以避免数据文件随着时间的推移而分散的问题。
@ -297,7 +297,7 @@ B树在数据库体系结构中是非常根深蒂固的为许多工作负载
### 其他索引结构
到目前为止,我们只讨论了关键值索引,它们就像关系模型中的**主键primary key**索引。主键唯一标识关系表中的一行或文档数据库中的一个文档或图形数据库中的一个顶点。数据库中的其他记录可以通过其主键或ID引用该行/文档/顶点,并且索引用于解析这样的引用。
到目前为止,我们只讨论了关键值索引,它们就像关系模型中的**主键primary key** 索引。主键唯一标识关系表中的一行或文档数据库中的一个文档或图形数据库中的一个顶点。数据库中的其他记录可以通过其主键或ID引用该行/文档/顶点,并且索引用于解析这样的引用。
有二级索引也很常见。在关系数据库中,您可以使用 `CREATE INDEX` 命令在同一个表上创建多个二级索引,而且这些索引通常对于有效地执行联接而言至关重要。例如,在[第2章](ch2.md)中的[图2-1](img/fig2-1.png)中,很可能在 `user_id` 列上有一个二级索引,以便您可以在每个表中找到属于同一用户的所有行。
@ -310,7 +310,7 @@ B树在数据库体系结构中是非常根深蒂固的为许多工作负载
在某些情况下从索引到堆文件的额外跳跃对读取来说性能损失太大因此可能希望将索引行直接存储在索引中。这被称为聚集索引。例如在MySQL的InnoDB存储引擎中表的主键总是一个聚簇索引二级索引用主键而不是堆文件中的位置【31】。在SQL Server中可以为每个表指定一个聚簇索引【32】。
在**聚集索引clustered index**(在索引中存储所有行数据)和**非聚集索引nonclustered index**(仅在索引中存储对数据的引用)之间的折衷被称为**包含列的索引index with included columns**或**覆盖索引covering index**其存储表的一部分在索引内【33】。这允许通过单独使用索引来回答一些查询这种情况叫做索引**覆盖cover**了查询【32】。
**聚集索引clustered index** (在索引中存储所有行数据)和 **非聚集索引nonclustered index** (仅在索引中存储对数据的引用)之间的折衷被称为 **包含列的索引index with included columns**或**覆盖索引covering index**其存储表的一部分在索引内【33】。这允许通过单独使用索引来回答一些查询这种情况叫做索引 **覆盖cover** 了查询【32】。
与任何类型的数据重复一样,聚簇和覆盖索引可以加快读取速度,但是它们需要额外的存储空间,并且会增加写入开销。数据库还需要额外的努力来执行事务保证,因为应用程序不应该因为重复而导致不一致。
@ -318,9 +318,9 @@ B树在数据库体系结构中是非常根深蒂固的为许多工作负载
至今讨论的索引只是将一个键映射到一个值。如果我们需要同时查询一个表中的多个列(或文档中的多个字段),这显然是不够的。
最常见的多列索引被称为**连接索引concatenated index**,它通过将一列的值追加到另一列后面,简单地将多个字段组合成一个键(索引定义中指定了字段的连接顺序)。这就像一个老式的纸质电话簿,它提供了一个从(姓,名)到电话号码的索引。由于排序顺序,索引可以用来查找所有具有特定姓氏的人,或所有具有特定姓-名组合的人。**然而,如果你想找到所有具有特定名字的人,这个索引是没有用的**。
最常见的多列索引被称为 **连接索引concatenated index** ,它通过将一列的值追加到另一列后面,简单地将多个字段组合成一个键(索引定义中指定了字段的连接顺序)。这就像一个老式的纸质电话簿,它提供了一个从(姓,名)到电话号码的索引。由于排序顺序,索引可以用来查找所有具有特定姓氏的人,或所有具有特定姓-名组合的人。**然而,如果你想找到所有具有特定名字的人,这个索引是没有用的**。
**多维索引multi-dimensional index**是一种查询多个列的更一般的方法,这对于地理空间数据尤为重要。例如,餐厅搜索网站可能有一个数据库,其中包含每个餐厅的经度和纬度。当用户在地图上查看餐馆时,网站需要搜索用户正在查看的矩形地图区域内的所有餐馆。这需要一个二维范围查询,如下所示:
**多维索引multi-dimensional index** 是一种查询多个列的更一般的方法,这对于地理空间数据尤为重要。例如,餐厅搜索网站可能有一个数据库,其中包含每个餐厅的经度和纬度。当用户在地图上查看餐馆时,网站需要搜索用户正在查看的矩形地图区域内的所有餐馆。这需要一个二维范围查询,如下所示:
```sql
SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
@ -359,9 +359,9 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
除了性能内存数据库的另一个有趣的领域是提供难以用基于磁盘的索引实现的数据模型。例如Redis为各种数据结构如优先级队列和集合提供了类似数据库的接口。因为它将所有数据保存在内存中所以它的实现相对简单。
最近的研究表明内存数据库体系结构可以扩展到支持比可用内存更大的数据集而不必重新采用以磁盘为中心的体系结构【45】。所谓的**反缓存anti-caching**方法通过在内存不足的情况下将最近最少使用的数据从内存转移到磁盘并在将来再次访问时将其重新加载到内存中。这与操作系统对虚拟内存和交换文件的操作类似但数据库可以比操作系统更有效地管理内存因为它可以按单个记录的粒度工作而不是整个内存页面。尽管如此这种方法仍然需要索引能完全放入内存中就像本章开头的Bitcask例子
最近的研究表明内存数据库体系结构可以扩展到支持比可用内存更大的数据集而不必重新采用以磁盘为中心的体系结构【45】。所谓的 **反缓存anti-caching** 方法通过在内存不足的情况下将最近最少使用的数据从内存转移到磁盘并在将来再次访问时将其重新加载到内存中。这与操作系统对虚拟内存和交换文件的操作类似但数据库可以比操作系统更有效地管理内存因为它可以按单个记录的粒度工作而不是整个内存页面。尽管如此这种方法仍然需要索引能完全放入内存中就像本章开头的Bitcask例子
如果**非易失性存储器NVM**技术得到更广泛的应用可能还需要进一步改变存储引擎设计【46】。目前这是一个新的研究领域值得关注。
如果 **非易失性存储器NVM** 技术得到更广泛的应用可能还需要进一步改变存储引擎设计【46】。目前这是一个新的研究领域值得关注。
@ -370,13 +370,13 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
在业务数据处理的早期,对数据库的写入通常对应于正在进行的商业交易:进行销售,向供应商下订单,支付员工工资等等。随着数据库扩展到那些没有不涉及钱易手,术语交易仍然卡住,指的是形成一个逻辑单元的一组读写。
事务不一定具有ACID原子性一致性隔离性和持久性属性。事务处理只是意味着允许客户端进行低延迟读取和写入 —— 而不是批量处理作业,而这些作业只能定期运行(例如每天一次)。我们在[第7章](ch7.md)中讨论ACID属性在[第10章](ch10.md)中讨论批处理。
即使数据库开始被用于许多不同类型的博客文章,游戏中的动作,地址簿中的联系人等等,基本访问模式仍然类似于处理业务事务。应用程序通常使用索引通过某个键查找少量记录。根据用户的输入插入或更新记录。由于这些应用程序是交互式的,因此访问模式被称为**在线事务处理OLTP, OnLine Transaction Processing**。
即使数据库开始被用于许多不同类型的博客文章,游戏中的动作,地址簿中的联系人等等,基本访问模式仍然类似于处理业务事务。应用程序通常使用索引通过某个键查找少量记录。根据用户的输入插入或更新记录。由于这些应用程序是交互式的,因此访问模式被称为 **在线事务处理OLTP, OnLine Transaction Processing**
但是,数据库也开始越来越多地用于数据分析,这些数据分析具有非常不同的访问模式。通常,分析查询需要扫描大量记录,每个记录只读取几列,并计算汇总统计信息(如计数,总和或平均值),而不是将原始数据返回给用户。例如,如果您的数据是一个销售交易表,那么分析查询可能是:
* 一月份我们每个商店的总收入是多少?
* 我们在最近的推广活动中销售多少香蕉?
* 哪种品牌的婴儿食品最常与X品牌的尿布一起购买?
* 一月份每个商店的总收入是多少?
* 在最近的推广活动中卖了多少香蕉?
* 哪个牌子的婴儿食品最常与X品牌的尿布同时购买?
这些查询通常由业务分析师编写,并提供给帮助公司管理层做出更好决策(商业智能)的报告。为了区分这种使用数据库的事务处理模式,它被称为**在线分析处理OLAP, OnLine Analytice Processing**。【47】。OLTP和OLAP之间的区别并不总是清晰的但是一些典型的特征在[表3-1]()中列出。
@ -394,11 +394,11 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
### 数据仓库
一个企业可能有几十个不同的交易处理系统:系统为面向客户的网站提供动力,控制实体商店的**销售点checkout**系统,跟踪仓库中的库存,规划车辆路线,管理供应商,管理员工等。这些系统中的每一个都是复杂的,需要一个人员去维护,所以系统最终都是自动运行的。
一个企业可能有几十个不同的交易处理系统:面向终端客户的网站,控制实体商店的收银系统,跟踪仓库库存,规划车辆路线,供应链管理,员工管理等。这些系统中每一个都很复杂,需要专人维护,所以系统最终都是自动运行的。
这些OLTP系统通常具有高度的可用性并以低延迟处理事务因为这些系统往往对业务运作至关重要。因此数据库管理员密切关注他们的OLTP数据库他们通常不愿意让业务分析人员在OLTP数据库上运行临时分析查询因为这些查询通常很昂贵,扫描大部分数据集,这会损害同时执行的事务的性能。
这些OLTP系统往往对业务运作至关重要,因而通常会要求 **高可用****低延迟**。所以DBA会密切关注他们的OLTP数据库他们通常不愿意让业务分析人员在OLTP数据库上运行临时分析查询因为这些查询通常开销巨大,会扫描大部分数据集,这会损害同时执行的事务的性能。
相比之下,数据仓库是一个独立的数据库,分析人员可以查询他们心中的内容,而不影响OLTP操作【48】。数据仓库包含公司所有各种OLTP系统中的只读数据副本。从OLTP数据库中提取数据使用定期的数据转储或连续的更新流转换成适合分析的模式清理并加载到数据仓库中。将数据存入仓库的过程称为“**抽取-转换-加载ETL**”,如[图3-8](img/fig3-8)所示。
相比之下,数据仓库是一个独立的数据库,分析人员可以查询他们想要的内容而不影响OLTP操作【48】。数据仓库包含公司各种OLTP系统中所有的只读数据副本。从OLTP数据库中提取数据使用定期的数据转储或连续的更新流转换成适合分析的模式清理并加载到数据仓库中。将数据存入仓库的过程称为“**抽取-转换-加载ETL**”,如[图3-8](img/fig3-8)所示。
![](img/fig3-8.png)
@ -406,7 +406,7 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
几乎所有的大型企业都有数据仓库但在小型企业中几乎闻所未闻。这可能是因为大多数小公司没有这么多不同的OLTP系统大多数小公司只有少量的数据 —— 可以在传统的SQL数据库中查询甚至可以在电子表格中分析。在一家大公司里要做一些在一家小公司很简单的事情需要很多繁重的工作。
使用单独的数据仓库而不是直接查询OLTP系统进行分析的一大优势是数据仓库可针对分析访问模式进行优化。事实证明本章前半部分讨论的索引算法对于OLTP来说工作得很好但对于回答分析查询并不是很好。在本章的其余部分中我们将看看为分析而优化的存储引擎。
使用单独的数据仓库而不是直接查询OLTP系统进行分析的一大优势是数据仓库可针对分析访问模式进行优化。事实证明本章前半部分讨论的索引算法对于OLTP来说工作得很好但对于回答分析查询并不是很好。在本章的其余部分中我们将研究为分析而优化的存储引擎。
#### OLTP数据库和数据仓库之间的分歧
@ -588,20 +588,20 @@ WHERE product_sk = 31 AND store_sk = 3
在本章中,我们试图深入了解数据库如何处理存储和检索。将数据存储在数据库中会发生什么,以及稍后再次查询数据时数据库会做什么?
在高层次上,我们看到存储引擎分为两大类:优化**事务处理OLTP**和**优化分析OLAP**的类别。这些用例的访问模式之间有很大的区别:
在高层次上,我们看到存储引擎分为两大类:优化 **事务处理OLTP****在线分析OLAP** 。这些用例的访问模式之间有很大的区别:
* OLTP系统通常面向用户这意味着他们可能会看到大量的请求。为了处理负载,应用程序通常只触及每个查询中的少量记录。应用程序使用某种键来请求记录,存储引擎使用索引来查找所请求的键的数据。磁盘寻道时间往往是这里的瓶颈。
* 数据仓库和类似的分析系统不太知名因为它们主要由业务分析人员使用而不是由最终用户使用。它们处理比OLTP系统少得多的查询量但是每个查询通常要求很高,需要在短时间内扫描数百万条记录。磁盘带宽(不是查找时间)往往是瓶颈,列式存储是这种工作负载越来越流行的解决方案。
* OLTP系统通常面向用户这意味着系统可能会收到大量的请求。为了处理负载,应用程序通常只访问每个查询中的少部分记录。应用程序使用某种键来请求记录,存储引擎使用索引来查找所请求的键的数据。磁盘寻道时间往往是这里的瓶颈。
* 数据仓库和类似的分析系统会低调一些因为它们主要由业务分析人员使用而不是由最终用户使用。它们的查询量要比OLTP系统少得多但通常每个查询开销高昂,需要在短时间内扫描数百万条记录。磁盘带宽(不是查找时间)往往是瓶颈,列式存储是这种工作负载越来越流行的解决方案。
在OLTP方面我们看到了来自两大主流学派的存储引擎:
在OLTP方面我们能看到两派主流的存储引擎:
***日志结构学派***
只允许附加到文件和删除过时的文件,但不会更新已经写入的文件。 BitcaskSSTablesLSM树LevelDBCassandraHBaseLucene等都属于这个
只允许附加到文件和删除过时的文件,但不会更新已经写入的文件。 BitcaskSSTablesLSM树LevelDBCassandraHBaseLucene等都属于这个类别
***就地更新学派***
将磁盘视为一组可以覆盖的固定大小的页面。 B树是这种哲学的最大的例子被用在所有主要的关系数据库中还有许多非关系数据库。
将磁盘视为一组可以覆写的固定大小的页面。 B树是这种哲学的典范用在所有主要的关系数据库中和许多非关系型数据库。
日志结构的存储引擎是相对较新的发展。他们的主要想法是他们系统地将随机访问写入顺序写入磁盘由于硬盘驱动器和固态硬盘的性能特点可以实现更高的写入吞吐量。在完成OLTP方面我们通过一些更复杂的索引结构和为保留所有数据而优化的数据库做了一个简短的介绍。
@ -757,7 +757,6 @@ WHERE product_sk = 31 AND store_sk = 3
------
| 上一章 | 目录 | 下一章 |

25
ch4.md
View File

@ -19,8 +19,8 @@
当数据**格式format**或**模式schema**发生变化时,通常需要对应用程序代码进行相应的更改(例如,为记录添加新字段,然后修改程序开始读写该字段)。但在大型应用程序中,代码变更通常不会立即完成:
* 对于**服务端server-side**应用程序,可能需要执行**滚动升级 rolling upgrade**(也称为**阶段发布staged rollout**),一次将新版本部署到少数几个节点,检查新版本是否运行正常,然后逐渐部完所有的节点。这样无需中断服务即可部署新版本,为频繁发布提供了可行性,从而带来更好的可演化性。
* 对于**客户端client-side**应用程序,升不升级就要看用户的心情了。用户可能相当长一段时间里都不会去升级软件。
* 对于 **服务端server-side** 应用程序,可能需要执行 **滚动升级 rolling upgrade** (也称为 **阶段发布staged rollout** ),一次将新版本部署到少数几个节点,检查新版本是否运行正常,然后逐渐部完所有的节点。这样无需中断服务即可部署新版本,为频繁发布提供了可行性,从而带来更好的可演化性。
* 对于 **客户端client-side** 应用程序,升不升级就要看用户的心情了。用户可能相当长一段时间里都不会去升级软件。
这意味着,新旧版本的代码,以及新旧数据格式可能会在系统中同时共处。系统想要继续顺利运行,就需要保持**双向兼容性**
@ -43,17 +43,17 @@
程序通常(至少)使用两种形式的数据:
1. 在内存中,数据保存在对象,结构体,列表,数组,哈希表,树等中。 这些数据结构针对CPU的高效访问和操作进行了优化通常使用指针
2. 如果要将数据写入文件,或通过网络发送,则必须将其**编码encode**为某种自包含的字节序列例如JSON文档。 由于每个进程都有自己独立的地址空间,一个进程中的指针对任何其他进程都没有意义,所以这个字节序列表示会与通常在内存中使用的数据结构完全不同[^i]。
2. 如果要将数据写入文件,或通过网络发送,则必须将其 **编码encode** 为某种自包含的字节序列例如JSON文档。 由于每个进程都有自己独立的地址空间,一个进程中的指针对任何其他进程都没有意义,所以这个字节序列表示会与通常在内存中使用的数据结构完全不同[^i]。
[^i]: 除一些特殊情况外,例如某些内存映射文件或直接在压缩数据上操作(如“[列压缩](ch4.md#列压缩)”中所述)。
所以,需要在两种表示之间进行某种类型的翻译。 从内存中表示到字节序列的转换称为**编码Encoding**(也称为**序列化serialization**或**编组marshalling**),反过来称为**解码Decoding**[^ii]**解析Parsing****反序列化deserialization****反编组( unmarshalling**[^译i]。
所以,需要在两种表示之间进行某种类型的翻译。 从内存中表示到字节序列的转换称为 **编码Encoding** (也称为**序列化serialization**或**编组marshalling**),反过来称为**解码Decoding**[^ii]**解析Parsing****反序列化deserialization****反编组( unmarshalling**[^译i]。
[^ii]: 请注意,**编码encode**与**加密encryption**无关。 本书不讨论加密。
[^ii]: 请注意,**编码encode** **加密encryption** 无关。 本书不讨论加密。
[^译i]: Marshal与Serialization的区别Marshal不仅传输对象的状态而且会一起传输对象的方法相关代码
> #### 术语冲突
> 不幸的是,在[第七章](ch7.md)**事务Transaction**的上下文里,**序列化Serialization**这个术语也出现了,而且具有完全不同的含义。尽管序列化可能是更常见的术语,为了避免术语重载,本书中坚持使用**编码Encoding**表达此含义。
> 不幸的是,在[第七章](ch7.md) **事务Transaction** 的上下文里,**序列化Serialization** 这个术语也出现了,而且具有完全不同的含义。尽管序列化可能是更常见的术语,为了避免术语重载,本书中坚持使用 **编码Encoding** 表达此含义。
这是一个常见的问题,因而有许多库和编码格式可供选择。 首先让我们概览一下。
@ -120,7 +120,7 @@ JSON比XML简洁但与二进制格式一比还是太占地方。这一事
### Thrift与Protocol Buffers
Apache Thrift 【15】和Protocol Buffersprotobuf【16】是基于相同原理的二进制编码库。 Protocol Buffers最初是在Google开发的Thrift最初是在Facebook开发的并且在2007~2008年都是开源的【17】。
Thrift和Protocol Buffers都需要一个模式来编码任何数据。要在Thrift的[例4-1]()中对数据进行编码可以使用Thrift**接口定义语言IDL**来描述模式,如下所示:
Thrift和Protocol Buffers都需要一个模式来编码任何数据。要在Thrift的[例4-1]()中对数据进行编码可以使用Thrift **接口定义语言IDL** 来描述模式,如下所示:
```c
struct Person {
@ -175,7 +175,7 @@ Thrift CompactProtocol编码在语义上等同于BinaryProtocol但是如[图4
您可以添加新的字段到架构,只要您给每个字段一个新的标签号码。如果旧的代码(不知道你添加的新的标签号码)试图读取新代码写入的数据,包括一个新的字段,其标签号码不能识别,它可以简单地忽略该字段。数据类型注释允许解析器确定需要跳过的字节数。这保持了前向兼容性:旧代码可以读取由新代码编写的记录。
向后兼容性呢?只要每个字段都有一个唯一的标签号码,新的代码总是可以读取旧的数据,因为标签号码仍然具有相同的含义。唯一的细节是,如果你添加一个新的领域,你不能要求。如果您要添加一个字段并将其设置为必需,那么如果新代码读取旧代码写入的数据,则该检查将失败,因为旧代码不会写入您添加的新字段。因此,为了保持向后兼容性,在模式的初始部署之后**添加的每个字段必须是可选的或具有默认值**。
向后兼容性呢?只要每个字段都有一个唯一的标签号码,新的代码总是可以读取旧的数据,因为标签号码仍然具有相同的含义。唯一的细节是,如果你添加一个新的领域,你不能要求。如果您要添加一个字段并将其设置为必需,那么如果新代码读取旧代码写入的数据,则该检查将失败,因为旧代码不会写入您添加的新字段。因此,为了保持向后兼容性,在模式的初始部署之后 **添加的每个字段必须是可选的或具有默认值**
删除一个字段就像添加一个字段,倒退和向前兼容性问题相反。这意味着您只能删除一个可选的字段(必填字段永远不能删除),而且您不能再次使用相同的标签号码(因为您可能仍然有数据写在包含旧标签号码的地方,而该字段必须被新代码忽略)。
@ -380,7 +380,7 @@ Web以这种方式工作客户Web浏览器向Web服务器发出请求
Web浏览器不是唯一的客户端类型。例如在移动设备或桌面计算机上运行的本地应用程序也可以向服务器发出网络请求并且在Web浏览器内运行的客户端JavaScript应用程序可以使用XMLHttpRequest成为HTTP客户端该技术被称为Ajax 【30】。在这种情况下服务器的响应通常不是用于显示给人的HTML而是用于便于客户端应用程序代码如JSON进一步处理的编码数据。尽管HTTP可能被用作传输协议但顶层实现的API是特定于应用程序的客户端和服务器需要就该API的细节达成一致。
此外服务器本身可以是另一个服务的客户端例如典型的Web应用服务器充当数据库的客户端。这种方法通常用于将大型应用程序按照功能区域分解为较小的服务这样当一个服务需要来自另一个服务的某些功能或数据时就会向另一个服务发出请求。这种构建应用程序的方式传统上被称为**面向服务的体系结构service-oriented architectureSOA**,最近被改进和更名为**微服务架构 **【31,32】。
此外服务器本身可以是另一个服务的客户端例如典型的Web应用服务器充当数据库的客户端。这种方法通常用于将大型应用程序按照功能区域分解为较小的服务这样当一个服务需要来自另一个服务的某些功能或数据时就会向另一个服务发出请求。这种构建应用程序的方式传统上被称为 **面向服务的体系结构service-oriented architectureSOA** ,最近被改进和更名为 **微服务架构 ** 【31,32】。
在某些方面服务类似于数据库它们通常允许客户端提交和查询数据。但是虽然数据库允许使用我们在第2章 中讨论的查询语言进行任意查询但是服务公开了一个特定于应用程序的API它只允许由服务的业务逻辑应用程序代码预定的输入和输出【33】。这种限制提供了一定程度的封装服务可以对客户可以做什么和不可以做什么施加细粒度的限制。
@ -391,7 +391,7 @@ Web浏览器不是唯一的客户端类型。例如在移动设备或桌面
**当服务使用HTTP作为底层通信协议时可称之为Web服务**。这可能是一个小错误因为Web服务不仅在Web上使用而且在几个不同的环境中使用。例如
1. 运行在用户设备上的客户端应用程序例如移动设备上的本地应用程序或使用Ajax的JavaScript web应用程序通过HTTP向服务发出请求。这些请求通常通过公共互联网进行。
2. 一种服务向同一组织拥有的另一项服务提出请求,这些服务通常位于同一数据中心内,作为面向服务/微型架构的一部分。 (支持这种用例的软件有时被称为**中间件middleware**。
2. 一种服务向同一组织拥有的另一项服务提出请求,这些服务通常位于同一数据中心内,作为面向服务/微型架构的一部分。 (支持这种用例的软件有时被称为 **中间件middleware**
3. 一种服务通过互联网向不同组织所拥有的服务提出请求。这用于不同组织后端系统之间的数据交换。此类别包括由在线服务如信用卡处理系统提供的公共API或用于共享访问用户数据的OAuth。
有两种流行的Web服务方法REST和SOAP。他们在哲学方面几乎是截然相反的往往是各自支持者之间的激烈辩论即使在每个阵营内也有很多争论。 例如,**HATEOAS超媒体作为应用程序状态的引擎**经常引发讨论【35】。
@ -412,11 +412,11 @@ REST风格的API倾向于更简单的方法通常涉及较少的代码生成
Web服务仅仅是通过网络进行API请求的一系列技术的最新版本其中许多技术受到了大量的炒作但是存在严重的问题。 Enterprise JavaBeansEJB和Java的**远程方法调用RMI**仅限于Java。**分布式组件对象模型DCOM**仅限于Microsoft平台。**公共对象请求代理体系结构CORBA**过于复杂不提供前向或后向兼容性【41】。
所有这些都是基于**远程过程调用RPC**的思想该过程调用自20世纪70年代以来一直存在【42】。 RPC模型试图向远程网络服务发出请求看起来与在同一进程中调用编程语言中的函数或方法相同这种抽象称为位置透明。尽管RPC起初看起来很方便但这种方法根本上是有缺陷的【43,44】。网络请求与本地函数调用非常不同
所有这些都是基于 **远程过程调用RPC** 的思想该过程调用自20世纪70年代以来一直存在【42】。 RPC模型试图向远程网络服务发出请求看起来与在同一进程中调用编程语言中的函数或方法相同这种抽象称为位置透明。尽管RPC起初看起来很方便但这种方法根本上是有缺陷的【43,44】。网络请求与本地函数调用非常不同
* 本地函数调用是可预测的,并且成功或失败,这仅取决于受您控制的参数。网络请求是不可预知的:由于网络问题,请求或响应可能会丢失,或者远程计算机可能很慢或不可用,这些问题完全不在您的控制范围之内。网络问题是常见的,所以你必须预测他们,例如通过重试失败的请求。
* 本地函数调用要么返回结果,要么抛出异常,或者永远不返回(因为进入无限循环或进程崩溃)。网络请求有另一个可能的结果:由于超时,它可能会返回没有结果。在这种情况下,你根本不知道发生了什么:如果你没有得到来自远程服务的响应,你无法知道请求是否通过。 (我们将在[第8章](ch8.md)更详细地讨论这个问题。)
* 如果您重试失败的网络请求,可能会发生请求实际上正在通过,只有响应丢失。在这种情况下,重试将导致该操作被执行多次,除非您在协议中引入除重(**幂等idempotence**)机制。本地函数调用没有这个问题。 (在[第十一章](ch11.md)更详细地讨论幂等性)
* 如果您重试失败的网络请求,可能会发生请求实际上正在通过,只有响应丢失。在这种情况下,重试将导致该操作被执行多次,除非您在协议中引入除重( **幂等idempotence**)机制。本地函数调用没有这个问题。 (在[第十一章](ch11.md)更详细地讨论幂等性)
* 每次调用本地功能时,通常需要大致相同的时间来执行。网络请求比函数调用要慢得多,而且其延迟也是非常可变的:在不到一毫秒的时间内它可能会完成,但是当网络拥塞或者远程服务超载时,可能需要几秒钟的时间完全一样的东西。
* 调用本地函数时,可以高效地将引用(指针)传递给本地内存中的对象。当你发出一个网络请求时,所有这些参数都需要被编码成可以通过网络发送的一系列字节。没关系,如果参数是像数字或字符串这样的基本类型,但是对于较大的对象很快就会变成问题。
@ -633,7 +633,6 @@ Actor模型是单个进程中并发的编程模型。逻辑被封装在角色中
1. Fred Hebert: “[Postscript: Maps](http://learnyousomeerlang.com/maps),” *learnyousomeerlang.com*, April 9, 2014.
------
| 上一章 | 目录 | 下一章 |

32
ch5.md
View File

@ -26,15 +26,15 @@
## 领导者与追随者
存储数据库副本的每个节点称为**副本replica**。当存在多个副本时,会不可避免的出现一个问题:如何确保所有数据都落在了所有的副本上?
存储数据库副本的每个节点称为 **副本replica** 。当存在多个副本时,会不可避免的出现一个问题:如何确保所有数据都落在了所有的副本上?
每一次向数据库的写入操作都需要传播到所有副本上,否则副本就会包含不一样的数据。最常见的解决方案被称为**基于领导者的复制leader-based replication**(也称**主动/被动active/passive** 或 **主/从master/slave**复制),如[图5-1](#fig5-1.png)所示。它的工作原理如下:
每一次向数据库的写入操作都需要传播到所有副本上,否则副本就会包含不一样的数据。最常见的解决方案被称为 **基于领导者的复制leader-based replication** (也称**主动/被动active/passive** 或 **主/从master/slave**复制),如[图5-1](#fig5-1.png)所示。它的工作原理如下:
1. 副本之一被指定为**领导者leader**,也称为 **主库master** **首要primary**。当客户端要向数据库写入时,它必须将请求发送给**领导者**,领导者会将新数据写入其本地存储。
2. 其他副本被称为**追随者followers**,亦称为**只读副本read replicas****从库slaves****次要 sencondaries****热备hot-standby**[^i]。每当领导者将新数据写入本地存储时,它也会将数据变更发送给所有的追随者,称之为**复制日志replication log**记录或**变更流change stream**。每个跟随者从领导者拉取日志,并相应更新其本地数据库副本,方法是按照领导者处理的相同顺序应用所有写入。
1. 副本之一被指定为 **领导者leader**,也称为 **主库master|primary** 。当客户端要向数据库写入时,它必须将请求发送给**领导者**,领导者会将新数据写入其本地存储。
2. 其他副本被称为**追随者followers**,亦称为**只读副本read replicas****从库slaves****备库 sencondaries****热备hot-standby**[^i]。每当领导者将新数据写入本地存储时,它也会将数据变更发送给所有的追随者,称之为**复制日志replication log**记录或**变更流change stream**。每个跟随者从领导者拉取日志,并相应更新其本地数据库副本,方法是按照领导者处理的相同顺序应用所有写入。
3. 当客户想要从数据库中读取数据时,它可以向领导者或追随者查询。 但只有领导者才能接受写操作(从客户端的角度来看从库都是只读的)。
[^i]: 不同的人对**热hot****温warn****冷cold** 备份服务器有不同的定义。 例如在PostgreSQL中**热备hot standby**指的是能接受客户端读请求的副本。而**温备warm standby**只是追随领导者,但不处理客户端的任何查询。 就本书而言,这些差异并不重要。
[^i]: 不同的人对 **热hot****温warn** **冷cold** 备份服务器有不同的定义。 例如在PostgreSQL中**热备hot standby**指的是能接受客户端读请求的副本。而**温备warm standby**只是追随领导者,但不处理客户端的任何查询。 就本书而言,这些差异并不重要。
![](img/fig5-1.png)
**图5-1 基于领导者(主-从)的复制**
@ -58,9 +58,9 @@
同步复制的优点是,从库保证有与主库一致的最新数据副本。如果主库突然失效,我们可以确信这些数据仍然能在从库上上找到。缺点是,如果同步从库没有响应(比如它已经崩溃,或者出现网络故障,或其它任何原因),主库就无法处理写入操作。主库必须阻止所有写入,并等待同步副本再次可用。
因此,将所有从库都设置为同步的是不切实际的:任何一个节点的中断都会导致整个系统停滞不前。实际上,如果在数据库上启用同步复制,通常意味着其中**一个**跟随者是同步的,而其他的则是异步的。如果同步从库变得不可用或缓慢,则使一个异步从库同步。这保证你至少在两个节点上拥有最新的数据副本:主库和同步从库。 这种配置有时也被称为**半同步semi-synchronous**【7】。
因此,将所有从库都设置为同步的是不切实际的:任何一个节点的中断都会导致整个系统停滞不前。实际上,如果在数据库上启用同步复制,通常意味着其中**一个**跟随者是同步的,而其他的则是异步的。如果同步从库变得不可用或缓慢,则使一个异步从库同步。这保证你至少在两个节点上拥有最新的数据副本:主库和同步从库。 这种配置有时也被称为 **半同步semi-synchronous**【7】。
通常情况下,基于领导者的复制都配置为完全异步。 在这种情况下,如果主库失效且不可恢复,则任何尚未复制给从库的写入都会丢失。 这意味着即使已经向客户端确认成功,写入也不能保证**持久Durable**。 然而,一个完全异步的配置也有优点:即使所有的从库都落后了,主库也可以继续处理写入。
通常情况下,基于领导者的复制都配置为完全异步。 在这种情况下,如果主库失效且不可恢复,则任何尚未复制给从库的写入都会丢失。 这意味着即使已经向客户端确认成功,写入也不能保证 **持久Durable** 。 然而,一个完全异步的配置也有优点:即使所有的从库都落后了,主库也可以继续处理写入。
弱化的持久性可能听起来像是一个坏的折衷,无论如何,异步复制已经被广泛使用了,特别当有很多追随者,或追随者异地分布时。 稍后将在“[复制延迟问题](#复制延迟问题)”中回到这个问题。
@ -81,7 +81,7 @@
1. 在某个时刻获取主库的一致性快照如果可能而不必锁定整个数据库。大多数数据库都具有这个功能因为它是备份必需的。对于某些场景可能需要第三方工具例如MySQL的innobackupex 【12】。
2. 将快照复制到新的从库节点。
3. 从库连接到主库并拉取快照之后发生的所有数据变更。这要求快照与主库复制日志中的位置精确关联。该位置有不同的名称例如PostgreSQL将其称为**日志序列号log sequence number, LSN**MySQL将其称为**二进制日志坐标binlog coordinates**。
3. 从库连接到主库并拉取快照之后发生的所有数据变更。这要求快照与主库复制日志中的位置精确关联。该位置有不同的名称例如PostgreSQL将其称为 **日志序列号log sequence number, LSN**MySQL将其称为 **二进制日志坐标binlog coordinates**
4. 当从库处理完快照之后积压的数据变更,我们说它**赶上caught up**了主库。现在它可以继续处理主库产生的数据变化了。
建立从库的实际步骤因数据库而异。在某些系统中,这个过程是完全自动化的,而在另外一些系统中,它可能是一个需要由管理员手动执行的,有点神秘的多步骤工作流。
@ -102,7 +102,7 @@
故障切换可以手动进行(通知管理员主库挂了,并采取必要的步骤来创建新的主库)或自动进行。自动故障切换过程通常由以下步骤组成:
1. 确认主库失效。有很多事情可能会出错:崩溃,停电,网络问题等等。没有万无一失的方法来检测出现了什么问题,所以大多数系统只是简单使用**超时Timeout**节点频繁地相互来回传递消息并且如果一个节点在一段时间内例如30秒没有响应就认为它挂了因为计划内维护而故意关闭主库不算
1. 确认主库失效。有很多事情可能会出错:崩溃,停电,网络问题等等。没有万无一失的方法来检测出现了什么问题,所以大多数系统只是简单使用 **超时Timeout** 节点频繁地相互来回传递消息并且如果一个节点在一段时间内例如30秒没有响应就认为它挂了因为计划内维护而故意关闭主库不算
2. 选择一个新的主库。这可以通过选举过程(主库由剩余副本以多数选举产生)来完成,或者可以由之前选定的**控制器节点controller node**来指定新的主库。主库的最佳人选通常是拥有旧主库最新数据副本的从库(最小化数据损失)。让所有的节点同意一个新的领导者,是一个**共识**问题,将在[第9章](ch9.md)详细讨论。
3. 重新配置系统以启用新的主库。客户端现在需要将它们的写请求发送给新主库(将在“[请求路由](ch6.md#请求路由)”中讨论这个问题)。如果老领导回来,可能仍然认为自己是主库,没有意识到其他副本已经让它下台了。系统需要确保老领导认可新领导,成为一个从库。
@ -112,9 +112,9 @@
* 如果数据库需要和其他外部存储相协调那么丢弃写入内容是极其危险的操作。例如在GitHub 【13】的一场事故中一个过时的MySQL从库被提升为主库。数据库使用自增ID作为主键因为新主库的计数器落后于老主库的计数器所以新主库重新分配了一些已经被老主库分配掉的ID作为主键。这些主键也在Redis中使用主键重用使得MySQL和Redis中数据产生不一致最后导致一些私有数据泄漏到错误的用户手中。
* 发生某些故障时(见[第8章](ch8.md))可能会出现两个节点都以为自己是主库的情况。这种情况称为**脑裂(split brain)**,非常危险:如果两个主库都可以接受写操作,却没有冲突解决机制(参见“[多领导者复制](#多领导者复制)”),那么数据就可能丢失或损坏。一些系统采取了安全防范措施:当检测到两个主库节点同时存在时会关闭其中一个节点[^ii]但设计粗糙的机制可能最后会导致两个节点都被关闭【14】。
* 发生某些故障时(见[第8章](ch8.md))可能会出现两个节点都以为自己是主库的情况。这种情况称为 **脑裂(split brain)**,非常危险:如果两个主库都可以接受写操作,却没有冲突解决机制(参见“[多领导者复制](#多领导者复制)”),那么数据就可能丢失或损坏。一些系统采取了安全防范措施:当检测到两个主库节点同时存在时会关闭其中一个节点[^ii]但设计粗糙的机制可能最后会导致两个节点都被关闭【14】。
[^ii]: 这种机制称为**屏蔽fencing**,充满感情的术语是:**爆彼之头Shoot The Other Node In The Head, STONITH**。
[^ii]: 这种机制称为 **屏蔽fencing**,充满感情的术语是:**爆彼之头Shoot The Other Node In The Head, STONITH**。
* 主库被宣告死亡之前的正确超时应该怎么配置?在主库失效的情况下,超时时间越长,意味着恢复时间也越长。但是如果超时设置太短,又可能会出现不必要的故障切换。例如,临时负载峰值可能导致节点的响应时间超时,或网络故障可能导致数据包延迟。如果系统已经处于高负载或网络问题的困扰之中,那么不必要的故障切换可能会让情况变得更糟糕。
@ -145,7 +145,7 @@
在[第3章](ch3.md)中,我们讨论了存储引擎如何在磁盘上表示数据,并且我们发现,通常写操作都是追加到日志中:
* 对于日志结构存储引擎(请参阅“[SSTables和LSM树](ch3.md#SSTables和LSM树)”),日志是主要的存储位置。日志段在后台压缩,并进行垃圾回收。
* 对于覆写单个磁盘块的[B树](ch3.md#B树),每次修改都会先写入**预写式日志Write Ahead Log, WAL**,以便崩溃后索引可以恢复到一个一致的状态。
* 对于覆写单个磁盘块的[B树](ch3.md#B树),每次修改都会先写入 **预写式日志Write Ahead Log, WAL**,以便崩溃后索引可以恢复到一个一致的状态。
在任何一种情况下,日志都是包含所有数据库写入的仅追加字节序列。可以使用完全相同的日志在另一个节点上构建副本:除了将日志写入磁盘之外,主库还可以通过网络将其发送给其从库。
@ -169,7 +169,7 @@
由于逻辑日志与存储引擎内部分离,因此可以更容易地保持向后兼容,从而使领导者和跟随者能够运行不同版本的数据库软件甚至不同的存储引擎。
对于外部应用程序来说逻辑日志格式也更容易解析。如果要将数据库的内容发送到外部系统如数据这一点很有用例如复制到数据仓库进行离线分析或建立自定义索引和缓存【18】。 这种技术被称为**捕获数据变更change data capture**第11章将重新讲到它。
对于外部应用程序来说逻辑日志格式也更容易解析。如果要将数据库的内容发送到外部系统如数据这一点很有用例如复制到数据仓库进行离线分析或建立自定义索引和缓存【18】。 这种技术被称为 **数据变更捕获change data capture**第11章将重新讲到它。
#### 基于触发器的复制
@ -191,7 +191,7 @@
在这种扩展体系结构中,只需添加更多的追随者,就可以提高只读请求的服务容量。但是,这种方法实际上只适用于异步复制——如果尝试同步复制到所有追随者,则单个节点故障或网络中断将使整个系统无法写入。而且越多的节点越有可能会被关闭,所以完全同步的配置是非常不可靠的。
不幸的是,当应用程序从异步从库读取时,如果从库落后,它可能会看到过时的信息。这会导致数据库中出现明显的不一致:同时对主库和从库执行相同的查询,可能得到不同的结果,因为并非所有的写入都反映在从库中。这种不一致只是一个暂时的状态——如果停止写入数据库并等待一段时间,从库最终会赶上并与主库保持一致。出于这个原因,这种效应被称为**最终一致性eventually consistency**[^iii]【22,23】
不幸的是,当应用程序从异步从库读取时,如果从库落后,它可能会看到过时的信息。这会导致数据库中出现明显的不一致:同时对主库和从库执行相同的查询,可能得到不同的结果,因为并非所有的写入都反映在从库中。这种不一致只是一个暂时的状态——如果停止写入数据库并等待一段时间,从库最终会赶上并与主库保持一致。出于这个原因,这种效应被称为 **最终一致性eventually consistency**[^iii]【22,23】
[^iii]: 道格拉斯·特里Douglas Terry等人创造了术语最终一致性。 【24】 并经由Werner Vogels 【22】推广成为许多NoSQL项目的战吼。 然而不只有NoSQL数据库是最终一致的关系型数据库中的异步复制追随者也有相同的特性。
@ -209,7 +209,7 @@
**图5-3 用户写入后从旧副本中读取数据。需要写后读(read-after-write)的一致性来防止这种异常**
在这种情况下,我们需要**读写一致性read-after-write consistency**,也称为**读己之写一致性read-your-writes consistency**【24】。这是一个保证如果用户重新加载页面他们总会看到他们自己提交的任何更新。它不会对其他用户的写入做出承诺其他用户的更新可能稍等才会看到。它保证用户自己的输入已被正确保存。
在这种情况下,我们需要 **读写一致性read-after-write consistency**,也称为 **读己之写一致性read-your-writes consistency**【24】。这是一个保证如果用户重新加载页面他们总会看到他们自己提交的任何更新。它不会对其他用户的写入做出承诺其他用户的更新可能稍等才会看到。它保证用户自己的输入已被正确保存。
如何在基于领导者的复制系统中实现读后一致性?有各种可能的技术,这里说一些:
@ -234,7 +234,7 @@
### 单调读
从异步从库读取第二个异常例子是,用户可能会遇到**时光倒流moving backward in time**。
从异步从库读取第二个异常例子是,用户可能会遇到 **时光倒流moving backward in time**
如果用户从不同从库进行多次读取,就可能发生这种情况。例如,[图5-4](img/fig5-4.png)显示了用户2345两次进行相同的查询首先查询了一个延迟很小的从库然后是一个延迟较大的从库。 如果用户刷新网页而每个请求被路由到一个随机的服务器这种情况是很有可能的。第一个查询返回最近由用户1234添加的评论但是第二个查询不返回任何东西因为滞后的从库还没有拉取写入内容。在效果上相比第一个查询第二个查询是在更早的时间点来观察系统。如果第一个查询没有返回任何内容那问题并不大因为用户2345可能不知道用户1234最近添加了评论。但如果用户2345先看见用户1234的评论然后又看到它消失那么对于用户2345就很让人头大了。

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

View File

@ -85,7 +85,7 @@
## 参考文献
1. Ulrich Drepper: “[What Every Programmer Should Know About Memory](http://www.benstopford.com/2009/11/24/understanding-the-shared-nothing-architecture/),” akkadia.org, November 21, 2007.
1. Ulrich Drepper: “[What Every Programmer Should Know About Memory](https://people.freebsd.org/~lstewart/articles/cpumemory.pdf),” akkadia.org, November 21, 2007.
2. Ben Stopford: “[Shared Nothing vs. Shared Disk Architectures: An Independent View](http://www.benstopford.com/2009/11/24/understanding-the-shared-nothing-architecture/),” benstopford.com, November 24, 2009.

View File

@ -54,9 +54,9 @@
我们主要关注的是数据系统的**架构architecture**,以及它们被集成到数据密集型应用中的方式。本书没有足够的空间覆盖部署,运维,安全,管理等领域 —— 这些都是复杂而重要的主题,仅仅在本书中用粗略的注解讨论这些对它们很不公平。每个领域都值得用单独的书去讲。
本书中描述的许多技术都被涵盖在**大数据Big Data**这个时髦词的范畴中。然而“大数据”这个术语被滥用,缺乏明确定义,以至于在严肃的工程讨论中没有用处。这本书使用歧义更小的术语,如“单节点”之于”分布式系统“,或”在线/交互式系统“之于”离线/批处理系统“。
本书中描述的许多技术都被涵盖在 **大数据Big Data** 这个时髦词的范畴中。然而“大数据”这个术语被滥用,缺乏明确定义,以至于在严肃的工程讨论中没有用处。这本书使用歧义更小的术语,如“单节点”之于”分布式系统“,或”在线/交互式系统“之于”离线/批处理系统“。
本书对自由和开源软件FOSS有一定偏好因为阅读修改和执行源码是了解一样东西详细工作原理的好方法。开放的平台也可以降低供应商垄断的风险。然而在适当的情况下,我们也会讨论专利软件(闭源软件,软件即服务 SaaS或一些在文献中描述过但未公开发行的公司内部软件
本书对 **自由和开源软件FOSS** 有一定偏好,因为阅读,修改和执行源码是了解某事物详细工作原理的好方法。开放的平台也可以降低供应商垄断的风险。然而在适当的情况下,我们也会讨论专利软件(闭源软件,软件即服务 SaaS或一些在文献中描述过但未公开发行的公司内部软件
## 本书纲要