format fix

This commit is contained in:
Vonng 2018-04-05 22:40:35 +08:00
parent bae11bab5a
commit 2b4d7d5606
4 changed files with 64 additions and 60 deletions

View File

@ -54,16 +54,16 @@
## 目录
### [](preface.md)
### [](preface.md)
### [数据系统的基石](part-i.md)
### [第一部分:数据系统的基石](part-i.md)
* [第一章:可靠性、可扩展性、可维护性](ch1.md)
* [第二章:数据模型与查询语言](ch2.md)
* [第三章:存储与检索](ch3.md)
* [第四章:编码与演化](ch4.md)
### [分布式数据](part-ii.md)
### [第二部分:分布式数据](part-ii.md)
* [第五章:复制](ch5.md)
* [第六章:分区](ch6.md)
@ -71,7 +71,7 @@
* [第八章:分布式系统的麻烦](ch8.md)
* [第九章:一致性与共识](ch9.md)
### [生数据](part-iii.md)
### [第三部分:衍生数据](part-iii.md)
* [第十章:批处理](ch10.md)
* [第十一章:流处理](ch11.md)
@ -83,8 +83,6 @@
## 翻译计划
* 机翻:只在乎结构:梳理文章结构、图片、引用、备注。
@ -93,26 +91,26 @@
精翻可以看,机翻基本没法看,初翻对于业内人士能凑合看。
| 章节 | 进度 | 锁定 |
| :--------------------------------: | :------: | :---: |
| 序言 | 初翻 | |
| 第一部分:数据系统基础 ——概览 | 精翻 | |
| 第一章:可靠性、可扩展性、可维护性 | 精翻 | |
| 第二章:数据模型与查询语言 | 初翻 | |
| 第三章:存储与检索 | 初翻 | |
| 第四章:编码与演化 | 初翻 | |
| 第二部分:分布式数据——概览 | 精翻 | |
| 第五章:复制 | 精翻 30% | Vonng |
| 第六章:分区 | 初翻 | |
| 第七章:事务 | 精翻 60% | Vonng |
| 第八章:分布式系统中的问题 | 初翻 | |
| 第九章:一致性与共识 | 初翻 | Vonng |
| 第三部分:前言 | 精翻 | |
| 第十章:批处理 | 草翻 | |
| 第十一章:流处理 | 草翻 | |
| 第十二章:数据系统的未来 | 草翻 | |
| 术语表 | - | |
| 后记 | 机翻 | |
| 章节 | 进度 | 锁定 |
| :--------------------------------: | :------: | :-----------: |
| 序言 | 初翻 | |
| 第一部分:数据系统基础 | 精翻 | |
| 第一章:可靠性、可扩展性、可维护性 | 精翻 | |
| 第二章:数据模型与查询语言 | 初翻 | @ jiajiadebug |
| 第三章:存储与检索 | 初翻 40% | Vonng |
| 第四章:编码与演化 | 初翻 | |
| 第二部分:分布式数据 | 精翻 | |
| 第五章:复制 | 精翻 30% | Vonng |
| 第六章:分区 | 初翻 | |
| 第七章:事务 | 精翻 60% | Vonng |
| 第八章:分布式系统中的问题 | 初翻 | |
| 第九章:一致性与共识 | 初翻 | |
| 第三部分:衍生数据 | 精翻 | |
| 第十章:批处理 | 草翻 | |
| 第十一章:流处理 | 草翻 | |
| 第十二章:数据系统的未来 | 草翻 | |
| 术语表 | - | |
| 后记 | 机翻 | |
@ -127,8 +125,7 @@ All contribution will give proper credit. 贡献者需要同意[法律声明](#
1. [序言初翻修正](https://github.com/Vonng/ddia/commit/afb5edab55c62ed23474149f229677e3b42dfc2c) by [@seagullbird](https://github.com/Vonng/ddia/commits?author=seagullbird)
2. [第一章语法标点校正](https://github.com/Vonng/ddia/commit/973b12cd8f8fcdf4852f1eb1649ddd9d187e3644) by [@nevertiree](https://github.com/Vonng/ddia/commits?author=nevertiree)
3. [第六章部分校正](https://github.com/Vonng/ddia/commit/d4eb0852c0ec1e93c8aacc496c80b915bb1e6d48) by @[MuAlex](https://github.com/Vonng/ddia/commits?author=MuAlex)
4. 第一部分前言ch2校正 by @jiajiadebug

49
ch3.md
View File

@ -205,7 +205,7 @@ Lucene是Elasticsearch和Solr使用的一种全文搜索的索引引擎它使
还有不同的策略来确定SSTables如何被压缩和合并的顺序和时间。最常见的选择是大小分层压实。 LevelDB和RocksDB使用平坦压缩LevelDB因此得名HBase使用大小分层Cassandra同时支持【16】。在规模级别的调整中更新和更小的SSTables先后被合并到更老的和更大的SSTable中。在水平压实中关键范围被拆分成更小的SSTables而较旧的数据被移动到单独的“水平”这使得压缩能够更加递增地进行并且使用更少的磁盘空间。
即使有许多微妙的东西LSM树的基本思想 - 保存一系列在后台合并的SSTables - 简单而有效。即使数据集比可用内存大得多它仍能继续正常工作。由于数据按排序顺序存储因此可以高效地执行范围查询扫描所有高于某些最小值和最高值的所有键并且因为磁盘写入是连续的所以LSM-tree可以支持非常高的写入吞吐量。
即使有许多微妙的东西LSM树的基本思想 —— 保存一系列在后台合并的SSTables —— 简单而有效。即使数据集比可用内存大得多它仍能继续正常工作。由于数据按排序顺序存储因此可以高效地执行范围查询扫描所有高于某些最小值和最高值的所有键并且因为磁盘写入是连续的所以LSM可以支持非常高的写入吞吐量。
@ -215,7 +215,7 @@ Lucene是Elasticsearch和Solr使用的一种全文搜索的索引引擎它使
像SSTables一样B树保持按键排序的键值对这允许高效的键值查找和范围查询。但这就是相似之处的结尾B树有着非常不同的设计理念。
我们前面看到的日志结构索引将数据库分解为可变大小的段通常是几兆字节或更大的大小并且总是按顺序编写段。相比之下B树将数据库分解成固定大小的块或页面传统上大小为4 KB有时会更大并且一次只能读取或写入一个页面。这种设计更接近于底层硬件因为磁盘也被安排在固定大小的块中。
我们前面看到的日志结构索引将数据库分解为可变大小的段通常是几兆字节或更大的大小并且总是按顺序编写段。相比之下B树将数据库分解成固定大小的块或页面传统上大小为4KB有时会更大并且一次只能读取或写入一个页面。这种设计更接近于底层硬件因为磁盘也被安排在固定大小的块中。
每个页面都可以使用地址或位置来标识,这允许一个页面引用另一个页面 —— 类似于指针,但在磁盘而不是在内存中。我们可以使用这些页面引用来构建一个页面树,如[图3-6](img/fig3-6.png)所示。
@ -225,11 +225,11 @@ Lucene是Elasticsearch和Solr使用的一种全文搜索的索引引擎它使
一个页面会被指定为B树的根在索引中查找一个键时就从这里开始。该页面包含几个键和对子页面的引用。每个子页面负责一段连续范围的键引用之间的键指明了引用子页面的键范围。
在[图3-6](img/fig3-6.png)的例子中我们正在寻找关键字251所以我们知道我们需要遵循边界200和300之间的页面引用。这将我们带到一个类似的页面进一步打破了200-300到子范围。
在[图3-6](img/fig3-6.png)的例子中,我们正在寻找关键字 251 ,所以我们知道我们需要遵循边界 200 300 之间的页面引用。这将我们带到一个类似的页面进一步打破了200 - 300到子范围。
最后,我们可以看到包含单个键(叶页)的页面,该页面包含每个键的内联值,或者包含对可以找到值的页面的引用。
在B树的一个页面中对子页面的引用的数量称为分支因子。例如在[图3-6](img/fig3-6.png)中分支因子是6 。在实践中,分支因子取决于存储页面参考和范围边界所需的空间量,但通常是几百个。
在B树的一个页面中对子页面的引用的数量称为分支因子。例如在[图3-6](img/fig3-6.png)中,分支因子是 6 。在实践中,分支因子取决于存储页面参考和范围边界所需的空间量,但通常是几百个。
如果要更新B树中现有键的值则搜索包含该键的叶页更改该页中的值并将该页写回到磁盘对该页的任何引用保持有效 。如果你想添加一个新的键,你需要找到其范围包含新键的页面,并将其添加到该页面。如果页面中没有足够的可用空间容纳新键,则将其分成两个半满页面,并更新父页面以解释键范围的新分区,如[图3-7](img/fig3-7.png)所示[^ii]。
@ -239,7 +239,7 @@ Lucene是Elasticsearch和Solr使用的一种全文搜索的索引引擎它使
**图3-7 通过分割页面来生长B树**
该算法确保树保持平衡:具有 n 个键的B树总是具有$O(log n)$的深度。大多数数据库可以放入一个三到四层的B树所以你不需要遵循许多页面引用来找到你正在查找的页面。 分支因子为500的4KB页面的四级树可以存储多达256 TB。
该算法确保树保持平衡:具有 n 个键的B树总是具有 $O(log n)$ 的深度。大多数数据库可以放入一个三到四层的B树所以你不需要遵追踪多页面引用来找到你正在查找的页面。 (分支因子为 500 4KB 页面的四级树可以存储多达 256TB 。)
#### 让B树更可靠
@ -249,9 +249,9 @@ B树的基本底层写操作是用新数据覆盖磁盘上的页面。假定覆
而且,一些操作需要覆盖几个不同的页面。例如,如果因为插入导致页面过度而拆分页面,则需要编写已拆分的两个页面,并覆盖其父页面以更新对两个子页面的引用。这是一个危险的操作,因为如果数据库在仅有一些页面被写入后崩溃,那么最终将导致一个损坏的索引(例如,可能有一个孤儿页面不是任何父项的子项) 。
为了使数据库对崩溃具有韧性B树实现通常会带有一个额外的磁盘数据结构**预写式日志(write-ahead-log**WAL也称为重做日志。这是一个只能追加的文件每个B树修改都可以应用到树本身的页面上。当数据库在崩溃后恢复时这个日志被用来使B树恢复到一致的状态【5,20】。
为了使数据库对崩溃具有韧性B树实现通常会带有一个额外的磁盘数据结构**预写式日志(WAL, write-ahead-log**(也称为**重做日志redo log**)。这是一个仅追加的文件每个B树修改都可以应用到树本身的页面上。当数据库在崩溃后恢复时这个日志被用来使B树恢复到一致的状态【5,20】。
更新页面的一个额外的复杂情况是如果多个线程要同时访问B树则需要仔细的并发控制 - 否则线程可能会看到树处于不一致的状态。这通常通过使用**锁存器latches**(轻量级锁)保护树的数据结构来完成。日志结构化的方法在这方面更简单,因为它们在后台进行所有的合并,而不会干扰传入的查询,并且不时地将旧的分段原子交换为新的分段。
更新页面的一个额外的复杂情况是如果多个线程要同时访问B树则需要仔细的并发控制 —— 否则线程可能会看到树处于不一致的状态。这通常通过使用**锁存器latches**(轻量级锁)保护树的数据结构来完成。日志结构化的方法在这方面更简单,因为它们在后台进行所有的合并,而不会干扰传入的查询,并且不时地将旧的分段原子交换为新的分段。
#### B树优化
@ -259,13 +259,13 @@ B树的基本底层写操作是用新数据覆盖磁盘上的页面。假定覆
* 一些数据库如LMDB使用写时复制方案【21】而不是覆盖页面并维护WAL进行崩溃恢复。修改的页面被写入到不同的位置并且树中的父页面的新版本被创建指向新的位置。这种方法对于并发控制也很有用我们将在“[快照隔离和可重复读](ch7.md#快照隔离和可重复读)”中看到。
* 我们可以通过不存储整个键来节省页面空间,但可以缩小它的大小。特别是在树内部的页面上,键只需要提供足够的信息来充当键范围之间的边界。在页面中包含更多的键允许树具有更高的分支因子,因此更少的层次
* 通常,页面可以放置在磁盘上的任何位置;没有什么要求附近的键范围页面附近的磁盘上。如果查询需要按照排序顺序扫描大部分关键字范围那么每个页面的布局可能会非常不方便因为每个读取的页面都可能需要磁盘查找。因此许多B-树实现尝试布局树使得叶子页面按顺序出现在磁盘上。但是随着树的增长维持这个顺序是很困难的。相比之下由于LSM树在合并过程中一次又一次地重写存储的大部分所以它们更容易使顺序键在磁盘上彼此靠近。
* 通常,页面可以放置在磁盘上的任何位置没有什么要求附近的键范围页面附近的磁盘上。如果查询需要按照排序顺序扫描大部分关键字范围那么每个页面的布局可能会非常不方便因为每个读取的页面都可能需要磁盘查找。因此许多B树实现尝试布局树使得叶子页面按顺序出现在磁盘上。但是随着树的增长维持这个顺序是很困难的。相比之下由于LSM树在合并过程中一次又一次地重写存储的大部分所以它们更容易使顺序键在磁盘上彼此靠近。
* 额外的指针已添加到树中。例如,每个叶子页面可以在左边和右边具有对其兄弟页面的引用,这允许不跳回父页面就能顺序扫描。
* B树的变体如分形树[22]借用一些日志结构的思想来减少磁盘寻道(而且它们与分形无关)。
* B树的变体如分形树【22】借用一些日志结构的思想来减少磁盘寻道(而且它们与分形无关)。
### 比较B树和LSM树
尽管B树实现通常比LSM树实现更成熟但LSM树由于其性能特点也非常有趣。根据经验LSM树通常写速度更快而B树被认为读取速度更快【23】。 LSM树上的读取通常比较慢因为他们必须在压缩的不同阶段检查几个不同的数据结构和SSTables。
尽管B树实现通常比LSM树实现更成熟但LSM树由于其性能特点也非常有趣。根据经验通常LSM树的写入速度更快而B树的读取速度更快【23】。 LSM树上的读取通常比较慢因为它们必须在压缩的不同阶段检查几个不同的数据结构和SSTables。
然而,基准通常对工作量的细节不确定和敏感。 您需要测试具有特定工作负载的系统,以便进行有效的比较。 在本节中,我们将简要讨论一些在衡量存储引擎性能时值得考虑的事情。
@ -273,11 +273,11 @@ B树的基本底层写操作是用新数据覆盖磁盘上的页面。假定覆
B树索引必须至少两次写入每一段数据一次写入预先写入日志一次写入树页面本身也许再次分页。即使在该页面中只有几个字节发生了变化也需要一次编写整个页面的开销。有些存储引擎甚至会覆盖同一个页面两次以免在电源故障的情况下导致页面部分更新【24,25】。
由于反复压缩和合并SSTables日志结构索引也会重写数据。这种影响 - 在数据库的生命周期中写入数据库导致对磁盘的多次写入 - 被称为**写放大Write amplification**。固态硬盘是特别值得关注的,固态硬盘在磨损之前只能覆盖一段时间。
由于反复压缩和合并SSTables日志结构索引也会重写数据。这种影响 —— 在数据库的生命周期中写入数据库导致对磁盘的多次写入 —— 被称为**写放大write amplification**。需要特别关注的是固态硬盘,固态硬盘在磨损之前只能覆写一段时间。
在写入繁重的应用程序中,性能瓶颈可能是数据库可以写入磁盘的速度。在这种情况下,写入放大具有直接的性能成本:存储引擎写入磁盘的次数越多,可用磁盘带宽内的每秒写入次数越少。
在写入繁重的应用程序中,性能瓶颈可能是数据库可以写入磁盘的速度。在这种情况下,写放大会导致直接的性能代价:存储引擎写入磁盘的次数越多,可用磁盘带宽内的每秒写入次数越少。
而且LSM树通常能够比B-树支持更高的写入吞吐量,部分原因是它们有时具有较低的写放大尽管这取决于存储引擎配置和工作负载部分是因为它们顺序地写入紧凑的SSTable文件而不是必须覆盖树中的几个页面【26】。这种差异在磁性硬盘驱动器上尤其重要顺序写入比随机写入快得多。
而且LSM树通常能够比B树支持更高的写入吞吐量部分原因是它们有时具有较低的写放大尽管这取决于存储引擎配置和工作负载部分是因为它们顺序地写入紧凑的SSTable文件而不是必须覆盖树中的几个页面【26】。这种差异在磁性硬盘驱动器上尤其重要顺序写入比随机写入快得多。
LSM树可以被压缩得更好因此经常比B树在磁盘上产生更小的文件。 B树存储引擎会由于分割而留下一些未使用的磁盘空间当页面被拆分或某行不能放入现有页面时页面中的某些空间仍未被使用。由于LSM树不是面向页面的并且定期重写SSTables以去除碎片所以它们具有较低的存储开销特别是当使用平坦压缩时【27】。
@ -287,7 +287,7 @@ LSM树可以被压缩得更好因此经常比B树在磁盘上产生更小的
日志结构存储的缺点是压缩过程有时会干扰正在进行的读写操作。尽管存储引擎尝试逐步执行压缩而不影响并发访问,但是磁盘资源有限,所以很容易发生请求需要等待而磁盘完成昂贵的压缩操作。对吞吐量和平均响应时间的影响通常很小,但是在更高百分比的情况下(参阅“[描述性能](ch1.md#描述性能)”对日志结构化存储引擎的查询响应时间有时会相当长而B树的行为则相对更具可预测性【28】。
压缩的另一个问题出现在高写入吞吐量:磁盘的有限写入带宽需要在初始写入(记录和刷新memtable到磁盘)和在后台运行的压缩线程之间共享。写入空数据库时,可以使用全磁盘带宽进行初始写入,但数据库越大,压缩所需的磁盘带宽就越多。
压缩的另一个问题出现在高写入吞吐量:磁盘的有限写入带宽需要在初始写入(记录和刷新内存表到磁盘)和在后台运行的压缩线程之间共享。写入空数据库时,可以使用全磁盘带宽进行初始写入,但数据库越大,压缩所需的磁盘带宽就越多。
如果写入吞吐量很高并且压缩没有仔细配置压缩跟不上写入速率。在这种情况下磁盘上未合并段的数量不断增加直到磁盘空间用完读取速度也会减慢因为它们需要检查更多段文件。通常情况下即使压缩无法跟上基于SSTable的存储引擎也不会限制传入写入的速率所以您需要进行明确的监控来检测这种情况【29,30】。
@ -361,7 +361,7 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
最近的研究表明内存数据库体系结构可以扩展到支持比可用内存更大的数据集而不必重新采用以磁盘为中心的体系结构【45】。所谓的**反缓存anti-caching**方法通过在内存不足的情况下将最近最少使用的数据从内存转移到磁盘并在将来再次访问时将其重新加载到内存中。这与操作系统对虚拟内存和交换文件的操作类似但数据库可以比操作系统更有效地管理内存因为它可以按单个记录的粒度工作而不是整个内存页面。尽管如此这种方法仍然需要索引能完全放入内存中就像本章开头的Bitcask例子
如果非易失性存储器NVM技术得到更广泛的应用可能还需要进一步改变存储引擎设计【46】。目前这是一个新的研究领域值得关注。
如果**非易失性存储器NVM**技术得到更广泛的应用可能还需要进一步改变存储引擎设计【46】。目前这是一个新的研究领域值得关注。
@ -390,7 +390,7 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
| 处理的数据 | 数据的最新状态(当前时间点) | 随时间推移的历史事件 |
| 数据集尺寸 | GB ~ TB | TB ~ PB |
起初,相同的数据库用于事务处理和分析查询。 SQL在这方面证明是非常灵活的对于OLTP类型的查询以及OLAP类型的查询来说效果很好。尽管如此在二十世纪八十年代末和九十年代初期公司有停止使用OLTP系统进行分析的趋势而是在单独的数据库上运行分析。这个单独的数据库被称为**数据仓库**。
起初,相同的数据库用于事务处理和分析查询。 SQL在这方面证明是非常灵活的对于OLTP类型的查询以及OLAP类型的查询来说效果很好。尽管如此在二十世纪八十年代末和九十年代初期公司有停止使用OLTP系统进行分析的趋势而是在单独的数据库上运行分析。这个单独的数据库被称为**数据仓库data warehouse**。
### 数据仓库
@ -404,7 +404,7 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
**图3-8 ETL至数据仓库的简化提纲**
几乎所有的大型企业都有数据仓库但在小型企业中几乎闻所未闻。这可能是因为大多数小公司没有这么多不同的OLTP系统大多数小公司只有少量的数据——可以在传统的SQL数据库中查询甚至可以在电子表格中分析。在一家大公司里要做一些在一家小公司很简单的事情需要很多繁重的工作。
几乎所有的大型企业都有数据仓库但在小型企业中几乎闻所未闻。这可能是因为大多数小公司没有这么多不同的OLTP系统大多数小公司只有少量的数据 —— 可以在传统的SQL数据库中查询甚至可以在电子表格中分析。在一家大公司里要做一些在一家小公司很简单的事情需要很多繁重的工作。
使用单独的数据仓库而不是直接查询OLTP系统进行分析的一大优势是数据仓库可针对分析访问模式进行优化。事实证明本章前半部分讨论的索引算法对于OLTP来说工作得很好但对于回答分析查询并不是很好。在本章的其余部分中我们将看看为分析而优化的存储引擎。
@ -495,7 +495,7 @@ GROUP BY
**图3-11 压缩位图索引存储布局**
通常情况下一列中不同值的数量与行数相比较小例如零售商可能有数十亿的销售交易但只有100,000个不同的产品。现在我们可以得到一个有 n 个不同值的列,并把它转换成 n 个独立的位图每个不同值的一个位图每行一位。如果该行具有该值则该位为1否则为0。
通常情况下一列中不同值的数量与行数相比较小例如零售商可能有数十亿的销售交易但只有100,000个不同的产品。现在我们可以得到一个有 n 个不同值的列,并把它转换成 n 个独立的位图:每个不同值的一个位图,每行一位。如果该行具有该值,则该位为 1 ,否则为 0
如果 n 非常小(例如,国家/地区列可能有大约200个不同的值则这些位图可以每行存储一位。但是如果n更大大部分位图中将会有很多的零我们说它们是稀疏的。在这种情况下位图可以另外进行游程编码如[图3-11](fig3-11.png)底部所示。这可以使列的编码非常紧凑。
@ -554,7 +554,7 @@ WHERE product_sk = 31 AND store_sk = 3
使用B树的更新就地方法对于压缩的列是不可能的。如果你想在排序表的中间插入一行你很可能不得不重写所有的列文件。由于行由列中的位置标识因此插入必须始终更新所有列。
幸运的是本章前面已经看到了一个很好的解决方案LSM树。所有的写操作首先进入一个内存中的存储在这里它们被添加到一个已排序的结构中并准备写入磁盘。内存中的存储是面向行还是列的这并不重要。当已经积累了足够的写入数据时它们将与磁盘上的列文件合并并批量写入新文件。这基本上是Vertica所做的[62]
幸运的是本章前面已经看到了一个很好的解决方案LSM树。所有的写操作首先进入一个内存中的存储在这里它们被添加到一个已排序的结构中并准备写入磁盘。内存中的存储是面向行还是列的这并不重要。当已经积累了足够的写入数据时它们将与磁盘上的列文件合并并批量写入新文件。这基本上是Vertica所做的【62】
查询需要检查磁盘上的列数据和最近在内存中的写入,并将两者结合起来。但是,查询优化器隐藏了用户的这个区别。从分析师的角度来看,通过插入,更新或删除操作进行修改的数据会立即反映在后续查询中。
@ -588,15 +588,20 @@ WHERE product_sk = 31 AND store_sk = 3
在本章中,我们试图深入了解数据库如何处理存储和检索。将数据存储在数据库中会发生什么,以及稍后再次查询数据时数据库会做什么?
在高层次上我们看到存储引擎分为两大类优化事务处理OLTP和优化分析OLAP的类别。这些用例的访问模式之间有很大的区别
在高层次上,我们看到存储引擎分为两大类:优化**事务处理OLTP****优化分析OLAP**的类别。这些用例的访问模式之间有很大的区别:
* OLTP系统通常面向用户这意味着他们可能会看到大量的请求。为了处理负载应用程序通常只触及每个查询中的少量记录。应用程序使用某种键来请求记录存储引擎使用索引来查找所请求的键的数据。磁盘寻道时间往往是这里的瓶颈。
* 数据仓库和类似的分析系统不太知名因为它们主要由业务分析人员使用而不是由最终用户使用。它们处理比OLTP系统少得多的查询量但是每个查询通常要求很高需要在短时间内扫描数百万条记录。磁盘带宽不是查找时间往往是瓶颈列式存储是这种工作负载越来越流行的解决方案。
在OLTP方面我们看到了来自两大主流学派的存储引擎
* 日志结构学派,只允许附加到文件和删除过时的文件,但不会更新已经写入的文件。 BitcaskSSTablesLSM树LevelDBCassandraHBaseLucene等都属于这个组。
* 就地更新学派,将磁盘视为一组可以覆盖的固定大小的页面。 B树是这种哲学的最大的例子被用在所有主要的关系数据库中还有许多非关系数据库。
***日志结构学派***
只允许附加到文件和删除过时的文件,但不会更新已经写入的文件。 BitcaskSSTablesLSM树LevelDBCassandraHBaseLucene等都属于这个组。
***就地更新学派***
将磁盘视为一组可以覆盖的固定大小的页面。 B树是这种哲学的最大的例子被用在所有主要的关系数据库中还有许多非关系数据库。
日志结构的存储引擎是相对较新的发展。他们的主要想法是他们系统地将随机访问写入顺序写入磁盘由于硬盘驱动器和固态硬盘的性能特点可以实现更高的写入吞吐量。在完成OLTP方面我们通过一些更复杂的索引结构和为保留所有数据而优化的数据库做了一个简短的介绍。

2
ch6.md
View File

@ -406,3 +406,5 @@ Couchbase不会自动重新平衡这简化了设计。通常情况下
| :--------------------: | :-----------------------------: | :--------------------: |
| [第五章:复制](ch5.md) | [设计数据密集型应用](README.md) | [第七章:事务](ch7.md) |
ou

20
ch7.md
View File

@ -475,17 +475,17 @@ UPDATE wiki_pages SET content = '新内容'
首先想象一下这个例子你正在为医院写一个医生轮班管理程序。医院通常会同时要求几位医生待命但底线是至少有一位医生在待命。医生可以放弃他们的班次例如如果他们自己生病了只要至少有一个同事在这一班中继续工作【40,41】。
现在想象一下Alice和Bob是两位值班医生。两人都感到不适所以他们都决定请假。不幸的是他们恰好在同一时间点击按钮关闭电话。[图7-8](img/fig7-8.png)说明了接下来的事情。
现在想象一下Alice和Bob是两位值班医生。两人都感到不适所以他们都决定请假。不幸的是他们恰好在同一时间点击按钮下班。[图7-8](img/fig7-8.png)说明了接下来的事情。
![](img/fig7-8.png)
**图7-8 写入偏差导致应用程序错误的示例**
在两个事务中,应用首先检查是否有两个或以上的医生正在值班;如果是的话,它就假定一名医生可以安全地翘班。由于数据库使用快照隔离两次检查都返回2所以两个事务都进入下一个阶段。爱丽丝更新自己的记录翘班了而鲍勃也干了一样的事情。两个事务都成功提交了,现在没有医生值班了。违反了至少有一名医生在值班的要求。
在两个事务中,应用首先检查是否有两个或以上的医生正在值班;如果是的话,它就假定一名医生可以安全地休班。由于数据库使用快照隔离,两次检查都返回 2 所以两个事务都进入下一个阶段。Alice更新自己的记录休班了而Bob也做了一样的事情。两个事务都成功提交了,现在没有医生值班了。违反了至少有一名医生在值班的要求。
#### 写偏差的特征
这种异常称为**写偏差**【28】。它既不是**脏写**,也不是**丢失更新**因为这两个事务正在更新两个不同的对象Alice和Bob各自的待命记录。在这里发生的冲突并不是那么明显但是这显然是一个竞争条件如果两个事务一个接一个地运行那么第二个医生就不能班了。异常行为只有在事务并发进行时才有可能。
这种异常称为**写偏差**【28】。它既不是**脏写**,也不是**丢失更新**因为这两个事务正在更新两个不同的对象Alice和Bob各自的待命记录。在这里发生的冲突并不是那么明显但是这显然是一个竞争条件如果两个事务一个接一个地运行那么第二个医生就不能班了。异常行为只有在事务并发进行时才有可能。
可以将写入偏差视为丢失更新问题的一般化。如果两个事务读取相同的对象,然后更新其中一些对象(不同的事务可能更新不同的对象),则可能发生写入偏差。在多个事务更新同一个对象的特殊情况下,就会发生脏写或丢失更新(取决于时机)。
@ -543,7 +543,7 @@ COMMIT;
***多人游戏***
在例7-1中我们使用一个锁来防止丢失更新也就是确保两个玩家不能同时移动同一个棋子。但是锁定并不妨碍玩家将两个不同的棋子移动到棋盘上的相同位置或者采取其他违反游戏规则的行为。按照您正在执行的规则类型也许可以使用唯一约束否则您很容易发生写入偏差。
[例7-1]()中,我们使用一个锁来防止丢失更新(也就是确保两个玩家不能同时移动同一个棋子)。但是锁定并不妨碍玩家将两个不同的棋子移动到棋盘上的相同位置,或者采取其他违反游戏规则的行为。按照您正在执行的规则类型,也许可以使用唯一约束,否则您很容易发生写入偏差。
***抢注用户名***
@ -563,11 +563,11 @@ COMMIT;
3. 如果应用决定继续操作,就执行写入(插入、更新或删除),并提交事务。
这个写入的效果改变了步骤2中的前决条件。换句话说如果在提交写入后重复执行一次步骤1的SELECT查询将会得到不同的结果。因为写入改变符合搜索条件的行集现在少了一个医生值班那时候的会议室现在已经被预订了棋盘上的这个位置已经被占据了用户名已经被抢注账户余额不够了
这个写入的效果改变了步骤2 中的先决条件。换句话说如果在提交写入后重复执行一次步骤1 的SELECT查询将会得到不同的结果。因为写入改变符合搜索条件的行集现在少了一个医生值班那时候的会议室现在已经被预订了棋盘上的这个位置已经被占据了用户名已经被抢注账户余额不够了
这些步骤可能以不同的顺序发生。例如可以首先进行写入然后进行SELECT查询最后根据查询结果决定是放弃还是提交。
在医生值班的例子中在步骤3中修改的行是步骤1中返回的行之一所以我们可以通过锁定步骤1中的行`SELECT FOR UPDATE`)来使事务安全并避免写入偏差。但是其他四个例子是不同的:它们检查是否**不存在**某些满足条件的行,写入会**添加**一个匹配相同条件的行。如果步骤1中的查询没有返回任何行则`SELECT FOR UPDATE`锁不了任何东西。
在医生值班的例子中在步骤3中修改的行是步骤1中返回的行之一所以我们可以通过锁定步骤1 中的行(`SELECT FOR UPDATE`)来使事务安全并避免写入偏差。但是其他四个例子是不同的:它们检查是否**不存在**某些满足条件的行,写入会**添加**一个匹配相同条件的行。如果步骤1中的查询没有返回任何行则`SELECT FOR UPDATE`锁不了任何东西。
这种效应:一个事务中的写入改变另一个事务的搜索查询的结果,被称为**幻读**【3】。快照隔离避免了只读查询中幻读但是在像我们讨论的例子那样的读写事务中幻影会导致特别棘手的写歪斜情况。
@ -589,11 +589,11 @@ COMMIT;
- 隔离级别难以理解,并且在不同的数据库中实现的不一致(例如,“可重复读”的含义天差地别)。
- 光检查应用代码很难判断在特定的隔离级别运行是否安全。 特别是在大型应用程序中,您可能并不知道并发发生的所有事情。
- 没有检测竞争条件的好工具。原则上来说静态分析可能会有帮助【26】但研究中的技术还没法实际应用。并发问题的测试是很难的因为它们通常是非确定性的——只有在倒霉的时机下才会出现问题。
- 没有检测竞争条件的好工具。原则上来说静态分析可能会有帮助【26】但研究中的技术还没法实际应用。并发问题的测试是很难的因为它们通常是非确定性的 —— 只有在倒霉的时机下才会出现问题。
这不是一个新问题从20世纪70年代以来就一直是这样了当时首先引入了较弱的隔离级别【2】。一直以来研究人员的答案都很简单使用**可序列化serializable**的隔离级别!
**可序列化Serializability**隔离通常被认为是最强的隔离级别。它保证即使事务可以并行执行,最终的结果也是一样的,就好像它们没有任何并发性,连续挨个执行一样。因此数据库保证,如果事务在单独运行时正常运行,则它们在并发运行时继续保持正确——换句话说,数据库可以防止**所有**可能的竞争条件。
**可序列化Serializability**隔离通常被认为是最强的隔离级别。它保证即使事务可以并行执行,最终的结果也是一样的,就好像它们没有任何并发性,连续挨个执行一样。因此数据库保证,如果事务在单独运行时正常运行,则它们在并发运行时继续保持正确 —— 换句话说,数据库可以防止**所有**可能的竞争条件。
但如果可序列化隔离级别比弱隔离级别的烂摊子要好得多,那为什么没有人见人爱?为了回答这个问题,我们需要看看实现可序列化的选项,以及它们如何执行。目前大多数提供可序列化的数据库都使用了三种技术之一,本章的剩余部分将会介绍这些技术。
@ -780,9 +780,9 @@ WHERE room_id = 123 AND
先前讨论了快照隔离中的写入偏差(参阅“[写入偏差和幻像](#写入偏差与幻读)”)时,我们观察到一个循环模式:事务从数据库读取一些数据,检查查询的结果,并根据它看到的结果决定采取一些操作(写入数据库)。但是,在快照隔离的情况下,原始查询的结果在事务提交时可能不再是最新的,因为数据可能在同一时间被修改。
换句话说,事务基于一个**前提premise**采取行动(事务开始时候的事实,例如:“目前有两名医生正在通话”)。之后当事务要提交时,原始数据可能已经改变——前提可能不再成立。
换句话说,事务基于一个**前提premise**采取行动(事务开始时候的事实,例如:“目前有两名医生正在值班”)。之后当事务要提交时,原始数据可能已经改变——前提可能不再成立。
当应用程序进行查询时(例如,“当前有多少医生正在调用?”),数据库不知道应用逻辑如何使用该查询结果。在这种情况下为了安全,数据库需要假设任何对该结果集的变更都可能会使该事务中的写入变得无效。 换而言之,事务中的查询与写入可能存在因果依赖。为了提供可序列化的隔离级别,如果事务在过时的前提下执行操作,数据库必须能检测到这种情况,并中止事务。
当应用程序进行查询时(例如,“当前有多少医生正在值班?”),数据库不知道应用逻辑如何使用该查询结果。在这种情况下为了安全,数据库需要假设任何对该结果集的变更都可能会使该事务中的写入变得无效。 换而言之,事务中的查询与写入可能存在因果依赖。为了提供可序列化的隔离级别,如果事务在过时的前提下执行操作,数据库必须能检测到这种情况,并中止事务。
数据库如何知道查询结果是否可能已经改变?有两种情况需要考虑: