finish chapter 6, fix some missing cross references

This commit is contained in:
Yin Gang 2021-08-04 19:46:49 +08:00
parent 541848820a
commit 6fd412b916
7 changed files with 66 additions and 66 deletions

View File

@ -351,11 +351,11 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
如果存在与单个键关联的大量数据,则“将具有相同键的所有记录放到相同的位置”这种模式就被破坏了。例如在社交网络中,大多数用户可能会与几百人有连接,但少数名人可能有数百万的追随者。这种不成比例的活动数据库记录被称为**关键对象linchpin object**【38】或**热键hot key**。
在单个Reducer中收集与某个名流相关的所有活动例如他们发布内容的回复可能导致严重的倾斜也称为**热点hot spot**)—— 也就是说一个Reducer必须比其他Reducer处理更多的记录参见“[负载倾斜与消除热点](ch6.md#负载倾斜与消除热点)“。由于MapReduce作业只有在所有Mapper和Reducer都完成时才完成所有后续作业必须等待最慢的Reducer才能启动。
在单个Reducer中收集与某个名流相关的所有活动例如他们发布内容的回复可能导致严重的倾斜也称为**热点hot spot**)—— 也就是说一个Reducer必须比其他Reducer处理更多的记录参见“[负载偏斜与热点消除](ch6.md#负载偏斜与热点消除)“。由于MapReduce作业只有在所有Mapper和Reducer都完成时才完成所有后续作业必须等待最慢的Reducer才能启动。
如果连接的输入存在热点键可以使用一些算法进行补偿。例如Pig中的**倾斜连接skewed join**方法首先运行一个抽样作业来确定哪些键是热键【39】。连接实际执行时Mapper会将热键的关联记录**随机**相对于传统MapReduce基于键散列的确定性方法发送到几个Reducer之一。对于另外一侧的连接输入与热键相关的记录需要被复制到所有处理该键的Reducer上【40】。
这种技术将处理热键的工作分散到多个Reducer上这样可以使其更好地并行化代价是需要将连接另一侧的输入记录复制到多个Reducer上。 Crunch中的**分片连接sharded join**方法与之类似,但需要显式指定热键而不是使用采样作业。这种技术也非常类似于我们在“[负载倾斜与消除热点](ch6.md#负载倾斜与消除热点)”中讨论的技术,使用随机化来缓解分区数据库中的热点。
这种技术将处理热键的工作分散到多个Reducer上这样可以使其更好地并行化代价是需要将连接另一侧的输入记录复制到多个Reducer上。 Crunch中的**分片连接sharded join**方法与之类似,但需要显式指定热键而不是使用采样作业。这种技术也非常类似于我们在“[负载偏斜与热点消除](ch6.md#负载偏斜与热点消除)”中讨论的技术,使用随机化来缓解分区数据库中的热点。
Hive的偏斜连接优化采取了另一种方法。它需要在表格元数据中显式指定热键并将与这些键相关的记录单独存放与其它文件分开。当在该表上执行连接时对于热键它会使用Map端连接参阅[下一节](#Map端连接))。

2
ch2.md
View File

@ -214,7 +214,7 @@ CODASYL中的查询是通过利用遍历记录列和跟随访问路径表在数
#### 与文档数据库相比
在一个方面,文档数据库还原为层次模型:在其父记录中存储嵌套记录([图2-1]()中的一对多关系,如`positions``education`和`contact_info`),而不是在单独的表中。
在一个方面,文档数据库还原为层次模型:在其父记录中存储嵌套记录([图2-1](img/fig2-1.png)中的一对多关系,如`positions``education`和`contact_info`),而不是在单独的表中。
但是,在表示多对一和多对多的关系时,关系数据库和文档数据库并没有根本的不同:在这两种情况下,相关项目都被一个唯一的标识符引用,这个标识符在关系模型中被称为**外键**,在文档模型中称为**文档引用**【9】。该标识符在读取时通过连接或后续查询来解析。迄今为止文档数据库没有走CODASYL的老路。

6
ch5.md
View File

@ -441,13 +441,13 @@
### 多主复制拓扑
**复制拓扑**replication topology描述写入从一个节点传播到另一个节点的通信路径。如果你有两个领导者如[图5-7]()所示只有一个合理的拓扑结构领导者1必须把他所有的写到领导者2反之亦然。当有两个以上的领导各种不同的拓扑是可能的。[图5-8]()举例说明了一些例子。
**复制拓扑**replication topology描述写入从一个节点传播到另一个节点的通信路径。如果你有两个领导者如[图5-7](img/fig5-7.png)所示只有一个合理的拓扑结构领导者1必须把他所有的写到领导者2反之亦然。当有两个以上的领导各种不同的拓扑是可能的。[图5-8](img/fig5-8.png)举例说明了一些例子。
![](img/fig5-8.png)
**图5-8 三个可以设置多领导者复制的示例拓扑。**
最普遍的拓扑是全部到全部([图5-8 [c]]()其中每个领导者将其写入每个其他领导。但是也会使用更多受限制的拓扑例如默认情况下MySQL仅支持**环形拓扑circular topology**【34】其中每个节点接收来自一个节点的写入并将这些写入加上自己的任何写入转发给另一个节点。另一种流行的拓扑结构具有星形的形状[^v]。一个指定的根节点将写入转发给所有其他节点。星型拓扑可以推广到树。
最普遍的拓扑是全部到全部([图5-8 (c)](img/fig5-8.png)其中每个领导者将其写入每个其他领导。但是也会使用更多受限制的拓扑例如默认情况下MySQL仅支持**环形拓扑circular topology**【34】其中每个节点接收来自一个节点的写入并将这些写入加上自己的任何写入转发给另一个节点。另一种流行的拓扑结构具有星形的形状[^v]。一个指定的根节点将写入转发给所有其他节点。星型拓扑可以推广到树。
[^v]: 不要与星型模式混淆(请参阅“[星型和雪花型:分析的模式](ch2.md#星型和雪花型:分析的模式)”),其中描述了数据模型的结构,而不是节点之间的通信拓扑。
@ -665,7 +665,7 @@
来看一个算法,它确定两个操作是否为并发的,还是一个在另一个之前。为了简单起见,我们从一个只有一个副本的数据库开始。一旦我们已经制定了如何在单个副本上完成这项工作,我们可以将该方法推广到具有多个副本的无领导者数据库。
[图5-13]()显示了两个客户端同时向同一购物车添加项目。 (如果这样的例子让你觉得太麻烦了,那么可以想象,两个空中交通管制员同时把飞机添加到他们正在跟踪的区域)最初,购物车是空的。在它们之间,客户端向数据库发出五次写入:
[图5-13](img/fig5-13.png)显示了两个客户端同时向同一购物车添加项目。 (如果这样的例子让你觉得太麻烦了,那么可以想象,两个空中交通管制员同时把飞机添加到他们正在跟踪的区域)最初,购物车是空的。在它们之间,客户端向数据库发出五次写入:
1. 客户端 1 将牛奶加入购物车。这是该键的第一次写入服务器成功存储了它并为其分配版本号1最后将值与版本号一起回送给客户端。
2. 客户端 2 将鸡蛋加入购物车,不知道客户端 1 同时添加了牛奶(客户端 2 认为它的鸡蛋是购物车中的唯一物品)。服务器为此写入分配版本号 2并将鸡蛋和牛奶存储为两个单独的值。然后它将这两个值**都**反回给客户端 2 ,并附上版本号 2 。

86
ch6.md
View File

@ -11,13 +11,13 @@
[TOC]
在[第5章](ch5.md)中,我们讨论了复制——即数据在不同节点上的副本,对于非常大的数据集,或非常高的吞吐量,仅仅进行复制是不够的:我们需要将数据进行**分区partitions**,也称为**分片sharding**[^i]。
在[第5章](ch5.md)中,我们讨论了复制——即数据在不同节点上的副本,对于非常大的数据集,或非常高的吞吐量,仅仅进行复制是不够的:我们需要将数据进行**分区partitions**,也称为**分片sharding**[^i]。
[^i]: 正如本章所讨论的,分区是一种有意将大型数据库分解成小型数据库的方式。它与 **网络分区net splits** 无关,这是节点之间网络中的一种故障类型。我们将在[第8章](ch8.md)讨论这些错误。
[^i]: 正如本章所讨论的,分区是一种有意将大型数据库分解成小型数据库的方式。它与 **网络分区network partitions, netsplits** 无关,这是节点之间网络故障的一种。我们将在[第8章](ch8.md)讨论这些错误。
> ##### 术语澄清
>
> 上文中的**分区(partition)**在MongoDBElasticsearch和Solr Cloud中被称为**分片(shard)**在HBase中称之为**区域(Region)**Bigtable中则是 **表块tablet**Cassandra和Riak中是**虚节点vnode)**Couchbase中叫做**虚桶(vBucket)**。但是**分区(partition)** 是约定俗成的叫法。
> 上文中的**分区(partition)**在MongoDBElasticsearch和Solr Cloud中被称为**分片(shard)**在HBase中称之为**区域(Region)**Bigtable中则是 **表块tablet**Cassandra和Riak中是**虚节点vnode)**Couchbase中叫做**虚桶(vBucket)**。但是**分区(partitioning)** 是约定俗成的叫法。
>
通常情况下,每条数据(每条记录,每行或每个文档)属于且仅属于一个分区。有很多方法可以实现这一点,本章将进行深入讨论。实际上,每个分区都是自己的小型数据库,尽管数据库可能支持同时进行多个分区的操作。
@ -26,15 +26,15 @@
对于在单个分区上运行的查询,每个节点可以独立执行对自己的查询,因此可以通过添加更多的节点来扩大查询吞吐量。大型,复杂的查询可能会跨越多个节点并行处理,尽管这也带来了新的困难。
分区数据库在20世纪80年代由Teradata和NonStop SQL【1】等产品率先推出最近因为NoSQL数据库和基于Hadoop的数据仓库重新被关注。有些系统是为事务性工作设计的有些系统则用于分析参阅“[事务处理或分析]”):这种差异会影响系统的运作方式,但是分区的基本原理均适用于这两种工作方式。
分区数据库在20世纪80年代由Teradata和NonStop SQL【1】等产品率先推出最近因为NoSQL数据库和基于Hadoop的数据仓库重新被关注。有些系统是为事务性工作设计的有些系统则用于分析参阅“[事务处理还是分析](ch3.md#事务处理还是分析)”):这种差异会影响系统的运作方式,但是分区的基本原理均适用于这两种工作方式。
在本章中,我们将首先介绍分割大型数据集的不同方法,并观察索引如何与分区配合。然后我们将讨论[重新平衡分区](#重新平衡分区),如果想要添加或删除群集中的节点,则必须进行再平衡。最后,我们将概述数据库如何将请求路由到正确的分区并执行查询。
在本章中,我们将首先介绍分割大型数据集的不同方法,并观察索引如何与分区配合。然后我们将讨论[分区再平衡rebalancing](#分区再平衡),如果想要添加或删除集群中的节点,则必须进行再平衡。最后,我们将概述数据库如何将请求路由到正确的分区并执行查询。
## 分区与复制
分区通常与复制结合使用,使得每个分区的副本存储在多个节点上。 这意味着,即使每条记录属于一个分区,它仍然可以存储在多个不同的节点上以获得容错能力。
一个节点可能存储多个分区。 如果使用主从复制模型,则分区和复制的组合如[图6-1]()所示。 每个分区领导者(主)被分配给一个节点,追随者(从)被分配给其他节点。 每个节点可能是某些分区的领导者,同时是其他分区的追随者。
一个节点可能存储多个分区。 如果使用主从复制模型,则分区和复制的组合如[图6-1](img/fig6-1.png)所示。 每个分区领导者(主)被分配给一个节点,追随者(从)被分配给其他节点。 每个节点可能是某些分区的领导者,同时是其他分区的追随者。
我们在[第5章](ch5.md)讨论的关于数据库复制的所有内容同样适用于分区的复制。 大多数情况下,分区方案的选择与复制方案的选择是独立的,为简单起见,本章中将忽略复制。
![](img/fig6-1.png)
@ -55,17 +55,17 @@
### 根据键的范围分区
一种分区的方法是为每个分区指定一块连续的键范围(从最小值到最大值),如纸百科全书的卷([图6-2]())。如果知道范围之间的边界,则可以轻松确定哪个分区包含某个值。如果您还知道分区所在的节点,那么可以直接向相应的节点发出请求(对于百科全书而言,就像从书架上选取正确的书籍)。
一种分区的方法是为每个分区指定一块连续的键范围(从最小值到最大值),如纸百科全书的卷([图6-2]())。如果知道范围之间的边界,则可以轻松确定哪个分区包含某个值。如果您还知道分区所在的节点,那么可以直接向相应的节点发出请求(对于百科全书而言,就像从书架上选取正确的书籍)。
![](img/fig6-2.png)
**图6-2 印刷版百科全书按照关键字范围进行分区**
键的范围不一定均匀分布,因为数据也很可能不均匀分布。例如在[图6-2]()中第1卷包含以A和B开头的单词但第12卷则包含以TUVXY和Z开头的单词。只是简单的规定每个卷包含两个字母会导致一些卷比其他卷大。为了均匀分配数据分区边界需要依据数据调整。
键的范围不一定均匀分布,因为数据也很可能不均匀分布。例如在[图6-2](img/fig6-2.png)中第1卷包含以A和B开头的单词但第12卷则包含以TUVXY和Z开头的单词。只是简单的规定每个卷包含两个字母会导致一些卷比其他卷大。为了均匀分配数据分区边界需要依据数据调整。
分区边界可以由管理员手动选择,也可以由数据库自动选择(我们会在“[重新平衡分区]()”中更详细地讨论分区边界的选择)。 Bigtable使用了这种分区策略以及其开源等价物HBase 【2, 3】RethinkDB和2.4版本之前的MongoDB 【4】。
分区边界可以由管理员手动选择,也可以由数据库自动选择(我们会在“[分区再平衡](#分区再平衡)”中更详细地讨论分区边界的选择)。 Bigtable使用了这种分区策略以及其开源等价物HBase 【2, 3】RethinkDB和2.4版本之前的MongoDB 【4】。
在每个分区中,我们可以按照一定的顺序保存键(参见“[SSTables和LSM-树]()”)。好处是进行范围扫描非常简单,您可以将键作为联合索引来处理,以便在一次查询中获取多个相关记录(参阅“[多列索引](#ch2.md#多列索引)”)。例如,假设我们有一个程序来存储传感器网络的数据,其中主键是测量的时间戳(年月日时分秒)。范围扫描在这种情况下非常有用,因为我们可以轻松获取某个月份的所有数据。
在每个分区中,我们可以按照一定的顺序保存键(参见“[SSTables和LSM树](ch3.md#SSTables和LSM树)”)。好处是进行范围扫描非常简单,您可以将键作为联合索引来处理,以便在一次查询中获取多个相关记录(参阅“[多列索引](ch3.md#多列索引)”)。例如,假设我们有一个程序来存储传感器网络的数据,其中主键是测量的时间戳(年月日时分秒)。范围扫描在这种情况下非常有用,因为我们可以轻松获取某个月份的所有数据。
然而Key Range分区的缺点是某些特定的访问模式会导致热点。 如果主键是时间戳,则分区对应于时间范围,例如,给每天分配一个分区。 不幸的是由于我们在测量发生时将数据从传感器写入数据库因此所有写入操作都会转到同一个分区即今天的分区这样分区可能会因写入而过载而其他分区则处于空闲状态【5】。
@ -75,7 +75,7 @@
由于偏斜和热点的风险,许多分布式数据存储使用散列函数来确定给定键的分区。
一个好的散列函数可以将偏斜的数据均匀分布。假设你有一个32位散列函数,无论何时给定一个新的字符串输入它将返回一个0到$2^{32}$ -1之间的“随机”数。即使输入的字符串非常相似它们的散列也会均匀分布在这个数字范围内。
一个好的散列函数可以将偏斜的数据均匀分布。假设你有一个32位散列函数,无论何时给定一个新的字符串输入它将返回一个0到$2^{32}$ -1之间的“随机”数。即使输入的字符串非常相似它们的散列也会均匀分布在这个数字范围内。
出于分区的目的散列函数不需要多么强壮的加密算法例如Cassandra和MongoDB使用MD5Voldemort使用Fowler-Noll-Vo函数。许多编程语言都有内置的简单哈希函数它们用于哈希表但是它们可能不适合分区例如在Java的`Object.hashCode()`和Ruby的`Object#hash`同一个键可能在不同的进程中有不同的哈希值【6】。
@ -85,25 +85,25 @@
**图6-3 按哈希键分区**
这种技术擅长在分区之间分配键。分区边界可以是均匀间隔的,也可以是伪随机选择的(在这种情况下,该技术有时也被称为**一致性哈希consistent hashing**)。
这种技术擅长在分区之间公平地分配键。分区边界可以是均匀间隔的,也可以是伪随机选择的(在这种情况下,该技术有时也被称为**一致性哈希consistent hashing**)。
> #### 一致性哈希
>
> 一致性哈希由Karger等人定义。【7】 用于跨互联网级别的缓存系统例如CDN中是一种能均匀分配负载的方法。它使用随机选择的 **分区边界partition boundaries** 来避免中央控制或分布式一致性的需要。 请注意这里的一致性与复制一致性请参阅第5章或ACID一致性参阅[第7章](ch7.md))无关,而是描述了重新平衡的特定方法。
> 一致性哈希由Karger等人定义。【7】 用于跨互联网级别的缓存系统例如CDN中是一种能均匀分配负载的方法。它使用随机选择的 **分区边界partition boundaries** 来避免中央控制或分布式共识的需要。 请注意,这里的一致性与复制一致性(请参阅[第5章](ch5.md)或ACID一致性参阅[第7章](ch7.md))无关,而是描述了一种重新平衡reblancing的特定方法。
>
> 正如我们将在“[重新平衡分区](#重新平衡分区)”中所看到的,这种特殊的方法对于数据库实际上并不是很好,所以在实际中很少使用(某些数据库的文档仍然指的是一致性哈希,但是它往往是不准确的)。 因为有可能产生混淆,所以最好避免使用一致性哈希这个术语,而只是把它称为**散列分区hash partitioning**。
> 正如我们将在“[分区再平衡](#分区再平衡)”中所看到的,这种特殊的方法对于数据库实际上并不是很好,所以在实际中很少使用(某些数据库的文档仍然会使用一致性哈希的说法,但是它往往是不准确的)。 因为有可能产生混淆,所以最好避免使用一致性哈希这个术语,而只是把它称为**散列分区hash partitioning**。
不幸的是,通过使用Key散列进行分区,我们失去了键范围分区的一个很好的属性:高效执行范围查询的能力。曾经相邻的密钥现在分散在所有分区中所以它们之间的顺序就丢失了。在MongoDB中如果您使用了基于散列的分区模式则任何范围查询都必须发送到所有分区【4】。Riak 【9】Couchbase 【10】或Voldemort不支持主键上的范围查询。
不幸的是,通过使用散列进行分区,我们失去了键范围分区的一个很好的属性:高效执行范围查询的能力。曾经相邻的现在分散在所有分区中所以它们之间的顺序就丢失了。在MongoDB中如果您使用了基于散列的分区模式则任何范围查询都必须发送到所有分区【4】。Riak 【9】Couchbase 【10】或Voldemort不支持主键上的范围查询。
Cassandra采取了折衷的策略【11, 12, 13】。 Cassandra中的表可以使用由多个列组成的复合主键来声明。键中只有第一列会作为散列的依据而其他列则被用作Casssandra的SSTables中排序数据的连接索引。尽管查询无法在复合主键的第一列中按范围扫表但如果第一列已经指定了固定值则可以对该键的其他列执行有效的范围扫描。
组合索引方法为一对多关系提供了一个优雅的数据模型。例如,在社交媒体网站上,一个用户可能会发布很多更新。如果更新的主键被选择为`(user_id, update_timestamp)`,那么您可以有效地检索特定用户在某个时间间隔内按时间戳排序的所有更新。不同的用户可以存储在不同的分区上,对于每个用户,更新按时间戳顺序存储在单个分区上。
### 负载倾斜与消除热点
### 负载偏斜与热点消除
如前所述,哈希分区可以帮助减少热点。但是,它不能完全避免它们:在极端情况下,所有的读写操作都是针对同一个键的,所有的请求都会被路由到同一个分区。
这种场景也许并不常见但并非闻所未闻例如在社交媒体网站上一个拥有数百万追随者的名人用户在做某事时可能会引发一场风暴【14】。这个事件可能导致大量写入同一个键键可能是名人的用户ID或者人们正在评论的动作的ID。哈希策略不起作用因为两个相同ID的哈希值仍然是相同的。
这种场景也许并不常见但并非闻所未闻例如在社交媒体网站上一个拥有数百万追随者的名人用户在做某事时可能会引发一场风暴【14】。这个事件可能导致同一个键的大量写入键可能是名人的用户ID或者人们正在评论的动作的ID。哈希策略不起作用因为两个相同ID的哈希值仍然是相同的。
如今大多数数据系统无法自动补偿这种高度偏斜的负载因此应用程序有责任减少偏斜。例如如果一个主键被认为是非常火爆的一个简单的方法是在主键的开始或结尾添加一个随机数。只要一个两位数的十进制随机数就可以将主键分散为100种不同的主键,从而存储在不同的分区中。
@ -112,12 +112,12 @@
也许在将来,数据系统将能够自动检测和补偿偏斜的工作负载;但现在,您需要自己来权衡。
## 分与次级索引
## 分与次级索引
到目前为止,我们讨论的分区方案依赖于键值数据模型。如果只通过主键访问记录,我们可以从该键确定分区,并使用它来将读写请求路由到负责该键的分区。
如果涉及次级索引,情况会变得更加复杂(参考“[其他索引结构]()”。辅助索引通常并不能唯一地标识记录而是一种搜索记录中出现特定值的方式查找用户123的所有操作查找包含词语`hogwash`的所有文章,查找所有颜色为红色的车辆等等。
如果涉及次级索引,情况会变得更加复杂(参考“[其他索引结构](ch3.md#其他索引结构)”。辅助索引通常并不能唯一地标识记录而是一种搜索记录中出现特定值的方式查找用户123的所有操作查找包含词语`hogwash`的所有文章,查找所有颜色为红色的车辆等等。
次级索引是关系型数据库的基础并且在文档数据库中也很普遍。许多键值存储如HBase和Volde-mort为了减少实现的复杂度而放弃了次级索引但是一些如Riak已经开始添加它们因为它们对于数据模型实在是太有用了。并且次级索引也是Solr和Elasticsearch等搜索服务器的基石。
@ -127,9 +127,9 @@
假设你正在经营一个销售二手车的网站(如[图6-4](img/fig6-4.png)所示)。 每个列表都有一个唯一的ID——称之为文档ID——并且用文档ID对数据库进行分区例如分区0中的ID 0到499分区1中的ID 500到999等
你想让用户搜索汽车,允许他们通过颜色和厂商过滤,所以需要一个在颜色和厂商上的次级索引(文档数据库中这些是**字段field**,关系数据库中这些是**列column** )。 如果您声明了索引,则数据库可以自动执行索引[^ii]。例如,无论何时将红色汽车添加到数据库,数据库分区都会自动将其添加到索引条目`colorred`的文档ID列表中。
你想让用户搜索汽车,允许他们通过颜色和厂商过滤,所以需要一个在颜色和厂商上的次级索引(文档数据库中这些是**字段field**,关系数据库中这些是**列column** )。 如果您声明了索引,则数据库可以自动执行索引[^ii]。例如,无论何时将红色汽车添加到数据库,数据库分区都会自动将其添加到索引条目`color:red`的文档ID列表中。
[^ii]: 如果数据库仅支持键值模型则你可能会尝试在应用程序代码中创建从值到文档ID的映射来实现辅助索引。 如果沿着这条路线走下去,请万分小心,确保您的索引与底层数据保持一致。 竞争条件和间歇性写入失败(其中一些更改已保存,但其他更改未保存)很容易导致数据不同步 - 参见“[多对象事务的需求]()”。
[^ii]: 如果数据库仅支持键值模型则你可能会尝试在应用程序代码中创建从值到文档ID的映射来实现辅助索引。 如果沿着这条路线走下去,请万分小心,确保您的索引与底层数据保持一致。 竞争条件和间歇性写入失败(其中一些更改已保存,但其他更改未保存)很容易导致数据不同步 - 参见“[多对象事务的需求](ch7.md#多对象事务的需求)”。
![](img/fig6-4.png)
@ -147,15 +147,15 @@
我们可以构建一个覆盖所有分区数据的**全局索引**,而不是给每个分区创建自己的次级索引(本地索引)。但是,我们不能只把这个索引存储在一个节点上,因为它可能会成为瓶颈,违背了分区的目的。全局索引也必须进行分区,但可以采用与主键不同的分区方式。
[图6-5](img/fig6-5.png)述了这可能是什么样子:来自所有分区的红色汽车在红色索引中,并且索引是分区的,首字母从`a`到`r`的颜色在分区0中`s`到`z`的在分区1。汽车制造商的索引也与之类似分区边界在`f`和`h`之间)。
[图6-5](img/fig6-5.png)述了这可能是什么样子:来自所有分区的红色汽车在红色索引中,并且索引是分区的,首字母从`a`到`r`的颜色在分区0中`s`到`z`的在分区1。汽车制造商的索引也与之类似分区边界在`f`和`h`之间)。
![](img/fig6-5.png)
**图6-5 基于关键词对二级索引进行分区**
我们将这种索引称为**关键词分区term-partitioned**,因为我们寻找的关键词决定了索引的分区方式。例如,一个关键词可能是:`颜色:红色`。**关键词(Term)** 来源于来自全文搜索索引(一种特殊的次级索引),指文档中出现的所有单词。
我们将这种索引称为**关键词分区term-partitioned**,因为我们寻找的关键词决定了索引的分区方式。例如,一个关键词可能是:`color:red`。**关键词(Term)** 这个名称来源于全文搜索索引(一种特殊的次级索引),指文档中出现的所有单词。
和之前一样,我们可以通过**关键词**本身或者它的散列进行索引分区。根据它本身分区对于范围扫描非常有用(例如对于数字,像汽车的报价),而对关键词的哈希分区提供了负载均衡的能力。
和之前一样,我们可以通过**关键词**本身或者它的散列进行索引分区。根据关键词本身来分区对于范围扫描非常有用(例如对于数值类的属性,像汽车的报价),而对关键词的哈希分区提供了负载均衡的能力。
关键词分区的全局索引优于文档分区索引的地方点是它可以使读取更有效率:不需要**分散/收集**所有分区,客户端只需要向包含关键词的分区发出请求。全局索引的缺点在于写入速度较慢且较为复杂,因为写入单个文档现在可能会影响索引的多个分区(文档中的每个关键词可能位于不同的分区或者不同的节点上) 。
@ -163,11 +163,11 @@
在实践中,对全局二级索引的更新通常是**异步**的也就是说如果在写入之后不久读取索引刚才所做的更改可能尚未反映在索引中。例如Amazon DynamoDB声称在正常情况下其全局次级索引会在不到一秒的时间内更新但在基础架构出现故障的情况下可能会有延迟【20】。
全局关键词分区索引的其他用途包括Riak的搜索功能【21】和Oracle数据仓库它允许您在本地和全局索引之间进行选择【22】。我们将在[第12章](ch12.md)中涉及实现关键字二级索引的话题。
全局关键词分区索引的其他用途包括Riak的搜索功能【21】和Oracle数据仓库它允许您在本地和全局索引之间进行选择【22】。我们将在[第12章](ch12.md)中继续关键词分区二级索引实现的话题。
## 分区再平衡
随着时间的推移,数据库会有各种变化
随着时间的推移,数据库会有各种变化
* 查询吞吐量增加所以您想要添加更多的CPU来处理负载。
* 数据集大小增加所以您想添加更多的磁盘和RAM来存储它。
@ -182,7 +182,7 @@
* 节点之间只移动必须的数据以便快速再平衡并减少网络和磁盘I/O负载。
### 平衡策略
### 平衡策略
有几种不同的分区分配方法【23】,让我们依次简要讨论一下。
@ -190,9 +190,9 @@
我们在前面说过([图6-3](img/fig6-3.png)),最好将可能的散列分成不同的范围,并将每个范围分配给一个分区(例如,如果$0≤hash(key)<b_0$则将键分配给分区0如果$b_0 hash(key) <b_1$则分配给分区1
也许你想知道为什么我们不使用 ***mod***(许多编程语言中的%运算符)。例如,`hash(key) mod 10`会返回一个介于0和9之间的数字如果我们将散列写为十进制数散列模10将是最后一个数字。如果我们有10个节点编号为0到9这似乎是将每个键分配给一个节点的简单方法。
也许你想知道为什么我们不使用 ***取模(mod***(许多编程语言中的%运算符)。例如,`hash(key) mod 10`会返回一个介于0和9之间的数字如果我们将散列写为十进制数散列模10将是最后一个数字。如果我们有10个节点编号为0到9这似乎是将每个键分配给一个节点的简单方法。
模$N$方法的问题是如果节点数量N发生变化大多数密钥将需要从一个节点移动到另一个节点。例如假设$hash(key)=123456$。如果最初有10个节点那么这个键一开始放在节点6上因为$123456\ mod\ 10 = 6$。当您增长到11个节点时密钥需要移动到节点3$123456\ mod\ 11 = 3$当您增长到12个节点时需要移动到节点0$123456\ mod\ 12 = 0$)。这种频繁的举动使得重新平衡过于昂贵。
N$mod N$方法的问题是如果节点数量N发生变化大多数密钥将需要从一个节点移动到另一个节点。例如假设$hash(key)=123456$。如果最初有10个节点那么这个键一开始放在节点6上因为$123456\ mod\ 10 = 6$。当您增长到11个节点时密钥需要移动到节点3$123456\ mod\ 11 = 3$当您增长到12个节点时需要移动到节点0$123456\ mod\ 12 = 0$)。这种频繁的举动使得重新平衡过于昂贵。
我们需要一种只移动必需数据的方法。
@ -212,21 +212,21 @@
在这种配置中,分区的数量通常在数据库第一次建立时确定,之后不会改变。虽然原则上可以分割和合并分区(请参阅下一节),但固定数量的分区在操作上更简单,因此许多固定分区数据库选择不实施分区分割。因此,一开始配置的分区数就是您可以拥有的最大节点数量,所以您需要选择足够多的分区以适应未来的增长。但是,每个分区也有管理开销,所以选择太大的数字会适得其反。
如果数据集的总大小难以预估(例如,如果它开始很小,但随着时间的推移可能会变得更大),选择正确的分区数是困难的。由于每个分区包含了总数据量固定比率的数据,因此每个分区的大小与集群中的数据总量成比例增长。如果分区非常大,再平衡和从节点故障恢复变得昂贵。但是,如果分区太小,则会产生太多的开销。当分区大小“恰到好处”的时候才能获得很好的性能,如果分区数量固定,但数据量变动很大,则难以达到最佳性能。
如果数据集的总大小难以预估(例如,可能它开始很小,但随着时间的推移会变得更大),选择正确的分区数是困难的。由于每个分区包含了总数据量固定比率的数据,因此每个分区的大小与集群中的数据总量成比例增长。如果分区非常大,再平衡和从节点故障恢复变得昂贵。但是,如果分区太小,则会产生太多的开销。当分区大小“恰到好处”的时候才能获得很好的性能,如果分区数量固定,但数据量变动很大,则难以达到最佳性能。
#### 动态分区
对于使用键范围分区的数据库(参阅“[按键范围分区](#按键范围分区)”),具有固定边界的固定数量的分区将非常不便:如果出现边界错误,则可能会导致一个分区中的所有数据或者其他分区中的所有数据为空。手动重新配置分区边界将非常繁琐。
对于使用键范围分区的数据库(参阅“[根据键的范围分区](#根据键的范围分区)”),具有固定边界的固定数量的分区将非常不便:如果出现边界错误,则可能会导致一个分区中的所有数据或者其他分区中的所有数据为空。手动重新配置分区边界将非常繁琐。
出于这个原因按键的范围进行分区的数据库如HBase和RethinkDB会动态创建分区。当分区增长到超过配置的大小时在HBase上默认值是10GB会被分成两个分区每个分区约占一半的数据【26】。与之相反如果大量数据被删除并且分区缩小到某个阈值以下则可以将其与相邻分区合并。此过程与B树顶层发生的过程类似参阅“[B树](ch2.md#B树)”)。
出于这个原因按键的范围进行分区的数据库如HBase和RethinkDB会动态创建分区。当分区增长到超过配置的大小时在HBase上默认值是10GB会被分成两个分区每个分区约占一半的数据【26】。与之相反如果大量数据被删除并且分区缩小到某个阈值以下则可以将其与相邻分区合并。此过程与B树顶层发生的过程类似参阅“[B树](ch3.md#B树)”)。
每个分区分配给一个节点每个节点可以处理多个分区就像固定数量的分区一样。大型分区拆分后可以将其中的一半转移到另一个节点以平衡负载。在HBase中分区文件的传输通过HDFS底层分布式文件系统来实现【3】。
每个分区分配给一个节点每个节点可以处理多个分区就像固定数量的分区一样。大型分区拆分后可以将其中的一半转移到另一个节点以平衡负载。在HBase中分区文件的传输通过HDFS底层使用的分布式文件系统来实现【3】。
动态分区的一个优点是分区数量适应总数据量。如果只有少量的数据少量的分区就足够了所以开销很小如果有大量的数据每个分区的大小被限制在一个可配置的最大值【23】。
需要注意的是一个空的数据库从一个分区开始因为没有关于在哪里绘制分区边界的先验信息。数据集开始时很小直到达到第一个分区的分割点所有写入操作都必须由单个节点处理而其他节点则处于空闲状态。为了解决这个问题HBase和MongoDB允许在一个空的数据库上配置一组初始分区这被称为**预分割pre-splitting**。在键范围分区的情况中预分割需要提前知道键是如何进行分配的【4,26】。
动态分区不仅适用于数据的范围分区而且也适用于散列分区。从版本2.4开始MongoDB同时支持范围和哈希分区,并且都是进行动态分割分区。
动态分区不仅适用于数据的范围分区而且也适用于散列分区。从版本2.4开始MongoDB同时支持范围和散列分区,并且都支持动态分割分区。
#### 按节点比例分区
@ -234,11 +234,11 @@
Cassandra和Ketama使用的第三种方法是使分区数与节点数成正比——换句话说每个节点具有固定数量的分区【23,27,28】。在这种情况下每个分区的大小与数据集大小成比例地增长而节点数量保持不变但是当增加节点数时分区将再次变小。由于较大的数据量通常需要较大数量的节点进行存储因此这种方法也使每个分区的大小较为稳定。
当一个新节点加入集群时它随机选择固定数量的现有分区进行拆分然后占有这些拆分分区中每个分区的一半同时将每个分区的另一半留在原地。随机化可能会产生不公平的分割但是平均在更大数量的分区上时在Cassandra中默认情况下每个节点有256个分区新节点最终从现有节点获得公平的负载份额。 Cassandra 3.0引入了另一种再分配的算法来避免不公平的分割【29】。
当一个新节点加入集群时它随机选择固定数量的现有分区进行拆分然后占有这些拆分分区中每个分区的一半同时将每个分区的另一半留在原地。随机化可能会产生不公平的分割但是平均在更大数量的分区上时在Cassandra中默认情况下每个节点有256个分区新节点最终从现有节点获得公平的负载份额。 Cassandra 3.0引入了另一种再平衡的算法来避免不公平的分割【29】。
随机选择分区边界要求使用基于散列的分区可以从散列函数产生的数字范围中挑选边界。实际上这种方法最符合一致性哈希的原始定义【7】参阅“[一致性哈希](#一致性哈希)”。最新的哈希函数可以在较低元数据开销的情况下达到类似的效果【8】。
### 运维:手动还是自动平衡
### 运维:手动还是自动平衡
关于再平衡有一个重要问题:自动还是手动进行?
@ -268,9 +268,9 @@
**图6-7 将请求路由到正确节点的三种不同方式。**
这是一个具有挑战性的问题,因为重要的是所有参与者都同意 - 否则请求将被发送到错误的节点,而不是正确处理。 在分布式系统中有达成共识的协议,但很难正确地实现(见[第9章](ch9.md))。
这是一个具有挑战性的问题,因为重要的是所有参与者都同意 - 否则请求将被发送到错误的节点,得不到正确的处理。 在分布式系统中有达成共识的协议,但很难正确地实现(见[第9章](ch9.md))。
许多分布式数据系统都依赖于一个独立的协调服务比如ZooKeeper来跟踪集群元数据如[图6-8](img/fig6-8.png)所示。 每个节点在ZooKeeper中注册自己ZooKeeper维护分区到节点的可靠映射。 其他参与者如路由层或分区感知客户端可以在ZooKeeper中订阅此信息。 只要分区分配发生改变或者集群中添加或删除了一个节点ZooKeeper就会通知路由层使路由信息保持最新状态。
许多分布式数据系统都依赖于一个独立的协调服务比如ZooKeeper来跟踪集群元数据如[图6-8](img/fig6-8.png)所示。 每个节点在ZooKeeper中注册自己ZooKeeper维护分区到节点的可靠映射。 其他参与者如路由层或分区感知客户端可以在ZooKeeper中订阅此信息。 只要分区分配发生改变或者集群中添加或删除了一个节点ZooKeeper就会通知路由层使路由信息保持最新状态。
![](img/fig6-8.png)
@ -278,7 +278,7 @@
例如LinkedIn的Espresso使用Helix 【31】进行集群管理依靠ZooKeeper实现了如[图6-8](img/fig6-8.png)所示的路由层。 HBaseSolrCloud和Kafka也使用ZooKeeper来跟踪分区分配。 MongoDB具有类似的体系结构但它依赖于自己的**配置服务器config server** 实现和mongos守护进程作为路由层。
Cassandra和Riak采取不同的方法他们在节点之间使用**流言协议gossip protocol** 来传播群集状态的变化。请求可以发送到任意节点,该节点会转发到包含所请求的分区的适当节点([图6-7]()中的方法1。这个模型在数据库节点中增加了更多的复杂性但是避免了对像ZooKeeper这样的外部协调服务的依赖。
Cassandra和Riak采取不同的方法他们在节点之间使用**流言协议gossip protocol** 来传播群集状态的变化。请求可以发送到任意节点,该节点会转发到包含所请求的分区的适当节点([图6-7](img/fig6-7.png)中的方法1。这个模型在数据库节点中增加了更多的复杂性但是避免了对像ZooKeeper这样的外部协调服务的依赖。
Couchbase不会自动重新平衡这简化了设计。通常情况下它配置了一个名为moxi的路由层它会从集群节点了解路由变化【32】。
@ -286,21 +286,21 @@
### 执行并行查询
到目前为止,我们只关注读取或写入单个键的非常简单的查询(对于文档分区的二级索引,另外还有分散/聚集查询)。这与大多数NoSQL分布式数据存储所支持的访问级别有关
到目前为止,我们只关注读取或写入单个键的非常简单的查询(加上基于文档分区的二级索引场景下的分散/聚集查询)。这也是大多数NoSQL分布式数据存储所支持的访问级。
然而,通常用于分析的**大规模并行处理MPP, Massively parallel processing** 关系型数据库产品在其支持的查询类型方面要复杂得多。一个典型的数据仓库查询包含多个连接,过滤,分组和聚合操作。 MPP查询优化器将这个复杂的查询分解成许多执行阶段和分区其中许多可以在数据库集群的不同节点上并行执行。涉及扫描大规模数据集的查询特别受益于这种并行执行。
数据仓库查询的快速并行执行是一个专门的话题由于分析有很重要的商业意义可以带来很多利益。我们将在第10章讨论并行查询执行的一些技巧。有关并行数据库中使用的技术的更详细的概述请参阅参考文献【1,33】。
数据仓库查询的快速并行执行是一个专门的话题,由于分析有很重要的商业意义,可以带来很多利益。我们将在[第10章](ch10.md)讨论并行查询执行的一些技巧。有关并行数据库中使用的技术的更详细的概述请参阅参考文献【1,33】。
## 本章小结
在本章中,我们探讨了将大数据集划分成更小的子集的不同方法。数据量非常大的时候,在单台机器上存储和处理不再可行,则分区十分必要。分区的目标是在多台机器上均匀分布数据和查询负载,避免出现热点(负载不成比例的节点)。这需要选择适合于您的数据的分区方案,并在将节点添加到集群或从集群删除时进行分区。
在本章中,我们探讨了将大数据集划分成更小的子集的不同方法。数据量非常大的时候,在单台机器上存储和处理不再可行,而分区则十分必要。分区的目标是在多台机器上均匀分布数据和查询负载,避免出现热点(负载不成比例的节点)。这需要选择适合于您的数据的分区方案,并在将节点添加到集群或从集群删除时进行分区再平衡
我们讨论了两种主要的分区方法:
***键范围分区***
其中键是有序的,并且分区拥有从某个最小值到某个最大值的所有键。排序的优势在于可以进行有效的范围查询,但是如果应用程序经常访问相邻的键,则存在热点的风险。
其中键是有序的,并且分区拥有从某个最小值到某个最大值的所有键。排序的优势在于可以进行有效的范围查询,但是如果应用程序经常访问相邻的键,则存在热点的风险。
在这种方法中,当分区变得太大时,通常将分区分成两个子分区,动态地再平衡分区。

24
ch7.md
View File

@ -268,7 +268,7 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
通过防止脏写,这个隔离级别避免了一些并发问题:
- 如果事务更新多个对象,脏写会导致不好的结果。例如,考虑 [图7-5](img/fig7-5.png)[图7-5](img/fig7-5.png) 以一个二手车销售网站为例Alice和Bob两个人同时试图购买同一辆车。购买汽车需要两次数据库写入网站上的商品列表需要更新以反映买家的购买销售发票需要发送给买家。在[图7-5](img/fig7-5.png)的情况下销售是属于Bob的因为他成功更新了商品列表但发票却寄送给了爱丽丝因为她成功更新了发票表。读已提交会阻止这样这样的事故。
- 但是,提交读取并不能防止[图7-1]()中两个计数器增量之间的竞争状态。在这种情况下,第二次写入发生在第一个事务提交后,所以它不是一个脏写。这仍然是不正确的,但是出于不同的原因,在“[防止更新丢失](#防止丢失更新)”中将讨论如何使这种计数器增量安全。
- 但是,提交读取并不能防止[图7-1](img/fig7-1.png)中两个计数器增量之间的竞争状态。在这种情况下,第二次写入发生在第一个事务提交后,所以它不是一个脏写。这仍然是不正确的,但是出于不同的原因,在“[防止更新丢失](#防止丢失更新)”中将讨论如何使这种计数器增量安全。
![](img/fig7-5.png)
@ -284,7 +284,7 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
但是要求读锁的办法在实践中效果并不好。因为一个长时间运行的写入事务会迫使许多只读事务等到这个慢写入事务完成。这会损失只读事务的响应时间,并且不利于可操作性:因为等待锁,应用某个部分的迟缓可能由于连锁效应,导致其他部分出现问题。
出于这个原因,大多数数据库[^vi]使用[图7-4]()的方式防止脏读:对于写入的每个对象,数据库都会记住旧的已提交值,和由当前持有写入锁的事务设置的新值。当事务正在进行时,任何其他读取对象的事务都会拿到旧值。 只有当新值提交后,事务才会切换到读取新值。
出于这个原因,大多数数据库[^vi]使用[图7-4](img/fig7-4.png)的方式防止脏读:对于写入的每个对象,数据库都会记住旧的已提交值,和由当前持有写入锁的事务设置的新值。当事务正在进行时,任何其他读取对象的事务都会拿到旧值。 只有当新值提交后,事务才会切换到读取新值。
[^vi]: 在撰写本文时,唯一在读已提交隔离级别使用读锁的主流数据库是使用`read_committed_snapshot = off`配置的IBM DB2和Microsoft SQL Server [23,36]。
@ -302,7 +302,7 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
这种异常被称为**不可重复读nonrepeatable read**或**读取偏差read skew**如果Alice在事务结束时再次读取账户1的余额她将看到与她之前的查询中看到的不同的值600美元。在读已提交的隔离条件下**不可重复读**被认为是可接受的Alice看到的帐户余额时确实在阅读时已经提交了。
> 不幸的是,术语**偏差skew** 这个词是过载的:以前使用它是因为热点的不平衡工作量(参阅“[偏斜的负载倾斜与消除热点](ch6.md#负载倾斜与消除热点)”),而这里偏差意味着异常的时机。
> 不幸的是,术语**偏差skew** 这个词是过载的:以前使用它是因为热点的不平衡工作量(参阅“[负载偏斜与热点消除](ch6.md#负载偏斜与热点消除)”),而这里偏差意味着异常的时机。
对于Alice的情况这不是一个长期持续的问题。因为如果她几秒钟后刷新银行网站的页面她很可能会看到一致的帐户余额。但是有些情况下不能容忍这种暂时的不一致
@ -324,11 +324,11 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
与读取提交的隔离类似,快照隔离的实现通常使用写锁来防止脏写(请参阅“[读已提交](#读已提交)”),这意味着进行写入的事务会阻止另一个事务修改同一个对象。但是读取不需要任何锁定。从性能的角度来看,快照隔离的一个关键原则是:**读不阻塞写,写不阻塞读**。这允许数据库在处理一致性快照上的长时间查询时,可以正常地同时处理写入操作。且两者间没有任何锁定争用。
为了实现快照隔离,数据库使用了我们看到的用于防止[图7-4]()中的脏读的机制的一般化。数据库必须可能保留一个对象的几个不同的提交版本,因为各种正在进行的事务可能需要看到数据库在不同的时间点的状态。因为它并排维护着多个版本的对象,所以这种技术被称为**多版本并发控制MVCC, multi-version concurrency control**。
为了实现快照隔离,数据库使用了我们看到的用于防止[图7-4](img/fig7-4.png)中的脏读的机制的一般化。数据库必须可能保留一个对象的几个不同的提交版本,因为各种正在进行的事务可能需要看到数据库在不同的时间点的状态。因为它并排维护着多个版本的对象,所以这种技术被称为**多版本并发控制MVCC, multi-version concurrency control**。
如果一个数据库只需要提供**读已提交**的隔离级别,而不提供**快照隔离**那么保留一个对象的两个版本就足够了提交的版本和被覆盖但尚未提交的版本。支持快照隔离的存储引擎通常也使用MVCC来实现**读已提交**隔离级别。一种典型的方法是**读已提交**为每个查询使用单独的快照,而**快照隔离**对整个事务使用相同的快照。
[图7-7]()说明了如何在PostgreSQL中实现基于MVCC的快照隔离【31】其他实现类似。当一个事务开始时它被赋予一个唯一的永远增长[^vii]的事务ID`txid`。每当事务向数据库写入任何内容时它所写入的数据都会被标记上写入者的事务ID。
[图7-7](img/fig7-7.png)说明了如何在PostgreSQL中实现基于MVCC的快照隔离【31】其他实现类似。当一个事务开始时它被赋予一个唯一的永远增长[^vii]的事务ID`txid`。每当事务向数据库写入任何内容时它所写入的数据都会被标记上写入者的事务ID。
[^vii]: 事实上事务ID是32位整数所以大约会在40亿次事务之后溢出。 PostgreSQL的Vacuum过程会清理老旧的事务ID确保事务ID溢出回卷不会影响到数据。
@ -340,7 +340,7 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
[^译注ii]: 在PostgreSQL中`created_by` 的实际名称为`xmin``deleted_by` 的实际名称为`xmax`
`UPDATE` 操作在内部翻译为 `DELETE``INSERT` 。例如,在[图7-7]()中事务13 从账户2 中扣除100美元将余额从500美元改为400美元。实际上包含两条账户2 的记录:余额为 \$500 的行被标记为**被事务13删除**,余额为 \$400 的行**由事务13创建**。
`UPDATE` 操作在内部翻译为 `DELETE``INSERT` 。例如,在[图7-7](img/fig7-7.png)中事务13 从账户2 中扣除100美元将余额从500美元改为400美元。实际上包含两条账户2 的记录:余额为 \$500 的行被标记为**被事务13删除**,余额为 \$400 的行**由事务13创建**。
#### 观察一致性快照的可见性规则
@ -351,7 +351,7 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
3. 由具有较晚事务ID在当前事务开始之后开始的的事务所做的任何写入都被忽略而不管这些事务是否已经提交。
4. 所有其他写入,对应用都是可见的。
这些规则适用于创建和删除对象。在[图7-7]()中当事务12 从账户2 读取时,它会看到 \$500 的余额,因为 \$500 余额的删除是由事务13 完成的根据规则3事务12 看不到事务13 执行的删除且400美元记录的创建也是不可见的按照相同的规则
这些规则适用于创建和删除对象。在[图7-7](img/fig7-7.png)中当事务12 从账户2 读取时,它会看到 \$500 的余额,因为 \$500 余额的删除是由事务13 完成的根据规则3事务12 看不到事务13 执行的删除且400美元记录的创建也是不可见的按照相同的规则
换句话说,如果以下两个条件都成立,则可见一个对象:
@ -384,7 +384,7 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
到目前为止已经讨论的**读已提交**和**快照隔离**级别,主要保证了**只读事务在并发写入时**可以看到什么。却忽略了两个事务并发写入的问题——我们只讨论了[脏写](#脏写),一种特定类型的写-写冲突是可能出现的。
并发的写入事务之间还有其他几种有趣的冲突。其中最着名的是**丢失更新lost update** 问题,如[图7-1]()所示,以两个并发计数器增量为例。
并发的写入事务之间还有其他几种有趣的冲突。其中最着名的是**丢失更新lost update** 问题,如[图7-1](img/fig7-1.png)所示,以两个并发计数器增量为例。
如果应用从数据库中读取一些值,修改它并写回修改的值(读取-修改-写入序列),则可能会发生丢失更新的问题。如果两个事务同时执行,则其中一个的修改可能会丢失,因为第二个写入的内容并没有包括第一个事务的修改(有时会说后面写入**狠揍clobber** 了前面的写入)这种模式发生在各种不同的情况下:
@ -656,7 +656,7 @@ VoltDB还使用存储过程进行复制但不是将事务的写入结果从
由于跨分区事务具有额外的协调开销,所以它们比单分区事务慢得多。 VoltDB报告的吞吐量大约是每秒1000个跨分区写入比单分区吞吐量低几个数量级并且不能通过增加更多的机器来增加【49】。
事务是否可以是划分至单个分区很大程度上取决于应用数据的结构。简单的键值数据通常可以非常容易地进行分区,但是具有多个二级索引的数据可能需要大量的跨分区协调(参阅“[分片与次级索引](ch6.md#分片与次级索引)”)。
事务是否可以是划分至单个分区很大程度上取决于应用数据的结构。简单的键值数据通常可以非常容易地进行分区,但是具有多个二级索引的数据可能需要大量的跨分区协调(参阅“[分区与次级索引](ch6.md#分区与次级索引)”)。
#### 串行执行小结
@ -684,7 +684,7 @@ VoltDB还使用存储过程进行复制但不是将事务的写入结果从
两阶段锁定定类似,但使锁的要求更强。只要没有写入,就允许多个事务同时读取同一个对象。但对象只要有写入(修改或删除),就需要**独占访问exclusive access** 权限:
- 如果事务A读取了一个对象并且事务B想要写入该对象那么B必须等到A提交或中止才能继续。 这确保B不能在A底下意外地改变对象。
- 如果事务A写入了一个对象并且事务B想要读取该对象则B必须等到A提交或中止才能继续。 (像[图7-1]()那样读取旧版本的对象在2PL下是不可接受的。
- 如果事务A写入了一个对象并且事务B想要读取该对象则B必须等到A提交或中止才能继续。 (像[图7-1](img/fig7-1.png)那样读取旧版本的对象在2PL下是不可接受的。
在2PL中写入不仅会阻塞其他写入也会阻塞读反之亦然。快照隔离使得**读不阻塞写,写也不阻塞读**(参阅“[实现快照隔离](#实现快照隔离)”这是2PL和快照隔离之间的关键区别。另一方面因为2PL提供了可序列化的性质它可以防止早先讨论的所有竞争条件包括丢失更新和写入偏差。
@ -791,7 +791,7 @@ WHERE room_id = 123 AND
#### 检测旧MVCC读取
回想一下快照隔离通常是通过多版本并发控制MVCC见[图7-10]()来实现的。当一个事务从MVCC数据库中的一致快照读时它将忽略取快照时尚未提交的任何其他事务所做的写入。在[图7-10]()中事务43 认为Alice的 `on_call = true` 因为事务42修改Alice的待命状态未被提交。然而在事务43想要提交时事务42 已经提交。这意味着在读一致性快照时被忽略的写入已经生效事务43 的前提不再为真。
回想一下快照隔离通常是通过多版本并发控制MVCC见[图7-10](img/fig7-10.png)来实现的。当一个事务从MVCC数据库中的一致快照读时它将忽略取快照时尚未提交的任何其他事务所做的写入。在[图7-10](img/fig7-10.png)中事务43 认为Alice的 `on_call = true` 因为事务42修改Alice的待命状态未被提交。然而在事务43想要提交时事务42 已经提交。这意味着在读一致性快照时被忽略的写入已经生效事务43 的前提不再为真。
![](img/fig7-10.png)
@ -815,7 +815,7 @@ WHERE room_id = 123 AND
当事务写入数据库时,它必须在索引中查找最近曾读取受影响数据的其他事务。这个过程类似于在受影响的键范围上获取写锁,但锁并不会阻塞事务指导其他读事务完成,而是像警戒线一样只是简单通知其他事务:你们读过的数据可能不是最新的啦。
在[图7-11]()中事务43 通知事务42 其先前读已过时反之亦然。事务42首先提交并成功尽管事务43 的写影响了42 但因为事务43 尚未提交所以写入尚未生效。然而当事务43 想要提交时来自事务42 的冲突写入已经被提交所以事务43 必须中止。
在[图7-11](img/fig7-11.png)中事务43 通知事务42 其先前读已过时反之亦然。事务42首先提交并成功尽管事务43 的写影响了42 但因为事务43 尚未提交所以写入尚未生效。然而当事务43 想要提交时来自事务42 的冲突写入已经被提交所以事务43 必须中止。
#### 可序列化的快照隔离的性能

4
ch8.md
View File

@ -325,7 +325,7 @@
**图8-3 客户端B的写入比客户端A的写入要晚但是B的写入具有较早的时间戳。**
在[图8-3]()中当一个写入被复制到其他节点时它会根据发生写入的节点上的时钟时钟标记一个时间戳。在这个例子中时钟同步是非常好的节点1和节点3之间的偏差小于3ms这可能比你在实践中预期的更好。
在[图8-3](img/fig8-3.png)中当一个写入被复制到其他节点时它会根据发生写入的节点上的时钟时钟标记一个时间戳。在这个例子中时钟同步是非常好的节点1和节点3之间的偏差小于3ms这可能比你在实践中预期的更好。
尽管如此,[图8-3](img/fig8-3.png)中的时间戳却无法正确排列事件:写入`x = 1`的时间戳为42.004秒,但写入`x = 2`的时间戳为42.003秒,即使`x = 2`在稍后出现。当节点2接收到这两个事件时会错误地推断出`x = 1`是最近的值,而丢弃写入`x = 2`。效果上表现为客户端B的增量操作会丢失。
@ -495,7 +495,7 @@ while(true){
#### 防护令牌
当使用锁或租约来保护对某些资源(如[图8-4](img/fig8-4.png)中的文件存储)的访问时,需要确保一个被误认为自己是“天选者”的节点不能扰乱系统的其它部分。实现这一目标的一个相当简单的技术就是**防护fencing**,如[图8-5]()所示
当使用锁或租约来保护对某些资源(如[图8-4](img/fig8-4.png)中的文件存储)的访问时,需要确保一个被误认为自己是“天选者”的节点不能扰乱系统的其它部分。实现这一目标的一个相当简单的技术就是**防护fencing**,如[图8-5](img/fig8-5.png)所示
![](img/fig8-5.png)

6
ch9.md
View File

@ -114,7 +114,7 @@
* $cas(x, v_{old}, v_{new})⇒r$ 表示客户端请求进行原子性的[**比较与设置**](ch7.md#比较并设置CAS)操作。如果寄存器 $x$ 的当前值等于 $v_{old}$ ,则应该原子地设置为 $v_{new}$ 。如果 $x≠v_{old}$ ,则操作应该保持寄存器不变并返回一个错误。 $r$ 是数据库的响应(正确或错误)。
[图9-4]()中的每个操作都在我们认为执行操作的时候用竖线标出(在每个操作的条柱之内)。这些标记按顺序连在一起,其结果必须是一个有效的寄存器读写序列(**每次读取都必须返回最近一次写入设置的值**)。
[图9-4](img/fig9-4.png)中的每个操作都在我们认为执行操作的时候用竖线标出(在每个操作的条柱之内)。这些标记按顺序连在一起,其结果必须是一个有效的寄存器读写序列(**每次读取都必须返回最近一次写入设置的值**)。
线性一致性的要求是,操作标记的连线总是按时间(从左到右)向前移动,而不是向后移动。这个要求确保了我们之前讨论的新鲜性保证:一旦新的值被写入或读取,所有后续的读都会看到写入的值,直到它被再次覆盖。
@ -122,7 +122,7 @@
**图9-4 可视化读取和写入看起来已经生效的时间点。 B的最后读取不是线性一致性的**
[图9-4]()中有一些有趣的细节需要指出:
[图9-4](img/fig9-4.png)中有一些有趣的细节需要指出:
* 第一个客户端B发送一个读取 `x` 的请求然后客户端D发送一个请求将 `x` 设置为 `0`然后客户端A发送请求将 `x` 设置为 `1`。尽管如此返回到B的读取值为 `1`由A写入的值。这是可以的这意味着数据库首先处理D的写入然后是A的写入最后是B的读取。虽然这不是请求发送的顺序但这是一个可以接受的顺序因为这三个请求是并发的。也许B的读请求在网络上略有延迟所以它在两次写入之后才到达数据库。
@ -238,7 +238,7 @@
在[图9-6](img/fig9-6.png)中,$x$ 的初始值为0写入客户端通过向所有三个副本 $n = 3, w = 3$ )发送写入将 $x$ 更新为 `1`。客户端A并发地从两个节点组成的法定人群 $r = 2$ )中读取数据,并在其中一个节点上看到新值 `1` 。客户端B也并发地从两个不同的节点组成的法定人数中读取并从两个节点中取回了旧值 `0`
法定人数条件满足( $w + r> n$ 但是这个执行是非线性一致的B的请求在A的请求完成后开始但是B返回旧值而A返回新值。 又一次如同Alice和Bob的例子 [图9-1]()
法定人数条件满足( $w + r> n$ 但是这个执行是非线性一致的B的请求在A的请求完成后开始但是B返回旧值而A返回新值。 又一次如同Alice和Bob的例子 [图9-1](img/fig9-1.png)
有趣的是通过牺牲性能可以使Dynamo风格的法定人数线性化读取者必须在将结果返回给应用之前同步执行读修复参阅“[读时修复与反熵过程](ch5.md#读时修复与反熵过程)”) 并且写入者必须在发送写入之前读取法定数量节点的最新状态【24,25】。然而由于性能损失Riak不执行同步读修复【26】。 Cassandra在进行法定人数读取时**确实**在等待读修复完成【27】但是由于使用了最后写入胜利的冲突解决方案当同一个键有多个并发写入时将不能保证线性一致性。