移除无关笔记

This commit is contained in:
Vonng 2018-03-01 11:53:19 +08:00
parent 1626167141
commit 2413ea0e66
4 changed files with 112 additions and 107 deletions

View File

@ -1,29 +1,33 @@
# db
# 设计数据密集型应用 - 中文翻译
> 不懂数据库的全栈工程师不是好架构师
>
> —— Vonng
## [设计数据密集型应用 - 中文翻译](ddia/README.md)
- 作者: Martin Kleppmann
- 原书名称《Designing Data-Intensive Application》
- 作者: [Martin Kleppmann](https://martin.kleppmann.com)
- 原书名称:[《Designing Data-Intensive Application》](http://shop.oreilly.com/product/0636920032175.do)
- 译者:[冯若航]( http://vonng.com/about) fengruohang@outlook.com
- 建议本地使用[Typora](https://www.typora.io)以获取最佳阅读体验。
-------------
## 法律声明
译者纯粹出于学习目的与个人兴趣翻译,本译文只供学习研究参考之用,不得公开传播发行,用于商业用途。有能力阅读英文书籍者请购买正版支持。
译者保留对译文的署名权,其他权利以原作者和出版社的主张为准,侵删。
## 译序
> 不懂数据库的全栈工程师不是好架构师
>
> —— Vonng
现今尤其是在互联网领域大多数应用都属于数据密集型应用。本书从底层数据结构到顶层架构设计将数据系统设计中的精髓娓娓道来。其中的宝贵经验无论是对架构师DBA、还是后端工程师、甚至产品经理都会有帮助。
这是一本理论结合实践的书,书中很多问题,译者在实际场景中都曾遇到过,读来让人击节扼腕。如果能早点读到这本书,该少走多少弯路啊!
这也是一本深入浅出的书,讲述概念的来龙去脉而不是卖弄定义,介绍事物发展演化历程而不是事实堆砌,将复杂的概念讲述的浅显易懂,但又直击本质不失深度。每章最后的引用质量非常好,是深入学习各个主题的绝佳索引。
本书为数据系统的设计、实现、与评价提供了很好的概念框架。读完并理解本书内容后,读者可以轻松看破大多数的技术忽悠,与技术砖家撕起来虎虎生风🤣。
本书为数据系统的设计、实现、与评价提供了很好的概念框架。读完并理解本书内容后,读者可以轻松看破大多数的技术忽悠,与技术砖家撕起来虎虎生风🤣。
这是2017年译者读过最好的一本技术类书籍这么好的书没有中文翻译实在是遗憾。某不才愿为先进技术文化的传播贡献一分力量。既可以深入学习有趣的技术主题又可以锻炼中英文语言文字功底何乐而不为
@ -35,17 +39,15 @@
> 在我们的社会中,技术是一种强大的力量。数据、软件、通信可以用于坏的方面:不公平的阶级固化,损害公民权利,保护既得利益集团。但也可以用于好的方面:让底层人民发出自己的声音,让每个人都拥有机会,避免灾难。本书献给所有将技术用于善途的人们。
---------
> 计算是一种流行文化,流行文化鄙视历史。 流行文化关乎个体身份和参与感,与合作无关。它活在当下,也与过去和未来无关。 我认为大部分(为钱)写代码的人就是这样, 他们不知道他们的文化来自哪里。
> ​计算是一种流行文化,流行文化鄙视历史。 流行文化关乎个体身份和参与感,但与合作无关。流行文化活在当下,也与过去和未来无关。 我认为大部分(为了钱)编写代码的人就是这样的, 他们不知道自己的文化来自哪里。
>
> ——阿兰·凯接受Dobb博士的杂志采访时2012年
> ——阿兰·凯接受Dobb博士的杂志采访时2012年
## 目录
#### [序言](ddia/preface.md)
## [目录](ddia/README.md)
#### [I. 数据系统基础](ddia/part-i.md)
@ -77,33 +79,31 @@
## 翻译计划
机翻:只在乎结构:梳理文章结构、图片、引用、备注。
初翻:保证自己经完全理解本章内容,人工修复显著的错误,重新组织语言。
精翻:确定术语的最终译法,修复格式瑕疵,着力信达雅。
* 机翻:只在乎结构:梳理文章结构、图片、引用、备注。
* 初翻:保证经完全理解本章内容,人工修复显著的错误,重新组织语言。
* 精翻:阅读相关领域文献书籍,确定术语的最终译法,修复格式瑕疵,着力信达雅。
通常机翻一章1个小时左右初翻一章6小时精翻一章三到五天。
精翻可以看,初翻凑合看,机翻没法看。精翻太累了,看心情吧
精翻可以看,机翻基本没法看,初翻对于业内人士能凑合看。
| 章节 | 进度 |
| ---------------------------------- | ------------ |
| 序言 | 机翻 |
| 第一部分:数据系统基础 ——概览 | 初翻 |
| 第一章:可靠性、可扩展性、可维护性 | **精翻** |
| 第二章:数据模型与查询语言 | 初翻 |
| 第三章:存储与检索 | 初翻 |
| 第四章:编码与演化 | 初翻 |
| 第二部分:分布式数据——概览 | 初翻 |
| 第五章:复制 | 初翻 |
| 第六章:分片 | 初翻 |
| 第七章:事务 | **精翻 50%** |
| 第八章:分布式系统的麻烦 | 机翻 |
| 第九章:一致性与共识 | 机翻 |
| 第三部分:前言 | 机翻 |
| 第十章:批处理 | 机翻 |
| 第十一章:流处理 | 机翻 |
| 第十二章:数据系统的未来 | 机翻 |
| 术语表 | - |
| 后记 | 机翻 |
| 章节 | 计划 | 进度 |
| ---------------------------------- | :--: | ------------ |
| 序言 | | 机翻 |
| 第一部分:数据系统基础 ——概览 | | 初翻 |
| 第一章:可靠性、可扩展性、可维护性 | | **精翻** |
| 第二章:数据模型与查询语言 | | 初翻 |
| 第三章:存储与检索 | | 初翻 |
| 第四章:编码与演化 | | 初翻 |
| 第二部分:分布式数据——概览 | | 初翻 |
| 第五章:复制 | | 初翻 |
| 第六章:分片 | | 初翻 |
| 第七章:事务 | | **精翻 50%** |
| 第八章:分布式系统的麻烦 | | 机翻 |
| 第九章:一致性与共识 | | 机翻 |
| 第三部分:前言 | | 机翻 |
| 第十章:批处理 | | 机翻 |
| 第十一章:流处理 | | 机翻 |
| 第十二章:数据系统的未来 | | 机翻 |
| 术语表 | | - |
| 后记 | | 机翻 |

View File

@ -16,7 +16,7 @@
又是一本深入浅出的书,按照事物发展演化的历程来介绍,将复杂的概念讲述的浅显易懂,但又直击本质,不失深度。每章最后的参考引用质量非常好,是进一步深入学习各个主题的绝佳索引。
本书为系统的设计、实现、与评价提供了很好的概念框架。读完并理解本书内容后,读者可以轻松看破大多数的技术忽悠,与技术砖家撕起来虎虎生风。
本书为系统的设计、实现、与评价提供了很好的概念框架。读完并理解本书内容后,读者可以轻松看破大多数的技术忽悠,与技术砖家撕起来虎虎生风。
这是2017年译者读过最好的一本技术类书籍这么好的书没有中文翻译实在是遗憾。某不才愿为先进技术文化的传播贡献一分力量。既可以深入学习有趣的技术主题又可以锻炼中英文语言文字功底何乐而不为呢
@ -40,7 +40,7 @@
### [数据系统的基石](part-i.md)
1. [可靠性、可扩展性、可维护性](ch1.md)
1. [可靠性、可扩展性、可维护性](ch1.md)
2. [数据模型与查询语言](ch2.md)
3. [存储与检索](ch3.md)
4. [编码与演化](ch4.md)
@ -78,26 +78,26 @@
精翻可以看,初翻凑合看,机翻没法看。精翻太累了,看心情吧。
| 章节 | 文件 | 计划 | 进度 |
| ------ | ------ | ---- | ---- |
| 序言 | [preface.md](preface.md) | | 机翻 |
| 第一部分:数据系统基础 ——概览 | [part-i.md](part-i.md) | | 初翻 |
| 第一章:可靠性、可扩展性、可维护性 | [ch1.md](ch1.md) | | **精翻** |
| 第二章:数据模型与查询语言 | [ch2.md](ch2.md) | | 初翻 |
| 第三章:存储与检索 | [ch3.md](ch3.md) | | 初翻 |
| 第四章:编码与演化 | [ch4.md](ch4.md) | | 初翻 |
| 第二部分:分布式数据——概览 | [part-ii.md](part-ii.md) | | 初翻 |
| 第五章:复制 | [ch5.md](ch5.md) | | 初翻 |
| 第六章:分片 | [ch6.md](ch6.md) | | 初翻 |
| 第七章:事务 | [ch7.md](ch7.md) | | **精翻 50%** |
| 第八章:分布式系统的麻烦 | [ch8.md](ch8.md) | | 机翻 |
| 第九章:一致性与共识 | [ch9.md](ch9.md) | | 机翻 |
| 第三部分:前言 | [part-iii.md](part-iii.md) | | 机翻 |
| 第十章:批处理 | [ch10.md](ch10.md) | | 机翻 |
| 第十一章:流处理 | [ch11.md](ch11.md) | | 机翻 |
| 第十二章:数据系统的未来 | [ch12.md](ch12.md) | | 机翻 |
| 术语表 | [glossary.md](glossary.md) | | - |
| 后记 | [colophon.md](colophon.md) | | 机翻 |
| 章节 | 文件 | 进度 |
| ------ | ------ | ---- |
| 序言 | [preface.md](preface.md) | 机翻 |
| 第一部分:数据系统基础 ——概览 | [part-i.md](part-i.md) | 初翻 |
| 第一章:可靠性、可扩展性、可维护性 | [ch1.md](ch1.md) | **精翻** |
| 第二章:数据模型与查询语言 | [ch2.md](ch2.md) | 初翻 |
| 第三章:存储与检索 | [ch3.md](ch3.md) | 初翻 |
| 第四章:编码与演化 | [ch4.md](ch4.md) | 初翻 |
| 第二部分:分布式数据——概览 | [part-ii.md](part-ii.md) | 初翻 |
| 第五章:复制 | [ch5.md](ch5.md) | 初翻 |
| 第六章:分片 | [ch6.md](ch6.md) | 初翻 |
| 第七章:事务 | [ch7.md](ch7.md) | **精翻 50%** |
| 第八章:分布式系统的麻烦 | [ch8.md](ch8.md) | 机翻 |
| 第九章:一致性与共识 | [ch9.md](ch9.md) | 机翻 |
| 第三部分:前言 | [part-iii.md](part-iii.md) | 机翻 |
| 第十章:批处理 | [ch10.md](ch10.md) | 机翻 |
| 第十一章:流处理 | [ch11.md](ch11.md) | 机翻 |
| 第十二章:数据系统的未来 | [ch12.md](ch12.md) | 机翻 |
| 术语表 | [glossary.md](glossary.md) | - |
| 后记 | [colophon.md](colophon.md) | 机翻 |

View File

@ -219,11 +219,13 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
如果两个事务不触及相同的数据,它们可以安全地**并行parallel**运行,因为两者都不依赖于另一个。当一个事务读取由另一个事务同时修改的数据时,或者当两个事务试图同时修改相同的数据时,并发问题(竞争条件)才会出现。
并发BUG很难通过测试找到因为这样的错误只有在运行不正常时才会触发。这样的时机可能很少通常很难重现。并发性也很难推理特别是在大型应用程序中您不一定知道哪些其他代码正在访问数据库。应用程序开发在一次只有一个用户时就很麻烦了有许多并发用户使得它更加困难因为任何一个数据都可能随时改变。
并发BUG很难通过测试找到因为这样的错误只有在特殊时机下才会触发。这样的时机可能很少通常很难重现[^译注i]。并发性也很难推理,特别是在大型应用中,你不一定知道哪些其他代码正在访问数据库。在一次只有一个用户时,应用开发已经很麻烦了,有许多并发用户使得它更加困难,因为任何一个数据都可能随时改变。
[^译注i]: 轶事:偶然出现的瞬时错误有时称为***Heisenbug***,而确定性的问题对应地称为***Bohrbugs***
出于这个原因,数据库一直试图通过提供**事务隔离transaction isolation**来隐藏应用程序开发者的并发问题。从理论上讲,隔离可以通过假装没有并发发生,让你的生活更加轻松:**可序列化serializable**的隔离等级意味着数据库保证事务的效果与连续运行(即一次一个,没有任何并发)是一样的。
实际上不幸的是:隔离并没有那么简单。**可序列化**会有性能损失许多数据库不愿意支付这个代价【8】。因此系统通常使用较弱的隔离级别来防止一些并发问题,但不是全部。这些隔离级别难以理解并且会导致微妙的错误但是它们仍然在实践中被使用【23】。
实际上不幸的是:隔离并没有那么简单。**可序列化**会有性能损失许多数据库不愿意支付这个代价【8】。因此系统通常使用较弱的隔离级别来防止一部分,而不是全部的并发问题。这些隔离级别难以理解并且会导致微妙的错误但是它们仍然在实践中被使用【23】。
并发性错误导致的并发性错误不仅仅是一个理论问题。他们造成了很多的资金损失【24,25】耗费了财务审计人员的调查【26】并导致客户数据被破坏【27】。关于这类问题的一个流行的评论是“如果你正在处理财务数据请使用ACID数据库” ——但是这一点没有提到。即使是很多流行的关系型数据库系统通常被认为是“ACID”也使用弱隔离级别所以它们也不一定能防止这些错误的发生。
@ -300,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的情况这不是一个长期持续的问题。因为如果她几秒钟后刷新银行网站的页面她很可能会看到一致的帐户余额。但是有些情况下不能容忍这种暂时的不一致
@ -334,7 +336,9 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
**图7-7 使用多版本对象实现快照隔离**
表中的每一行都有一个`created_by`字段其中包含将该行插入到表中的的事务ID。此外每行都有一个`deleted_by`字段,最初是空的。如果某个事务删除了一行,那么该行实际上并未从数据库中删除,而是通过将`deleted_by`字段设置为请求删除的事务的ID来标记为删除。在稍后的时间当确定没有事务可以再访问已删除的数据时数据库中的垃圾收集过程会将所有带有删除标记的行移除并释放其空间。
表中的每一行都有一个`created_by`字段其中包含将该行插入到表中的的事务ID。此外每行都有一个`deleted_by`字段,最初是空的。如果某个事务删除了一行,那么该行实际上并未从数据库中删除,而是通过将`deleted_by`字段设置为请求删除的事务的ID来标记为删除。在稍后的时间当确定没有事务可以再访问已删除的数据时数据库中的垃圾收集过程会将所有带有删除标记的行移除并释放其空间。[^译注ii]
[^译注ii]: 在PostgreSQL中`created_by`实际名称为`xmin``deleted_by`实际名称为`xmax`
`UPDATE`操作在内部翻译为`DELETE`和`INSERT`。例如,在[图7-7]()中事务13从账户2中扣除100美元将余额从500美元改为400美元。实际上包含两条账户2的记录余额为\$500的行被标记为**被事务13删除**,余额为\$400的行**由事务13创建**。
@ -342,8 +346,8 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
当一个事务从数据库中读取时事务ID用于决定它可以看见哪些对象看不见哪些对象。通过仔细定义可见性规则数据库可以向应用程序呈现一致的数据库快照。工作如下
1. 在每次事务开始时,数据库列出当时所有其他(尚未提交或中止)的事务清单,即使之后提交了,这些事务的写入也都会被忽略。
2. 被中止事务执行的任何写入都将被忽略。
1. 在每次事务开始时,数据库列出当时所有其他(尚未提交或中止)的事务清单,即使之后提交了,这些事务的写入也都会被忽略。
2. 被中止事务执行的任何写入都将被忽略。
3. 由具有较晚事务ID在当前事务开始之后开始的的事务所做的任何写入都被忽略而不管这些事务是否已经提交。
4. 所有其他写入,对应用都是可见的。
@ -360,9 +364,9 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
索引如何在多版本数据库中工作?一种选择是使索引简单地指向对象的所有版本,并且需要索引查询来过滤掉当前事务不可见的任何对象版本。当垃圾收集删除任何事务不再可见的旧对象版本时,相应的索引条目也可以被删除。
在实践中许多实现细节决定了多版本并发控制的性能。例如如果同一对象的不同版本可以放入同一个页面中PostgreSQL的优化可以避免更新索引[31]
在实践中许多实现细节决定了多版本并发控制的性能。例如如果同一对象的不同版本可以放入同一个页面中PostgreSQL的优化可以避免更新索引【31】
在CouchDBDatomic和LMDB中使用另一种方法。虽然它们也使用B树请参阅第79页上的“B树”,但它们使用的是一种**仅追加/写时拷贝append-only/copy-on-write**的变体,它们在更新时不覆盖树的页面,而为每个修改页面创建一份副本。从父页面直到树根都会级联更新,以指向它们子页面的新版本。任何不受写入影响的页面都不需要被复制,并且保持不变[33,34,35]
在CouchDBDatomic和LMDB中使用另一种方法。虽然它们也使用[B树](ch2.md#B树),但它们使用的是一种**仅追加/写时拷贝append-only/copy-on-write**的变体,它们在更新时不覆盖树的页面,而为每个修改页面创建一份副本。从父页面直到树根都会级联更新,以指向它们子页面的新版本。任何不受写入影响的页面都不需要被复制,并且保持不变【33,34,35】
使用仅追加的B树每个写入事务或一批事务都会创建一颗新的B树当创建时从该特定树根生长的树就是数据库的一个一致性快照。没必要根据事务ID过滤掉对象因为后续写入不能修改现有的B树它们只能创建新的树根。但这种方法也需要一个负责压缩和垃圾收集的后台进程。
@ -370,15 +374,15 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
快照隔离是一个有用的隔离级别特别对于只读事务而言。但是许多数据库实现了它却用不同的名字来称呼。在Oracle中称为**可序列化Serializable**的在PostgreSQL和MySQL中称为**可重复读repeatable read**【23】。
这种命名混淆的原因是SQL标准没有快照隔离的概念因为标准是基于System R 1975年定义的隔离级别【2】那时候**快照隔离**尚未发明。相反,它定义了**可重复读**,表面上看起来与快照隔离很相似。 PostgreSQL和MySQL称其**快照隔离**级别为**可重复读repeatable read**,因为它符合标准要求,所以它们可以称自己“兼容标准”。
这种命名混淆的原因是SQL标准没有**快照隔离**的概念因为标准是基于System R 1975年定义的隔离级别【2】那时候**快照隔离**尚未发明。相反,它定义了**可重复读**,表面上看起来与快照隔离很相似。 PostgreSQL和MySQL称其**快照隔离**级别为**可重复读repeatable read**,因为这样符合标准要求,所以它们可以声称自己“标准兼容”。
不幸的是SQL标准对隔离级别的定义是有缺陷的——模糊不精确并不像标准应该的那样独立于实现【28】。有几个数据库实现了可重复读但它们实际提供的保证存在很大的差异尽管表面上是标准化的【23】。在研究文献【29,30】中已经有了可重复读的正式定义,但大多数的实现并不能满足这个正式定义。最后IBM DB2使用“可重复读”来引用可串行化【8】。
不幸的是SQL标准对隔离级别的定义是有缺陷的——模糊不精确并不像标准应有的样子独立于实现【28】。有几个数据库实现了可重复读但它们实际提供的保证存在很大的差异尽管表面上是标准化的【23】。在研究文献【29,30】中已经有了可重复读的正式定义但大多数的实现并不能满足这个正式定义。最后IBM DB2使用“可重复读”来引用可串行化【8】。
结果,没有人真正知道**可重复读**的意思。
### 防止丢失更新
到目前为止,我们讨论的读已提交和快照隔离级别主要是:**保证了只读事务在并发写入时可以看到什么**。却忽略了两个事务并发写入的问题——我们只讨论了[脏写](#脏写),一种特定类型的写-写冲突是可能出现的。
到目前为止已经讨论的**读已提交**和**快照隔离**级别,主要保证了**只读事务在并发写入时**可以看到什么。却忽略了两个事务并发写入的问题——我们只讨论了[脏写](#脏写),一种特定类型的写-写冲突是可能出现的。
并发的写入事务之间还有其他几种有趣的冲突。其中最着名的是**丢失更新lost update**问题,如[图7-1]()所示,以两个并发计数器增量为例。
@ -388,7 +392,7 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
- 在复杂值中进行本地修改例如将元素添加到JSON文档中的一个列表需要解析文档进行更改并写回修改的文档
- 两个用户同时编辑wiki页面每个用户通过将整个页面内容发送到服务器来保存其更改覆写数据库中当前的任何内容。
由于这是一个普遍的问题,所以已经开发了各种解决方案。
这是一个普遍的问题,所以已经开发了各种解决方案。
#### 原子写
@ -457,7 +461,7 @@ UPDATE wiki_pages SET content = '新内容'
锁和CAS操作假定有一个最新的数据副本。但是多主或无主复制的数据库通常允许多个写入并发执行并异步复制到副本上因此无法保证有一份数据的最新副本。所以基于锁或CAS操作的技术不适用于这种情况。 (我们将在“[线性化](ch9.md#线性化)”中更详细地讨论这个问题。)
相反,如“[ch5.md#检测并发写入](#检测并发写入)”一节所述,这种复制数据库中的一种常见方法是允许并发写入创建多个冲突版本的值(也称为兄弟),并使用应用代码或特殊数据结构在事实发生之后解决和合并这些版本。
相反,如“[检测并发写入](ch5.md#检测并发写入)”一节所述,这种复制数据库中的一种常见方法是允许并发写入创建多个冲突版本的值(也称为兄弟),并使用应用代码或特殊数据结构在事实发生之后解决和合并这些版本。
原子操作可以在复制的上下文中很好地工作尤其当它们具有可交换性时可以在不同的副本上以不同的顺序应用它们且仍然可以得到相同的结果。例如递增计数器或向集合添加元素是可交换的操作。这是Riak 2.0数据类型背后的思想它可以防止复制副本丢失更新。当不同的客户端同时更新一个值时Riak自动将更新合并在一起以免丢失更新【39】。
@ -471,7 +475,7 @@ UPDATE wiki_pages SET content = '新内容'
首先想象一下这个例子你正在为医院写一个医生轮班管理程序。医院通常会同时要求几位医生待命但底线是至少有一位医生在待命。医生可以放弃他们的班次例如如果他们自己生病了只要至少有一个同事在这一班中继续工作【40,41】。
现在想象一下Alice和Bob是两位值班医生。两人都感到不适所以他们都决定请假。不幸的是他们恰好在同一时间点击按钮关闭电话。图7-8说明了接下来的事情。
现在想象一下Alice和Bob是两位值班医生。两人都感到不适所以他们都决定请假。不幸的是他们恰好在同一时间点击按钮关闭电话。[图7-8](img/fig7-8.png)说明了接下来的事情。
![](img/fig7-8.png)
@ -479,9 +483,9 @@ UPDATE wiki_pages SET content = '新内容'
在两个事务中应用首先检查是否有两个或以上的医生正在值班如果是的话它就假定一名医生可以安全地翘班。由于数据库使用快照隔离两次检查都返回2所以两个事务都进入下一个阶段。爱丽丝更新自己的记录翘班了而鲍勃也干了一样的事情。两个事务都成功提交了现在没有医生值班了。违反了至少有一名医生在值班的要求。
#### 写偏差的特征
#### 写偏差的特征
这种异常称为**写偏差**【28】。它既不是**脏写**,也不是**丢失更新**因为这两个事务正在更新两个不同的对象Alice和Bob各自的待命记录。在这里发生的冲突并不是那么明显但是这显然是一个竞争条件如果两个事务一个接一个地运行那么第二个医生就不能翘班了。异常行为只有在事务并发进行时才有可能。
这种异常称为**写偏差**【28】。它既不是**脏写**,也不是**丢失更新**因为这两个事务正在更新两个不同的对象Alice和Bob各自的待命记录。在这里发生的冲突并不是那么明显但是这显然是一个竞争条件如果两个事务一个接一个地运行那么第二个医生就不能翘班了。异常行为只有在事务并发进行时才有可能。
可以将写入偏差视为丢失更新问题的一般化。如果两个事务读取相同的对象,然后更新其中一些对象(不同的事务可能更新不同的对象),则可能发生写入偏差。在多个事务更新同一个对象的特殊情况下,就会发生脏写或丢失更新(取决于时机)。
@ -508,9 +512,9 @@ COMMIT;
* 和以前一样,`FOR UPDATE`告诉数据库锁定返回的所有行用于更新。
#### 写偏差的更多例子
#### 写偏差的更多例子
写偏差乍看像是一个深奥的问题,但一旦你意识到这一点,就可能会注意到更多可能发生的情况。以下是一些例子:
写偏差乍看像是一个深奥的问题,但一旦意识到这一点,很容易会注意到更多可能的情况。以下是一些例子:
***会议室预订系统***
@ -605,12 +609,12 @@ COMMIT;
尽管这似乎是一个明显的主意但数据库设计人员只是在2007年左右才决定单线程循环执行事务是可行的【45】。如果多线程并发在过去的30年中被认为是获得良好性能的关键所在那么究竟是什么改变致使单线程执行变为可能呢
两个进展引了这个反思:
两个进展引了这个反思:
- RAM足够便宜了许多场景现在都可以将完整的活跃数据集保存在内存中。参阅“[在内存中存储一切](ch3.md#在内存中存储一切)”)。当事务需要访问的所有数据都在内存中时,事务处理的执行速度要比等待数据从磁盘加载时快得多。
- 数据库设计人员意识到OLTP事务通常很短而且只进行少量的读写操作参阅“[事务处理或分析?](ch3.md#事务处理还是分析?)”)。相比之下,长时间运行的分析查询通常是只读的,因此它们可以在串行执行循环之外的一致快照(使用快照隔离)上运行。
串行执行事务的方法在VoltDB / H-StoreRedis和Datomic中实现【46,47,48】。设计用于单线程执行的系统有时可以比支持并发的系统更好因为它可以避免锁的协调开销。但是其吞吐量仅限于单个CPU核的吞吐量。为了充分利用单一线程需要与传统形式不同的结构的事务。
串行执行事务的方法在VoltDB/H-StoreRedis和Datomic中实现【46,47,48】。设计用于单线程执行的系统有时可以比支持并发的系统更好因为它可以避免锁的协调开销。但是其吞吐量仅限于单个CPU核的吞吐量。为了充分利用单一线程需要与传统形式不同的结构的事务。
#### 在存储过程中封装事务
@ -638,27 +642,27 @@ COMMIT;
但是这些问题都是可以克服的。现代的存储过程实现放弃了PL/SQL而是使用现有的通用编程语言VoltDB使用Java或GroovyDatomic使用Java或Clojure而Redis使用Lua。
存储过程与内存数据存储,使得在单个线程上执行所有事务变得可行。由于它们不需要等待I/O且避免了其他并发控制机制的开销,它们可以在单个线程上实现相当好的吞吐量。
**存储过程与内存存储**使得在单个线程上执行所有事务变得可行。由于不需要等待I/O且避免了并发控制机制的开销它们可以在单个线程上实现相当好的吞吐量。
VoltDB还使用存储过程进行复制但不是将事务的写入结果从一个节点复制到另一个节点而是在每个节点上执行相同的存储过程。因此VoltDB要求存储过程是**确定性的**在不同的节点上运行时它们必须产生相同的结果。举个例子如果事务需要使用当前的日期和时间则必须通过特殊的确定性API来实现。
VoltDB还使用存储过程进行复制但不是将事务的写入结果从一个节点复制到另一个节点而是在每个节点上执行相同的存储过程。因此VoltDB要求存储过程是**确定性的**在不同的节点上运行时它们必须产生相同的结果。举个例子如果事务需要使用当前的日期和时间则必须通过特殊的确定性API来实现。
#### 分区
顺序执行所有事务使并发控制简单多了,但是数据库的事务吞吐量被限制在了单个机器上单个CPU核的速度。只读事务可以使用快照隔离在其他地方执行,但对于写入吞吐量较高的应用程序,单线程事务处理器可能成为一个严重的瓶颈。
顺序执行所有事务使并发控制简单多了,但数据库的事务吞吐量被限制为单机单核的速度。只读事务可以使用快照隔离在其它地方执行,但对于写入吞吐量较高的应用,单线程事务处理器可能成为一个严重的瓶颈。
为了扩展到多个CPU核心和多个节点可以对您的数据进行分区(参见[第6章](ch6.md)在VoltDB中这是支持的。如果您可以找到一种对数据集进行分区的方法以便每个事务只需要在单个分区中读写数据那么每个分区就可以拥有自己独立运行的事务处理线程。在这种情况下可以为每个分区指派一个独立的CPU核事务吞吐量就可以与CPU核数保持线性扩展【47】。
为了扩展到多个CPU核心和多个节点可以对数据进行分区参见[第6章](ch6.md)在VoltDB中支持这样做。如果你可以找到一种对数据集进行分区的方法以便每个事务只需要在单个分区中读写数据那么每个分区就可以拥有自己独立运行的事务处理线程。在这种情况下可以为每个分区指派一个独立的CPU核事务吞吐量就可以与CPU核数保持线性扩展【47】。
但是,对于需要访问多个分区的任何事务,数据库必须在触及的所有分区之间协调事务。存储过程需要跨越所有分区锁定执行,以确保整个系统的可串行性。
由于跨分区事务具有额外的协调开销,所以它们比单分区事务慢得多。 VoltDB报告的吞吐量大约是每秒1000个跨分区写入比单分区吞吐量低几个数量级并且不能通过增加更多的机器来增加【49】。
事务是否可以是划分至单个分区很大程度上取决于应用数据的结构。简单的键值数据通常可以非常容易地进行分区,但是有多个二级索引的数据可能需要大量的跨分区协调(参阅“[分片与次级索引](ch6.md#分片与次级索引)”)。
事务是否可以是划分至单个分区很大程度上取决于应用数据的结构。简单的键值数据通常可以非常容易地进行分区,但是有多个二级索引的数据可能需要大量的跨分区协调(参阅“[分片与次级索引](ch6.md#分片与次级索引)”)。
#### 串行执行小结
在特定约束条件下,串行执行事务已经成为一种实现可序列化隔离等级的可行办法。
在特定约束条件下,真的串行执行事务已经成为一种实现可序列化隔离等级的可行办法。
- 每个事务都必须小而快,因为只要一个缓慢的事务即可拖慢所有事务处理。
- 每个事务都必须小而快,只要有一个缓慢的事务,就会拖慢所有事务处理。
- 仅限于活跃数据集可以放入内存的情况。很少访问的数据可能会被移动到磁盘,但如果需要在单线程执行的事务中访问,系统就会变得非常慢[^x]。
- 写入吞吐量必须低到能在单个CPU核上处理如若不然事务需要能划分至单个分区且不需要跨分区协调。
- 跨分区事务是可能的,但是它们的使用程度有很大的限制。
@ -675,7 +679,7 @@ VoltDB还使用存储过程进行复制但不是将事务的写入结果从
>
> 请注意虽然两阶段锁定2PL听起来非常类似于两阶段提交2PC但它们是完全不同的东西。我们将在第9章讨论2PC。
之前我们看到锁通常用于防止脏写(参阅“[无脏写]()”一节):如果两个事务同时尝试写入同一个对象,则锁可确保第二个写入必须等到第一个写入完成事务(中止或提交),然后才能继续。
之前我们看到锁通常用于防止脏写(参阅“[没有脏写](没有脏写)”一节):如果两个事务同时尝试写入同一个对象,则锁可确保第二个写入必须等到第一个写入完成事务(中止或提交),然后才能继续。
两阶段锁定定类似,但使锁的要求更强。只要没有写入,就允许多个事务同时读取同一个对象。但对象只要有写入(修改或删除),就需要**独占访问exclusive access**权限:
@ -758,7 +762,7 @@ WHERE room_id = 123 AND
也许不是:一个称为**可序列化快照隔离SSI, serializable snapshot isolation**的算法是非常有前途的。它提供了完整的可序列化隔离级别,但与快照隔离相比只有只有很小的性能损失。 SSI是相当新的它在2008年首次被描述【40】并且是Michael Cahill的博士论文【51】的主题。
今天SSI既用于单节点数据库9.1版以来PostgreSQL中的可序列化隔离级别和分布式数据库FoundationDB使用类似的算法。由于SSI与其他并发控制机制相比还很年轻还处于在实践中证明自己表现的阶段。但它有可能因为足够快而在未来成为新的默认选项。
今天SSI既用于单节点数据库PostgreSQL9.1以后的可序列化隔离级别和分布式数据库FoundationDB使用类似的算法。由于SSI与其他并发控制机制相比还很年轻还处于在实践中证明自己表现的阶段。但它有可能因为足够快而在未来成为新的默认选项。
#### 悲观与乐观的并发控制

View File

@ -16,7 +16,7 @@
[TOC]
最近几章中反复出现的主题是系统如何处理错误的事情。例如,我们讨论了**副本故障转移**第156页的“[处理节点中断]()”),**复制延迟**第161页的“[复制延迟问题](ch6.md#复制延迟问题)”)和事务控制(第233页的“弱隔离级别”)。当我们了解可能在实际系统中出现的各种边缘情况时,我们会更好地处理它们。
最近几章中反复出现的主题是系统如何处理错误的事情。例如,我们讨论了**副本故障转移**(“[处理节点中断](#ch5.md#处理节点宕机)”),**复制延迟**第161页的“[复制延迟问题](ch6.md#复制延迟问题)”)和事务控制(“[弱隔离级别](ch7.md#弱隔离级别)”)。当我们了解可能在实际系统中出现的各种边缘情况时,我们会更好地处理它们。
但是,尽管我们已经谈了很多错误,但最后几章仍然过于乐观。现实更加黑暗。我们现在将我们的悲观主义转向最大化,并假设任何可能出错的东西**都会**出错[^i]。(经验丰富的系统运维会告诉你这是一个合理的假设,如果你问得好,他们可能会一边治疗心理创伤一边告诉你一些可怕的故事)
@ -36,15 +36,16 @@
单个计算机上的软件没有根本的原因:当硬件正常工作时,相同的操作总是产生相同的结果(这是确定性的)。如果存在硬件问题(例如,内存损坏或连接器松动),其后果通常是整个系统故障(例如,内核恐慌,“蓝屏死机”,启动失败)。具有良好软件的个人计算机通常功能完全或完全破坏,但不是介于两者之间。
、这是计算机设计中的一个慎重的选择:如果发生内部错误,我们宁愿电脑完全崩溃,而不是返回错误的结果,因为错误的结果很难处理。因此,计算机隐藏了它们所实现的模糊的物理现实,并呈现出一个理想化的系统模型,并以数学完美的方式运作。 CPU指令总是做同样的事情;如果您将一些数据写入内存或磁盘,那么这些数据将保持不变,并且不会被随机破坏。总是正确的计算这个设计目标一直回到第一台数字计算机[3]
、这是计算机设计中的一个慎重的选择:如果发生内部错误,我们宁愿电脑完全崩溃,而不是返回错误的结果,因为错误的结果很难处理。因此,计算机隐藏了它们所实现的模糊的物理现实,并呈现出一个理想化的系统模型,并以数学完美的方式运作。 CPU指令总是做同样的事情;如果您将一些数据写入内存或磁盘,那么这些数据将保持不变,并且不会被随机破坏。总是正确的计算这个设计目标一直回到第一台数字计算机【3】
当你编写运行在多台计算机上的软件时,情况根本不同。在分布式系统中,我们不再处于理想化的系统模型中,我们别无选择,只能面对现实世界的混乱现实。而在现实世界中,如此轶事所示,各种各样的事情可能会出现问题[4]
当你编写运行在多台计算机上的软件时,情况根本不同。在分布式系统中,我们不再处于理想化的系统模型中,我们别无选择,只能面对现实世界的混乱现实。而在现实世界中,如此轶事所示,各种各样的事情可能会出现问题【4】
> 在我有限的经验中我已经和很多东西打过交道单个数据中心DC中长期存在的网络分区配电单元PDU故障开关故障整个机架意外的电源短路全直流主干故障全直流电源故障以及一个低血糖的司机把他的福特皮卡撞碎在数据中心的HVAC加热通风和空气系统上。而且我甚至不是一个运维。
>
> ——柯达黑尔
在分布式系统中,尽管系统的其他部分工作正常,但系统的某些部分可能会以某种不可预知的方式被破坏。这被称为部分失败。难点在于部分失败是不确定的:如果你试图做任何涉及多个节点和网络的事情,它有时可能会工作,有时会出现不可预知的失败。正如我们将要看到的,你甚至不知道是否成功了,因为消息通过网络传播的时间也是不确定的!
这种不确定性和部分失效的可能性,使得分布式系统难以工作[5]。
### 云计算与超级计算机
@ -55,7 +56,7 @@
* 另一个极端是云计算,这种云计算的定义不是很好[6]但通常与多租户数据中心连接IP网络的商品计算机通常是以太网弹性/按需资源分配以及计量计费。
* 传统企业数据中心位于这两个极端之间。
用这些哲学来处理错误的方法非常不同。在超级计算机中,作业通常会检查计算的状态,以便持久存储。如果一个节点出现故障,通常的解决方案是简单地停止整个集群的工作负载。故障节点修复后,计算从上一个检查点重新开始[7,8]。因此,超级计算机更像是一个单节点计算机而不是分布式系统:它通过让它升级成完全失败来处理部分失败 - 如果系统的任何部分发生故障,只是让所有的事情都崩溃(就像单台机器上的内核恐慌)。
用这些哲学来处理错误的方法非常不同。在超级计算机中,作业通常会检查计算的状态,以便持久存储。如果一个节点出现故障,通常的解决方案是简单地停止整个集群的工作负载。故障节点修复后,计算从上一个检查点重新开始【7,8】。因此,超级计算机更像是一个单节点计算机而不是分布式系统:它通过让它升级成完全失败来处理部分失败 - 如果系统的任何部分发生故障,只是让所有的事情都崩溃(就像单台机器上的内核恐慌)。
在本书中,我们将重点放在实现互联网服务的系统上,这些系统通常与超级计算机看起来有很大不同
@ -84,7 +85,7 @@
> * 纠错码允许数字数据在通信信道上准确传输,偶尔会出现一些错误,例如由于无线网络上的无线电干扰[12]。
> * IPInternet协议不可靠可能丢弃延迟复制或重排数据包。 TCP传输控制协议在IP之上提供了更可靠的传输层它确保丢失的数据包被重新传输消除重复并且数据包被重新组装成它们被发送的顺序。
>
> 虽然这个系统可以比它的底层部分更可靠,但它的可靠性总是有限的。例如,纠错码可以处理少量的单比特错误,但是如果你的信号被干扰所淹没,那么通过你的通信信道可以得到多少数据是有根本的限制的[13]。 TCP可以隐藏数据包的丢失重复和重新排序但是它不能神奇地消除网络中的延迟。
> 虽然这个系统可以比它的底层部分更可靠,但它的可靠性总是有限的。例如,纠错码可以处理少量的单比特错误,但是如果你的信号被干扰所淹没,那么通过你的通信信道可以得到多少数据是有根本的限制的【13】。 TCP可以隐藏数据包的丢失重复和重新排序但是它不能神奇地消除网络中的延迟。
>
> 虽然更可靠的高级系统并不完美但它仍然有用因为它处理了一些棘手的低级错误所以其余的错误通常更容易推理和处理。我们将在第519页的“端到端的论点”中进一步探讨这个问题。
@ -98,11 +99,11 @@
互联网和数据中心通常是以太网中的大多数内部网络都是异步分组网络。在这种网络中一个节点可以向另一个节点发送一个消息一个数据包但是网络不能保证它什么时候到达或者是否到达。如果您发送请求并期待响应则很多事情可能会出错其中一些如图8-1所示
1. 您的请求可能已经丢失(可能有人拔掉了网线)。
2. 您的请求可能正在队列中等待,稍后将交付(也许网络或收件人超载)。
3. 远程节点可能失败(可能是崩溃或关机)。
1. 请求可能已经丢失(可能有人拔掉了网线)。
2. 请求可能正在队,稍后将交付(也许网络或收件人超载)。
3. 远程节点可能已经失效(可能是崩溃或关机)。
4. 远程节点可能暂时停止了响应(可能会遇到长时间的垃圾回收暂停;请参阅第295页上的“暂停进程”但稍后会再次响应。
5. 远程节点可能已经处理了的请求,但是网络上的响应已经丢失(可能是网络交换机配置错误)。
5. 远程节点可能已经处理了的请求,但是网络上的响应已经丢失(可能是网络交换机配置错误)。
6. 远程节点可能已经处理了您的请求,但是响应已经被延迟并且稍后将被传递(可能是网络或者您自己的机器过载)。
![](img/fig8-1.png)
@ -609,7 +610,7 @@ Web应用程序确实需要预期受终端用户控制的客户端如Web浏
安全性通常被非正式地定义为没有什么不好的事情发生,而活着就像最终发生的事情一样。但是,最好不要过多地阅读那些非正式的定义,因为好与坏的含义是主观的。安全性和活性的实际定义是精确的和数学的[90]
* 如果安全属性被违反,我们可以指向一个特定的时间点(例如,如果违反了唯一性属性,我们可以确定重复的防护令牌返回的特定操作) 。违反安全财产后,违规行为不能撤销 - 损害已经完成
* 如果安全属性被违反,我们可以指向一个特定的时间点(例如,如果违反了唯一性属性,我们可以确定重复的防护令牌返回的特定操作) 。违反安全属性后,违规行为不能撤销——损失已经发生
* 活性属性反过来:在某个时间点(例如,一个节点可能发送了一个请求,但还没有收到响应),它可能不成立,但总是希望在未来(即通过接受答复)。
区分安全性和活性属性的一个优点是可以帮助我们处理困难的系统模型。对于分布式算法,在系统模型的所有可能情况下,要求安全属性始终保持是常见的[88]。也就是说,即使所有节点崩溃,或者整个网络出现故障,算法仍然必须确保它不会返回错误的结果(即保证安全性得到满足)。
@ -624,7 +625,7 @@ Web应用程序确实需要预期受终端用户控制的客户端如Web浏
法定人数算法请参见第179页的“读写法定人数”依赖节点来记住它声称存储的数据。如果一个节点可能患有健忘症忘记了以前存储的数据这会打破法定条件从而破坏算法的正确性。也许需要一个新的系统模型在这个模型中我们假设稳定的存储大多存在崩溃但有时可能会丢失。但是那个模型就变得更难以推理了。
算法的理论描述可以简单宣称一些事在假设上是不会发生的——在非拜占庭式系统中。但实际上我们还是需要对可能发生和不可能发生的故障做出假设,真实世界的实现,仍然会包括处理“假设上不可能”情况的代码,即使代码可能就是`printf("你逊爆了")`和`exit(666)`,实际上也就是留给运维来擦屁股。(这可以说是计算机科学和软件工程间的一个差异)。
算法的理论描述可以简单宣称一些事在假设上是不会发生的——在非拜占庭式系统中。但实际上我们还是需要对可能发生和不可能发生的故障做出假设,真实世界的实现,仍然会包括处理“假设上不可能”情况的代码,即使代码可能就是`printf("you sucks")`和`exit(666)`,实际上也就是留给运维来擦屁股。(这可以说是计算机科学和软件工程间的一个差异)。
这并不是说理论上抽象的系统模型是毫无价值的,恰恰相反。它们对于将实际系统的复杂性降低到一个我们可以推理的可处理的错误是非常有帮助的,以便我们能够理解这个问题,并试图系统地解决这个问题。我们可以证明算法是正确的,通过显示它们的属性总是保持在某个系统模型中
@ -642,7 +643,7 @@ Web应用程序确实需要预期受终端用户控制的客户端如Web浏
这种部分失效可能发生的事实是分布式系统的决定性特征。每当软件试图做任何涉及其他节点的事情时,就有可能偶尔会失败,或者随机变慢,或者根本没有响应(最终超时)。在分布式系统中,我们试图将部分故障的容忍度建立到软件中,这样整个系统即使在某些组成部分被破坏的情况下也可以继续运行。
为了容忍错误,第一步是检测它们,但即使这样也很难。大多数系统没有检测节点是否发生故障的准确机制,所以大多数分布式算法依靠超时来确定远程节点是否仍然可用。但是,超时无法区分网络和节点故障,并且可变的网络延迟有时会导致节点被错误地怀疑发生故障。此外,有时一个节点可能处于降级状态:例如,由于驱动程序错误[94]千兆网络接口可能突然下降到1 Kb / s的吞吐量。这样一个“跛行”而不是死的节点可能比一个干净的故障节点更难处理。
为了容忍错误,第一步是检测它们,但即使这样也很难。大多数系统没有检测节点是否发生故障的准确机制,所以大多数分布式算法依靠超时来确定远程节点是否仍然可用。但是,超时无法区分网络和节点故障,并且可变的网络延迟有时会导致节点被错误地怀疑发生故障。此外,有时一个节点可能处于降级状态:例如,由于驱动程序错误[94]千兆网络接口可能突然下降到1 Kb/s的吞吐量。这样一个“跛行”而不是死的节点可能比一个干净的故障节点更难处理。
一旦检测到故障,使系统容忍它是不容易的:没有全局变量,没有共享内存,没有共同的知识或机器之间的任何其他种类的共享状态。节点甚至不知道现在几点了,更不用说更深刻的了。信息从一个节点流向另一个节点的唯一方法是通过不可靠的网络发送信息。重大决策不能由一个节点安全地完成,因此我们需要从其他节点获得帮助的协议,并争取达到法定人数。