ddia ch7精翻50%

This commit is contained in:
Vonng 2018-02-25 20:28:27 +08:00
parent 3b66a98ee5
commit 28e8b3262f
4 changed files with 229 additions and 149 deletions

59
OTHER.md Normal file
View File

@ -0,0 +1,59 @@
# PG
> 不懂数据库的全栈工程师不是好架构师
>
> —— Vonng
## [PostgreSQL](pg/)
PostgreSQL是世界上最先进的开源关系型数据库
### SQL
- [PostgreSQL中的锁](note/pg-sql-lock.md)
### GIS
- [PostGIS入门](gis/intro.md) 【EMPTY】
- [PostGIS教程-翻译计划](gis/README.md)
### Adminstration
- [Installation](pg/pg-admin-install.md)
- [Setup](pg/pg-admin-setup.md)
- [Configuration](pg/pg-admin-config.md)
- [Authentication](pg/pg-admin-auth.md)
- [PostgreSQL 监控](pg/pg-admin-monitor.md)
- [PostgreSQL备份与恢复](pg/pg-admin-backup.md)
- [PostgreSQL WAL与检查点](pg/pg-admin-wal.md)
- [PostgreSQL 高可用](pg/pg-admin-ha.md)
### Tunning
- [Memory Tunning](pg/pg-tune-memory.md)
- [Checkpoint Tuning Basic](pg/pg-tune-checkpoint)
- [Autovacuum Tuning Basics](pg/pg-tune-autovacuum.md)
### Extension
- [PostGIS Installation](pg/ext-postgis-install.md)
- [FileFDW Usage](pg/ext-file_fdw-intro.md)
- [RedisFDW Installation](pg/ext-redis_fdw-install.md)
- [MongoFDW Installation](pg/ext-mongo_fdw-install.md)
- [PgAdmin Installation](pg/ext-pgadmin-install.md)
### Reference
- [PostgreSQL 9.6 中文文档](http://www.postgres.cn/docs/9.6/)
- [PostgreSQL 10.1 官方文档](https://www.postgresql.org/docs/10/static/index.html)
- [PostGIS 2.4 官方文档](https://postgis.net/docs/manual-2.4/)
- [Introduction to PostGIS](http://workshops.boundlessgeo.com/postgis-intro/index.html)

115
README.md
View File

@ -6,9 +6,42 @@
## Data System
## [设计数据密集型应用 - 中文翻译](ddia/README.md)
数据系统"学习笔记"[DDIA](ddia/README.md)
- 作者: Martin Kleppmann
- 原书名称《Designing Data-Intensive Application》
- 译者:[冯若航]( http://vonng.com/about) fengruohang@outlook.com
- 建议本地使用[Typora](https://www.typora.io)以获取最佳阅读体验。
-------------
### 译序
现今尤其是互联网领域大多数应用都属于数据密集型应用。这本书从底层数据结构讲到顶层架构设计将系统设计中的精髓娓娓道来。对架构师、DBA、后端工程师尤其有帮助。
这是一本理论结合实践的书,书中很多问题,译者在实际场景中都曾遇到过,读来让人击节赞叹。如果能早点读到这本书,该少走多少弯路啊!
又是一本深入浅出的书,按照事物发展演化的历程来介绍,将复杂的概念讲述的浅显易懂,但又直击本质,不失深度。每章最后的参考引用质量非常好,是进一步深入学习各个主题的绝佳索引。
本书为系统的设计、实现、与评价提供了很好的概念框架。读完并理解本书内容后,读者可以轻松看破大多数的技术忽悠,与技术砖家撕起逼来虎虎生风。
这是2017年译者读过最好的一本技术类书籍这么好的书没有中文翻译实在是遗憾。某不才愿为先进技术文化的传播贡献一分力量。既可以深入学习有趣的技术主题又可以锻炼中英文语言文字功底何乐而不为呢
### 前言
> 在我们的社会中,技术是一种强大的力量。数据、软件、通信可以用于坏的方面:不公平的阶级固化,损害公民权利,保护既得利益集团。但也可以用于好的方面:让底层人民发出自己的声音,让每个人都拥有机会,避免灾难。本书献给所有将技术用于善途的人们。
> 计算是一种流行文化,流行文化鄙视历史。 流行文化关乎个体身份和参与感,与合作无关。它活在当下,也与过去和未来无关。 我认为大部分(为钱)写代码的人就是这样, 他们不知道他们的文化来自哪里。
>
> ——阿兰·凯接受Dobb博士的杂志采访时2012年
### 目录
#### [序言](ddia/preface.md)
#### [I. 数据系统基础](ddia/part-i.md)
@ -32,63 +65,41 @@
12. [数据系统的未来](ddia/ch12.md)
#### [术语表](ddia/glossary.md)
## Storage
* SSD Internal
#### [后记](ddia/colophon.md)
## [PostgreSQL](pg/)
### 翻译计划
PostgreSQL是世界上最先进的开源关系型数据库
机翻:只在乎结构:梳理文章结构、图片、引用、备注。
### SQL
初翻:保证自己经完全理解本章内容,人工修复显著的错误,重新组织语言。
* [PostgreSQL中的锁](note/pg-sql-lock.md)
精翻:确定术语的最终译法,修复格式瑕疵,着力信达雅。
### GIS
通常机翻一章1个小时左右初翻一章6小时精翻一章三到五天。
* [PostGIS入门](gis/intro.md) 【EMPTY】
* [PostGIS教程-翻译计划](gis/README.md)
### Adminstration
- [Installation](pg/pg-admin-install.md)
- [Setup](pg/pg-admin-setup.md)
- [Configuration](pg/pg-admin-config.md)
- [Authentication](pg/pg-admin-auth.md)
- [PostgreSQL 监控](pg/pg-admin-monitor.md)
- [PostgreSQL备份与恢复](pg/pg-admin-backup.md)
- [PostgreSQL WAL与检查点](pg/pg-admin-wal.md)
- [PostgreSQL 高可用](pg/pg-admin-ha.md)
### Tunning
* [Memory Tunning](pg/pg-tune-memory.md)
* [Checkpoint Tuning Basic](pg/pg-tune-checkpoint)
* [Autovacuum Tuning Basics](pg/pg-tune-autovacuum.md)
### Extension
* [PostGIS Installation](pg/ext-postgis-install.md)
* [FileFDW Usage](pg/ext-file_fdw-intro.md)
* [RedisFDW Installation](pg/ext-redis_fdw-install.md)
* [MongoFDW Installation](pg/ext-mongo_fdw-install.md)
* [PgAdmin Installation](pg/ext-pgadmin-install.md)
### Reference
* [PostgreSQL 9.6 中文文档](http://www.postgres.cn/docs/9.6/)
* [PostgreSQL 10.1 官方文档](https://www.postgresql.org/docs/10/static/index.html)
* [PostGIS 2.4 官方文档](https://postgis.net/docs/manual-2.4/)
* [Introduction to PostGIS](http://workshops.boundlessgeo.com/postgis-intro/index.html)
精翻可以看,初翻凑合看,机翻没法看。精翻太累了,看心情吧。
| 章节 | 进度 |
| ---------------------------------- | ------------ |
| 序言 | 机翻 |
| 第一部分:数据系统基础 ——概览 | 初翻 |
| 第一章:可靠性、可扩展性、可维护性 | **精翻** |
| 第二章:数据模型与查询语言 | 初翻 |
| 第三章:存储与检索 | 初翻 |
| 第四章:编码与演化 | 初翻 |
| 第二部分:分布式数据——概览 | 初翻 |
| 第五章:复制 | 初翻 |
| 第六章:分片 | 初翻 |
| 第七章:事务 | **精翻 50%** |
| 第八章:分布式系统的麻烦 | 机翻 |
| 第九章:一致性与共识 | 机翻 |
| 第三部分:前言 | 机翻 |
| 第十章:批处理 | 机翻 |
| 第十一章:流处理 | 机翻 |
| 第十二章:数据系统的未来 | 机翻 |
| 术语表 | - |
| 后记 | 机翻 |

View File

@ -8,6 +8,18 @@
-----------------
## 译序
现今尤其是互联网领域大多数应用都属于数据密集型应用。这本书从底层数据结构讲到顶层架构设计将系统设计中的精髓娓娓道来。对架构师、DBA、后端工程师尤其有帮助。
这是一本理论结合实践的书,书中很多问题,译者在实际场景中都曾遇到过,读来让人击节赞叹。如果能早点读到这本书,该少走多少弯路啊!
又是一本深入浅出的书,按照事物发展演化的历程来介绍,将复杂的概念讲述的浅显易懂,但又直击本质,不失深度。每章最后的参考引用质量非常好,是进一步深入学习各个主题的绝佳索引。
本书为系统的设计、实现、与评价提供了很好的概念框架。读完并理解本书内容后,读者可以轻松看破大多数的技术忽悠,与技术砖家撕起逼来虎虎生风。
这是2017年译者读过最好的一本技术类书籍这么好的书没有中文翻译实在是遗憾。某不才愿为先进技术文化的传播贡献一分力量。既可以深入学习有趣的技术主题又可以锻炼中英文语言文字功底何乐而不为呢
## 前言
@ -62,9 +74,9 @@
精翻:确定术语的最终译法,修复格式瑕疵,着力信达雅。
通常机翻一章1个小时左右初翻一章6小时精翻一章天。
通常机翻一章1个小时左右初翻一章6小时精翻一章三到五天。
精翻可以看,初翻凑合看,机翻没法看。估计春节后能初翻完一遍。精翻太累了,看心情吧。
精翻可以看,初翻凑合看,机翻没法看。精翻太累了,看心情吧。
| 章节 | 文件 | 计划 | 进度 |
| ------ | ------ | ---- | ---- |
@ -77,7 +89,7 @@
| 第二部分:分布式数据——概览 | [part-ii.md](part-ii.md) | | 初翻 |
| 第五章:复制 | [ch5.md](ch5.md) | | 初翻 |
| 第六章:分片 | [ch6.md](ch6.md) | | 初翻 |
| 第七章:事务 | [ch7.md](ch7.md) | | 初翻 |
| 第七章:事务 | [ch7.md](ch7.md) | | **精翻 50%** |
| 第八章:分布式系统的麻烦 | [ch8.md](ch8.md) | | 机翻 |
| 第九章:一致性与共识 | [ch9.md](ch9.md) | | 机翻 |
| 第三部分:前言 | [part-iii.md](part-iii.md) | | 机翻 |

View File

@ -61,95 +61,93 @@
相比之下ACID的原子性并**不**是关于**并发concurrent**的。它并不是在描述如果几个进程试图同时访问相同的数据会发生什么情况,这种情况包含在缩写***I***中,即[**隔离性Isolation**](#隔离性Isolation)
ACID的原子性描述了当客户想进行多次写入但在一些写处理完之后出现故障的情况。例如进程崩溃,网络连接中断,磁盘变满或者某种完整性约束被违反。如果这些写被分组到一个原子事务中,并且该事务由于错误而不能完成(提交),则该事务将被中止,并且数据库必须丢弃或撤消该事务中迄今为止所做的任何写入。
ACID的原子性描述了当客户想进行多次写入但在一些写操作处理完之后出现故障的情况。例如进程崩溃,网络连接中断,磁盘变满或者某种完整性约束被违反。如果这些写操作被分组到一个原子事务中,并且该事务由于错误而不能完成(提交),则该事务将被中止,并且数据库必须丢弃或撤消该事务中迄今为止所做的任何写入。
如果没有原子性,在多处更改进行到一半时发生错误,很难知道哪些更改已经生效,哪些没有生效。该应用程序可以再试一次,但冒着进行两次相同变更的风险,可能会导致数据重复或错误的数据。原子性简化了这个问题:如果事务被**中止abort**,应用程序可以确定它没有改变任何东西,所以可以安全地重试。
ACID原子性的定义特征是能够在错误时中止事务丢弃该事务进行的所有写入变更的能力。也许**可终止性abortability**是更好的术语,但本书将继续使用原子性,因为这是惯用词。
ACID原子性的定义特征是**能够在错误时中止事务,丢弃该事务进行的所有写入变更的能力。**或许**可中止性abortability**是更好的术语,但本书将继续使用原子性,因为这是惯用词。
#### 一致性Consistency
一致性这个词重载的很厉害:
* 在[第5章](ch5.md)中,我们讨论了副本一致性,以及异步复制系统中的最终一致性问题(参阅“[复制延迟问题](ch5.md#复制延迟问题)”)。
* **一致性散列Consistency Hash**是某些系统用于重新分区的一种分区方法(参阅“[一致性散列](ch6.md#一致性散列)”)
* 在CAP定理(参见[第9章](ch9.md#CAP定理)中,一致性一词用于表示**可线性化**(参见“[线性化](ch9.md#线性化)”)
* [一致性散列Consistency Hash](ch6.md#一致性散列))是某些系统用于重新分区的一种分区方法。
* 在[CAP定理](ch9.md#CAP定理)中,一致性一词用于表示[线性化](ch9.md#线性化)。
* 在ACID的上下文中**一致性**是指数据库在应用程序的特定概念中处于“良好状态”。
不幸的是,同一个词至少有四种不同的含义。
很不幸,这一个词就至少有四种不同的含义。
ACID一致性的概念是对数据的一组始终为真的特定陈述。即**不变量invariants**。例如,在会计系统中,所有账户的信用和债务必须始终保持平衡。如果一个事务从满足这些不变量的有效数据库开始,且在事务处理期间的任何写入都保持这种有效性,那么可以确定,不变量总是满足的。
ACID一致性的概念是**对数据的一组特定陈述必须始终成立**。即**不变量invariants**。例如,在会计系统中,所有账户整体上必须借贷相抵。如果一个事务开始于一个满足这些不变量的有效数据库,且在事务处理期间的任何写入操作都保持这种有效性,那么可以确定,不变量总是满足的。
但是,这种一致性的思想取决于应用程序的不变量的概念,应用程序有责任正确定义它的事务,以保持一致性。这不是数据库可以保证的事情:如果你写入违反不变量的脏数据,数据库无法阻止你。 (一些特定类型的不变量可以由数据库检查,例如使用外键约束或唯一性约束,但是一般来说,应用程序定义什么数据是有效的或者无效的—— 数据库只存储它。)
但是,一致性的这种概念取决于应用程序对不变量的观念,应用程序负责正确定义它的事务,并保持一致性。这并不是数据库可以保证的事情:如果你写入违反不变量的脏数据,数据库无法阻止你。 (一些特定类型的不变量可以由数据库检查,例如外键约束或唯一约束,但是一般来说,是应用程序来定义什么样的数据是有效的,什么样是无效的。—— 数据库只管存储。)
原子性隔离性和持久性是数据库的属性而一致性在ACID意义上是应用程序的属性。应用程序可能依赖数据库的原子性和隔离属性来实现一致性,但这并不取决于数据库本身。因此字母C不属于ACID[^i]。
原子性隔离性和持久性是数据库的属性而一致性在ACID意义上是应用程序的属性。应用程序可能依赖数据库的原子性和隔离属性来实现一致性但这并不取决于数据库。因此字母C不属于ACID[^i]。
[^i]: 乔·海勒斯坦Joe Hellerstein指出海德尔和路透社的论文中“ACID中的C”是被“扔进去凑缩写单词的”而且那时候C并没有被认为很重要
[^i]: 乔·海勒斯坦Joe Hellerstein指出论Härder与Reuter的论文中“ACID中的C”是被“扔进去凑缩写单词的”【7】而且那时候大家都不怎么在乎一致性
#### 隔离性Isolation
大多数数据库会同时被多个客户端访问。如果它们各自读写数据库的不同部分,这是没有问题的,但是如果它们正在访问相同的数据库记录,则可能会遇到**并发**问题(**竞争条件race conditions**)。
大多数数据库会同时被多个客户端访问。如果它们各自读写数据库的不同部分,这是没有问题的,但是如果它们访问相同的数据库记录,则可能会遇到**并发**问题(**竞争条件race conditions**)。
[图7-1](img/fig7-1.png)是这类问题的一个简单例子。假设你有两个客户同时增加一个存储在数据库中的计数器。每个客户端需要读取当前值加1再回写新值假设数据库中没有自增操作。在[图7-1](img/fig7-1.png)中计数器应该从42增至44因为发生了两次自增但由于竞态条件实际上只到了43。
[图7-1](img/fig7-1.png)是这类问题的一个简单例子。假设你有两个客户端同时在数据库中增长一个计数器。假设数据库中没有自增操作每个客户端需要读取计数器的当前值加1再回写新值。[图7-1](img/fig7-1.png)中因为发生了两次增长计数器应该从42增至44但由于竞态条件实际上只增至43。
ACID意义上的隔离性意味着同时执行的事务是相互隔离的:它们不能踩到彼此的脚。传统的数据库教科书将隔离性形式化为**可序列化Serializability**这意味着每个事务可以假装它是唯一在整个数据库上运行的事务。数据库确保当事务已经提交时结果与它们按顺序运行一个接一个是一样的尽管实际上它们可能是并发运行的【10】。
ACID意义上的隔离性意味着**同时执行的事务是相互隔离的**:它们不能相互冒犯。传统的数据库教科书将隔离性形式化为**可序列化Serializability**这意味着每个事务可以假装它是唯一在整个数据库上运行的事务。数据库确保当事务已经提交时结果与它们按顺序运行一个接一个是一样的尽管实际上它们可能是并发运行的【10】。
![](img/fig7-1.png)
**图7-1 两个客户之间的竞争状态同时递增计数器**
然而在实践中很少会使用可序列化隔离因为它会带来性能损失。一些流行的数据库如Oracle 11g甚至没有实现它。在Oracle中有一个名为“serializable”的隔离级别但实际上它实现了一种叫做**快照隔离**的功能这是一种比可序列化更弱的保证【8,11】。我们将在第233页的“[弱隔离等级]()”中探索快照隔离和其他形式的隔离。
然而实践中很少会使用可序列化隔离因为它有性能损失。一些流行的数据库如Oracle 11g甚至没有实现它。在Oracle中有一个名为“可序列化”的隔离级别但实际上它实现了一种叫做**快照隔离snapshot isolation**的功能,**这是一种比可序列化更弱的保证**【8,11】。我们将在“[弱隔离等级]()”中研究快照隔离和其他形式的隔离。
#### 持久性Durability
数据库系统的目的是提供一个安全的地方,可以存储数据而不用担心丢失数据。持久性是一个承诺,即一旦事务成功完成,即使存在硬件故障或数据库崩溃,所写的任何数据也不会被遗忘
数据库系统的目的是提供一个安全的地方存储数据而不用担心丢失。**持久性**是一个承诺,即一旦事务成功完成,即使发生硬件故障或数据库崩溃,写入的任何数据也不会丢失
在单节点数据库中,持久性通常意味着数据已被写入非易失性存储设备,如硬盘驱动器或SSD。它通常还包括预写日志或类似的文件参阅“[让B树更可靠](ch3.md#让B树更可靠)”),以便在磁盘上的数据结构损坏时进行恢复。在有复制的数据库中,可用性可能意味着数据已成功复制到一些节点。为了提供持久性保证,数据库必须等到这些写入或复制完成后才能报告事务成功提交。
在单节点数据库中持久性通常意味着数据已被写入非易失性存储设备如硬盘或SSD。它通常还包括预写日志或类似的文件参阅“[让B树更可靠](ch3.md#让B树更可靠)”),以便在磁盘上的数据结构损坏时进行恢复。在带复制的数据库中,持久性可能意味着数据已成功复制到一些节点。为了提供持久性保证,数据库必须等到这些写入或复制完成后才能报告事务成功提交。
如“[可靠性](ch1.md#可靠性)”一节所述,完美的持久性是不存在的:如果所有硬盘和所有备份同时被销毁,那显然没有任何数据库能救你。
如“[可靠性](ch1.md#可靠性)”一节所述,**完美的持久性是不存在的**:如果所有硬盘和所有备份同时被销毁,那显然没有任何数据库能救得了你。
> #### 复制和持久性
>
> 在历史上持久性意味着写入归档磁带。然后它被理解为写入磁盘或SSD。最近它已被改编为意味着复制。
>
> 哪种实现更好?
> 在历史上持久性意味着写入归档磁带。后来它被理解为写入硬盘或SSD。最近它已经适应了“复制replication”的新内涵。哪种实现更好一些
>
> 真相是,没有什么是完美的:
>
> * 如果您写入磁盘并且机器死机,即使您的数据没有丢失,在您修复机器或将磁盘传输到另一台机器之前,也无法访问。复制的系统可以保持可用
> * 一个相关的故障停电或一个在特定输入时导致所有节点崩溃的Bug可能会一次性删除所有副本请参阅「[可靠性](ch1.md#可靠性)」),丢失任何仅存在内存中的数据。因此内存数据库仍然与磁盘写入相关
> * 在异步复制系统中,当主库不可用时,最近的写入操作可能会丢失(参阅第156页的「[处理节点宕机](ch5.md#处理节点宕机)」)。
> * 当电源突然断电时,特别是固态硬盘被证明有时会违反应有的保证甚至fsync也不能保证正常工作【12】。磁盘固件可能有错误就像任何其他类型的软件一样【13,14】。
> * 如果你写入磁盘然后机器宕机,即使数据没有丢失,在修复机器或将磁盘转移到其他机器之前,也是无法访问的。这种情况下,复制系统可以保持可用性
> * 一个相关性故障停电或一个特定输入导致所有节点崩溃的Bug可能会一次性摧毁所有副本参阅「[可靠性](ch1.md#可靠性)」),任何仅存储在内存中的数据都会丢失,故内存数据库仍然要和磁盘写入打交道
> * 在异步复制系统中,当主库不可用时,最近的写入操作可能会丢失(参阅「[处理节点宕机](ch5.md#处理节点宕机)」)。
> * 当电源突然断电时,特别是固态硬盘有证据显示有时会违反应有的保证甚至fsync也不能保证正常工作【12】。硬盘固件可能有错误就像任何其他类型的软件一样【13,14】。
> * 存储引擎和文件系统之间的微妙交互可能会导致难以追踪的错误并可能导致磁盘上的文件在崩溃后被损坏【15,16】。
> * 磁盘上的数据可能会逐渐破损而不会被检测到【17】。如果数据已损坏一段时间副本和最近的备份也可能损坏。这种情况下,您将需要尝试从历史备份中恢复数据。
> * 一项关于固态硬盘的研究发现,在运行的前四年30到80的硬盘至少发生一个坏块【18】。磁性硬盘驱动器的坏道率较低但比SSD更高的完全故障率
> * 磁盘上的数据可能会在没有检测到的情况下逐渐损坏【17】。如果数据已损坏一段时间副本和最近的备份也可能损坏。这种情况下需要尝试从历史备份中恢复数据。
> * 一项关于固态硬盘的研究发现,在运行的前四年30到80的硬盘会产生至少一个坏块【18】。相比固态硬盘磁盘的坏道率较低但完全失效的概率更高
> * 如果SSD断电可能会在几周内开始丢失数据具体取决于温度【19】。
>
> 在实践中,没有一种技术可以提供绝对保证。只有各种降低风险的技术,包括写入磁盘,复制到远程机器和备份——它们可以且应该一起使用。与往常一样,最好抱着怀疑的态度采纳任何理论上的“保证”
> 在实践中,没有一种技术可以提供绝对保证。只有各种降低风险的技术,包括写入磁盘,复制到远程机器和备份——它们可以且应该一起使用。与往常一样,最好抱着怀疑的态度接受任何理论上的“保证”
### 单对象和多对象操作
回顾一下在ACID中原子性和隔离性描述了如果客户在同一事务中进行多次写入时数据库应该做的事情:
回顾一下在ACID中原子性和隔离性描述了客户端在同一事务中执行多次写入时,数据库应该做的事情:
***原子性***
如果在一系列写操作的中途发生错误,则应中止事务处理,并丢弃当前事务的所有写入。换句话说,数据库免去了用户对部分失败的担忧——通过提供“要么不做,要做全做”的保证。
如果在一系列写操作的中途发生错误,则应中止事务处理,并丢弃当前事务的所有写入。换句话说,数据库免去了用户对部分失败的担忧——通过提供“**宁为玉碎,不为瓦全**all-or-nothing”的保证。
***隔离性***
同时运行的事务不应该互相干扰。例如,如果一个事务进行多次写入,则另一个事务应该看到全部或者全部写入,而不是一些子集。
同时运行的事务不应该互相干扰。例如,如果一个事务进行多次写入,则另一个事务要么看到全部写入结果,要么什么都看不到,但不应该是一些子集。
这些定义假定您想一次修改几个对象文档记录。如果需要保持多个数据同步则通常需要这种多对象事务。图7-2显示了一个来自电子邮件应用程序的例子。要显示用户的未读邮件数量可以查询如下所示的内容
这些定义假设你想同时修改多个对象(行,文档,记录)。通常需要**多对象事务multi-object transaction**来保持多块数据同步。[图7-2](img/fig7-2.png)展示了一个来自电邮应用的例子。执行以下查询来显示用户未读邮件数量
```sql
SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
```
是,如果电子邮件太多,您可能会发现这个查询太慢,并决定将未读邮件的数量存储在一个单独的字段(一种规范化)中。现在,每当一个新的消息进来,你也必须增加未读的计数器,每当一个消息被标记为已读,你也必须减少未读的计数器。
如果邮件太多,你可能会觉得这个查询太慢,并决定用单独的字段存储未读邮件的数量(一种反规范化)。现在每当一个新消息写入时,必须也增长未读计数器,每当一个消息被标记为已读时,也必须减少未读计数器。
在[图7-2](img/fig7-2.png)中用户2遇到异常情况箱列表显示未读消息,但计数器显示零未读消息,因为计数器增量还没有发生[^ii]。隔离将通过确保用户2看到插入的电子邮件和更新的计数器或者都不是但不是一个不一致的中间点
在[图7-2](img/fig7-2.png)中用户2遇到异常情况件列表里显示有未读消息,但计数器显示为零未读消息,因为计数器增长还没有发生[^ii]。隔离性可以避免这个问题通过确保用户2要么同时看到新邮件和增长后的计数器要么都看不到。反正不会看到执行到一半的中间结果
[^ii]: 可以说,电子邮件应用程序中的错误计数器并不是特别重要的问题。 或者,考虑一个客户的账户余额,而不是未读的计数器,一次支付事务而不是电子邮件
[^ii]: 可以说邮件应用中的错误计数器并不是什么特别重要的问题。但换种方式来看,你可以把未读计数器换成客户账户余额,把邮件收发看成支付交易
![](img/fig7-2.png)
@ -159,59 +157,59 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
![](img/fig7-3.png)
**图7-3 原子性可以确保,如果发生错误,则该事务的任何先前写入都会被撤消,以避免不一致的状态。**
**图7-3 原子性确保发生错误,事务先前的任何写入都会被撤消,以避免状态不一致**
多对象事务需要某种方式来确定哪些读写操作属于同一个事务。在关系数据库中通常基于客户端到数据库服务器的TCP连接来完成在任何特定连接上`BEGIN TRANSACTION`和`COMMIT`语句之间的所有内容都被认为是同一事务的一部分.[^iii]
多对象事务需要某种方式来确定哪些读写操作属于同一个事务。在关系型数据库中通常基于客户端与数据库服务器的TCP连接在任何特定连接上`BEGIN TRANSACTION`和`COMMIT`语句之间的所有内容,被认为是同一事务的一部分.[^iii]
[^iii]: 这并不理想。如果TCP连接中断则事务必须中止。如果中断发生在客户端请求提交之后但在服务器确认提交发生之前客户端不知道事务是否已提交。为了解决这个问题事务管理器可以通过一个唯一的事务标识符对操作进行分组这个标识符没有绑定到特定的TCP连接。我们将回到第516页的“数据库的端到端的争论”中的这个主题。
[^iii]: 这并不完美。如果TCP连接中断则事务必须中止。如果中断发生在客户端请求提交之后但在服务器确认提交发生之前客户端不知道事务是否已提交。为了解决这个问题,事务管理器可以通过一个唯一事务标识符来对操作进行分组这个标识符并未绑定到特定TCP连接。后续再“[数据库端到端的争论](ch12.md#数据库端到端的争论)”一节将回到这个主题。
另一方面许多非关系数据库并没有将这些操作组合在一起的方法。即使存在多对象API例如键值存储可能具有在一个操作中更新几个键的多重放置操作但这并不一定意味着它具有事务语义该命令可能成功一些键和其他的失败,使数据库处于部分更新的状态。
另一方面许多非关系数据库并没有将这些操作组合在一起的方法。即使存在多对象API例如键值存储可能具有在一个操作中更新几个键的多重放置操作但这并不一定意味着它具有事务语义该命令可能在一些键上成功,在其他的键上失败,使数据库处于部分更新的状态。
#### 单对象写入
一个对象被改变时原子性和隔离也是适用的。例如假设您正在向数据库写入一个20 KB的JSON文档
单个对象发生改变时原子性和隔离也是适用的。例如假设您正在向数据库写入一个20 KB的JSON文档
- 如果在发送第一个10 KB之后网络连接中断数据库是否存储了不可解析的10 KB JSON片段
- 如果在数据库正在覆盖磁盘上的前一个值的过程中电源发生故障,是否最终将新旧值拼接在一起?
- 如果另一个客户端在写入过程中读取该文档,是否会看到部分更新的值?
这些问题会令人难以置信的困惑,因此存储引擎几乎普遍的目标是在一个节点上的单个对象例如键值对上提供原子性和隔离性。原子性可以通过使用日志来实现崩溃恢复请参阅第82页的“使B树可靠”并且可以使用每个对象的锁来实现隔离每次只允许一个线程访问对象 )。
这些问题非常让人头大,故存储引擎一个几乎普遍的目标是:对单节点上的单个对象例如键值对上提供原子性和隔离性。原子性可以通过使用日志来实现崩溃恢复请参阅第82页的“[使B树可靠]()”),并且可以使用每个对象的锁来实现隔离(每次只允许一个线程访问对象) )。
一些数据库也提供更复杂的原子操作,例如增操作,这样就不需要像[图7-1](img/fig7-1.png)那样的读取-修改-写入序列。同样流行的是**[比较和设置CAS, compare-and-set](#比较并设置CAS)**操作,只有当其他人没有同时更改该值时才允许进行写操作。
一些数据库也提供更复杂的原子操作,例如增操作,这样就不需要像[图7-1](img/fig7-1.png)那样的读取-修改-写入序列。同样流行的是**[比较和设置CAS, compare-and-set](#比较并设置CAS)**操作,当值没有并发被其他人修改过时,才允许执行写操作。
这些单对象操作很有用,因为它们可以防止在多个客户端尝试同时写入同一个对象时丢失更新(参阅“[防止丢失更新](#防止丢失更新)”。但是它们不是通常意义上的事务。CAS以及其他单一对象操作被称为“轻量级事务”甚至出于营销目的被称为“ACID”【20,21,22】但是这个术语是误导性的。事务通常被理解为将多个对象上的多个操作分组为一个执行单元的机制。[^iv]
这些单对象操作很有用,因为它们可以防止在多个客户端尝试同时写入同一个对象时丢失更新(参阅“[防止丢失更新](#防止丢失更新)”。但是它们不是通常意义上的事务。CAS以及其他单一对象操作被称为“轻量级事务”甚至出于营销目的被称为“ACID”【20,21,22】但是这个术语是误导性的。事务通常被理解为**将多个对象上的多个操作合并为一个执行单元的机制**。[^iv]
[^iv]: 严格地说,**原子自增atomic increment**这个术语在多线程编程的意义上使用了原子这个词。 在ACID的情况下它实际上应该被称为**孤立isolated**的或**可序列化serializable**的增量。 但这就太吹毛求疵了。
#### 多对象事务的需求
许多分布式数据存储已经放弃了多对象事务,因为它们很难跨分区实现,而且在需要高可用性或高性能的情况下,它们可能会碍事挡路。但就算是分布式系统,也并没有什么根本上能拒绝事务的原因,我们将在[第9章](ch9.md)讨论分布式事务的实现。
许多分布式数据存储已经放弃了多对象事务,因为多对象事务很难跨分区实现,而且在需要高可用性或高性能的情况下,它们可能会碍事。但说到底,在分布式数据库中实现事务,并没有什么根本性的障碍。[第9章](ch9.md)将讨论分布式事务的实现。
但是我们是否需要多对象事务?是否有可能只用键值数据模型和单对象操作来实现任何应用程序?
但是我们是否需要多对象事务?**是否有可能只用键值数据模型和单对象操作来实现任何应用程序?**
有一些场景中,单对象插入,更新和删除是足够的。但是许多其他场景需要协调写入几个不同的对象:
* 在关系数据模型中,一个表中的行通常具有对另一个表中的行的外键引用。 同样,在一个类似图形的数据模型中,一个顶点与其他顶点有边)。多对象事务允许你确保这些引用保持有效:当插入几个相互引用的记录时,外键必须是正确的,最新的,不然数据就会变的没有意义。
* 在文档数据模型中,需要一起更新的字段通常在同一个文档中,这被视为单个对象 - 更新单个文档时不需要多对象事务。但是缺乏连接功能的文档数据库也会鼓励非规范化请参阅第38页上的“[与文档数据库相关的对比]()”。当需要更新非规范化的信息时如图7-2所示需要一次更新多个文档。事务在这种情况下非常有用,可以防止非规范化的数据不同步。
* 在具有二级索引的数据库中(几乎除了纯粹的键值存储以外几乎都有),每次更改值时都需要更新索引。从事务角度来看,这些索引是不同的数据库对象:例如,如果没有事务隔离,记录可能出现在一个索引中,但没有出现在另一个索引中,因为第二个索引的更新还没有发生。
* 在关系数据模型中,一个表中的行通常具有对另一个表中的行的外键引用。 类似的是,在一个图数据模型中,一个顶点有着到其他顶点的边)。多对象事务使你确信这些引用始终有效:当插入几个相互引用的记录时,外键必须是正确的,最新的,不然数据就没有意义。
* 在文档数据模型中,需要一起更新的字段通常在同一个文档中,这被视为单个对象——更新单个文档时不需要多对象事务。但是,缺乏连接功能的文档数据库会鼓励非规范化(参阅“[关系型数据库与文档数据库在今日的对比](ch2.md#关系型数据库与文档数据库在今日的对比)”)。当需要更新非规范化的信息时,如[图7-2](img/fig7-2.png)所示,需要一次更新多个文档。事务在这种情况下非常有用,可以防止非规范化的数据不同步。
* 在具有二级索引的数据库中(除了纯粹的键值存储以外几乎都有),每次更改值时都需要更新索引。从事务角度来看,这些索引是不同的数据库对象:例如,如果没有事务隔离,记录可能出现在一个索引中,但没有出现在另一个索引中,因为第二个索引的更新还没有发生。
这些应用程序仍然可以在没有事务的情况下实现。然而,没有原子性,错误处理就变得复杂得多,缺乏隔离性,就会导致并发问题。我们将在“[弱隔离级别](#弱隔离级别)”中讨论这些问题,并在[第12章]()中探讨其他方法。
这些应用仍然可以在没有事务的情况下实现。然而,**没有原子性,错误处理就要复杂得多,缺乏隔离性,就会导致并发问题**。我们将在“[弱隔离级别](#弱隔离级别)”中讨论这些问题,并在[第12章]()中探讨其他方法。
#### 处理错误和中止
事务的一个关键特性是,如果发生错误,它可以中止并安全地重试。 ACID数据库是基于这样一种想法:如果数据库有违反其原子性,隔离性或持久性的危险,宁愿完全放弃事务,而不是留下完成的一半。
事务的一个关键特性是,如果发生错误,它可以中止并安全地重试。 ACID数据库基于这样的哲学:如果数据库有违反其原子性,隔离性或持久性的危险,宁愿完全放弃事务,而不是留下半成品
不是所有的系统都遵循这个理念。具体来讲具有无主复制的数据存储请参见第167页的“无引导复制”在“尽力而为”的基础做了不少工作可以概括为“数据库将尽其所能当运行遇到错误时它不会撤消它已经完成的事情“ ——所以这是应用程序的责任,从错误中恢复
然而并不是所有的系统都遵循这个哲学。特别是具有[无主复制](ch6.md#无主复制)的数据存储,主要是在“尽力而为”的基础上进行工作。可以概括为“数据库将做尽可能多的事,运行遇到错误时,它不会撤消它已经完成的事情“ ——所以,从错误中恢复是应用程序的责任
错误将不可避免地发生,但许多软件开发人员更喜欢只考虑乐观的情况而不是错误处理的复杂性。例如像Rails的ActiveRecord和Django这样的**对象关系映射ORM, object-relation Mapping**框架不会重试中断的事务—— 这个错误通常会导致一个异常,从堆栈向上传播。所以任何用户输入都会被丢弃,用户拿到一个错误信息。这实在是太耻辱了,因为中止的重点就是允许安全的重试。
错误发生不可避免,但许多软件开发人员倾向于只考虑乐观情况而不是错误处理的复杂性。例如像Rails的ActiveRecord和Django这样的**对象关系映射ORM, object-relation Mapping**框架不会重试中断的事务—— 这个错误通常会导致一个从堆栈向上传播的异常,所以任何用户输入都会被丢弃,用户拿到一个错误信息。这实在是太耻辱了,因为中止的重点就是允许安全的重试。
虽然重试一个中止的事务是一个简单而有效的错误处理机制,但它并不完美:
尽管重试一个中止的事务是一个简单而有效的错误处理机制,但它并不完美:
- 如果事务实际上成功了,但是在服务器试图确认成功提交给客户端(所以客户端认为失败)时网络发生故障,那么重试事务会导致它被执行两次,除非你有一个额外的应用级除重机制。
- 如果错误是由于负载过大造成的,则重试事务将使问题变得更糟,而不是更好。为了避免这种正反馈循环,可以限制重试次数,使用指数退避算法,并单独处理与过载相关的错误(如果允许)。
- 仅在临时性错误(例如,由于死锁,异常情况,临时性网络中断和故障转移)后才值得重试。在发生永久性错误(例如,违反约束)之后重试是毫无意义的。
- 如果事务在数据库之外也有副作用,即使事务被中止,也可能发生这些副作用。例如,如果您正在发送电子邮件,则每次重试事务时都重新发送电子邮件肯定不是你想要的。如果您想确保几个不同的系统提交或放弃在一起**二阶段提交2PC, two-phase commit**可以提供帮助(将在“[原子提交和两阶段提交2PC](ch9.md#原子提交与二阶段提交2PC)”中讨论这个问题)。
- 如果客户端进程在重试时失败,则任何试图写入数据库的数据都将丢失。
- 如果事务实际上成功了,但是在服务器试图向客户端确认提交成功时网络发生故障(所以客户端认为提交失败了),那么重试事务会导致事务被执行两次——除非你有一个额外的应用级除重机制。
- 如果错误是由于负载过大造成的,则重试事务将使问题变得更糟,而不是更好。为了避免这种正反馈循环,可以限制重试次数,使用指数退避算法,并单独处理与过载相关的错误(如果允许)。
- 仅在临时性错误(例如,由于死锁,异常情况,临时性网络中断和故障转移)后才值得重试。在发生永久性错误(例如,违反约束)之后重试是毫无意义的。
- 如果事务在数据库之外也有副作用,即使事务被中止,也可能发生这些副作用。例如,如果你正在发送电子邮件,那你肯定不希望每次重试事务时都重新发送电子邮件。如果你想确保几个不同的系统一起提交或放弃**二阶段提交2PC, two-phase commit**可以提供帮助(“[原子提交和两阶段提交2PC](ch9.md#原子提交与二阶段提交2PC)”中讨论这个问题)。
- 如果客户端进程在重试中失效,任何试图写入数据库的数据都将丢失。
@ -221,11 +219,11 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
如果两个事务不触及相同的数据,它们可以安全地**并行parallel**运行,因为两者都不依赖于另一个。当一个事务读取由另一个事务同时修改的数据时,或者当两个事务试图同时修改相同的数据时,并发问题(竞争条件)才会出现。
并发性错误很难通过测试找到,因为这样的错误只有在运行不正常时才会触发。这样的时机可能很少发生,通常很难重现。并发性也很难推理,特别是在大型应用程序中,您不一定知道哪些其他代码正在访问数据库。应用程序开发在一次只有一个用户时就很麻烦了,有许多并发用户使得它更加困难,因为任何一个数据都可能随时改变。
并发BUG很难通过测试找到,因为这样的错误只有在运行不正常时才会触发。这样的时机可能很少,通常很难重现。并发性也很难推理,特别是在大型应用程序中,您不一定知道哪些其他代码正在访问数据库。应用程序开发在一次只有一个用户时就很麻烦了,有许多并发用户使得它更加困难,因为任何一个数据都可能随时改变。
出于这个原因,数据库一直试图通过提供**事务隔离transaction isolation**来隐藏应用程序开发者的并发问题。从理论上讲,隔离可以通过假装没有并发发生,让你的生活更加轻松:**可序列化serializable**的隔离登记意味着数据库保证事务的效果与连续运行(即一次一个,没有任何并发)是一样的。
出于这个原因,数据库一直试图通过提供**事务隔离transaction isolation**来隐藏应用程序开发者的并发问题。从理论上讲,隔离可以通过假装没有并发发生,让你的生活更加轻松:**可序列化serializable**的隔离等级意味着数据库保证事务的效果与连续运行(即一次一个,没有任何并发)是一样的。
实际上不幸的是:隔离并没有那么简单。**可序列化**会有性能损失,许多数据库不愿意付这个代价[8]。因此系统通常使用较弱的隔离级别来防止一些并发问题但不是全部。这些隔离级别难以理解并且会导致微妙的错误但是它们仍然在实践中被使用【23】。
实际上不幸的是:隔离并没有那么简单。**可序列化**会有性能损失,许多数据库不愿意支付这个代价【8】。因此系统通常使用较弱的隔离级别来防止一些并发问题但不是全部。这些隔离级别难以理解并且会导致微妙的错误但是它们仍然在实践中被使用【23】。
并发性错误导致的并发性错误不仅仅是一个理论问题。他们造成了很多的资金损失【24,25】耗费了财务审计人员的调查【26】并导致客户数据被破坏【27】。关于这类问题的一个流行的评论是“如果你正在处理财务数据请使用ACID数据库” ——但是这一点没有提到。即使是很多流行的关系型数据库系统通常被认为是“ACID”也使用弱隔离级别所以它们也不一定能防止这些错误的发生。
@ -256,14 +254,14 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
为什么要防止脏读,有几个原因:
- 如果事务需要更新多个对象脏读取意味着另一个事务可能会只看到一部分更新。例如在图7-2中用户看到新的未读电子邮件但看不到更新的计数器。这就是电子邮件的脏读。看到处于部分更新状态的数据库会让用户感到困惑并可能导致其他事务做出错误的决定。
- 如果事务中止则所有写入操作都需要回滚如图7-3所示。如果数据库允许脏读那就意味着一个事务可能会看到稍后需要回滚的数据即从未实际提交给数据库的数据。想想后果就让人头大。
- 如果事务需要更新多个对象,脏读取意味着另一个事务可能会只看到一部分更新。例如,在[图7-2](img/fig7-2.png)中,用户看到新的未读电子邮件,但看不到更新的计数器。这就是电子邮件的脏读。看到处于部分更新状态的数据库会让用户感到困惑,并可能导致其他事务做出错误的决定。
- 如果事务中止,则所有写入操作都需要回滚(如[图7-3](img/fig7-3.png)所示)。如果数据库允许脏读,那就意味着一个事务可能会看到稍后需要回滚的数据,即从未实际提交给数据库的数据。想想后果就让人头大。
#### 没有脏写
如果两个事务同时尝试更新数据库中的相同对象,会发生什么情况?我们不知道写入的顺序是怎样的,但是我们通常认为后面的写入会覆盖前面的写入。
但是,如果先前的写入是尚未提交事务的一部分,又会发生什么情况,后面的写入会覆盖一个尚未提交的值?这被称作**脏写dirty write**[28]。在**读已提交**的隔离级别上运行的事务必须防止脏写,通常是延迟第二次写入,直到第一次写入事务提交或中止为止。
但是,如果先前的写入是尚未提交事务的一部分,又会发生什么情况,后面的写入会覆盖一个尚未提交的值?这被称作**脏写dirty write**【28】。在**读已提交**的隔离级别上运行的事务必须防止脏写,通常是延迟第二次写入,直到第一次写入事务提交或中止为止。
通过防止脏写,这个隔离级别避免了一些并发问题:
@ -276,9 +274,9 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
#### 实现读已提交
读已提交是一个非常流行的隔离级别。这是Oracle 11gPostgreSQLSQL Server 2012MemSQL和其他许多数据库的默认设置【8】。
**读已提交**是一个非常流行的隔离级别。这是Oracle 11gPostgreSQLSQL Server 2012MemSQL和其他许多数据库的默认设置【8】。
最常见的情况是,数据库通过使用**行锁row-level lock**来防止脏写:当事务想要修改特定对象(行或文档)时,它必须首先获得该对象的锁。然后必须持有该锁直到事务被提交或中止。一次只有一个事务可持有任何给定对象的锁;如果另一个事务要写入同一个对象,则必须等到第一个事务提交或中止后才能获取该锁并继续。这种锁定是读已提交模式(或更强的隔离级别)的数据库自动完成的。
最常见的情况是,数据库通过使用**行锁row-level lock**来防止脏写:当事务想要修改特定对象(行或文档)时,它必须首先获得该对象的锁。然后必须持有该锁直到事务被提交或中止。一次只有一个事务可持有任何给定对象的锁;如果另一个事务要写入同一个对象,则必须等到第一个事务提交或中止后才能获取该锁并继续。这种锁定是读已提交模式(或更强的隔离级别)的数据库自动完成的。
如何防止脏读?一种选择是使用相同的锁,并要求任何想要读取对象的事务来简单地获取该锁,然后在读取之后立即再次释放该锁。这能确保不会读取进行时,对象不会在脏的状态,有未提交的值(因为在那段时间锁会被写入该对象的事务持有)。
@ -292,7 +290,7 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
如果只从表面上看读已提交隔离级别你就认为它完成了事务所需的一切,那是可以原谅的。它允许**中止**(原子性的要求);它防止读取不完整的事务结果,并排写入的并发写入。事实上这些功能非常有用,比起没有事务的系统来,可以提供更多的保证。
但是在使用此隔离级别时,仍然有很多地方可能会产生并发错误。例如图7-6说明了读已提交时可能发生的问题。
但是在使用此隔离级别时,仍然有很多地方可能会产生并发错误。例如[图7-6](img/fig7-6.png)说明了读已提交时可能发生的问题。
![](img/fig7-6.png)
@ -302,7 +300,7 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
这种异常被称为**不可重复读nonrepeatable read**或**读取偏差read skew**如果Alice在事务结束时再次读取账户1的余额她将看到与她之前的查询中看到的不同的值600美元。在读已提交的隔离条件下**不可重复读**被认为是可接受的Alice看到的帐户余额时确实在阅读时已经提交了。
> 不幸的是,术语**偏差skew** 这个词是过载的:我们以前使用它是因为热点的不平衡工作量(参阅“[偏斜的工作负荷和减轻热点]()”),而这里偏差意味着异常的时机。
> 不幸的是,术语**偏差skew** 这个词是过载的:我们以前使用它是因为热点的不平衡工作量(参阅“[偏斜的负载倾斜与消除热点](ch6.md#负载倾斜与消除热点)”),而这里偏差意味着异常的时机。
对于Alice的情况这不是一个长期持续的问题。因为如果她几秒钟后刷新银行网站的页面她很可能会看到一致的帐户余额。但是有些情况下不能容忍这种暂时的不一致
@ -312,9 +310,9 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
***分析查询和完整性检查***
有时,您可能需要运行一个查询,扫描大部分的数据库。这样的查询在分析中很常见(参阅“[事务处理或分析?]()”),也可能是定期完整性检查(即监视数据损坏)的一部分。如果这些查询在不同时间点观察数据库的不同部分,则可能会返回毫无意义的结果。
有时,您可能需要运行一个查询,扫描大部分的数据库。这样的查询在分析中很常见(参阅“[事务处理或分析?](ch3.md#事务处理还是分析?)”),也可能是定期完整性检查(即监视数据损坏)的一部分。如果这些查询在不同时间点观察数据库的不同部分,则可能会返回毫无意义的结果。
**快照隔离snapshot isolation**[28]是这个问题最常见的解决方案。想法是,每个事务都从数据库的**一致快照consistent snapshot**中读取——也就是说,事务可以看到事务开始时在数据库中提交的所有数据。即使这些数据随后被另一个事务更改,每个事务也只能看到该特定时间点的旧数据。
**快照隔离snapshot isolation**【28】是这个问题最常见的解决方案。想法是,每个事务都从数据库的**一致快照consistent snapshot**中读取——也就是说,事务可以看到事务开始时在数据库中提交的所有数据。即使这些数据随后被另一个事务更改,每个事务也只能看到该特定时间点的旧数据。
快照隔离对长时间运行的只读查询(如备份和分析)非常有用。如果查询的数据在查询执行的同时发生变化,则很难理解查询的含义。当一个事务可以看到数据库在某个特定时间点冻结时的一致快照,理解起来就很容易了。
@ -328,7 +326,7 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
如果一个数据库只需要提供**读已提交**的隔离级别,而不提供**快照隔离**那么保留一个对象的两个版本就足够了提交的版本和被覆盖但尚未提交的版本。支持快照隔离的存储引擎通常也使用MVCC来实现**读已提交**隔离级别。一种典型的方法是**读已提交**为每个查询使用单独的快照,而**快照隔离**对整个事务使用相同的快照。
[图7-7]()说明了如何在PostgreSQL中实现基于MVCC的快照隔离[31](其他实现类似)。当一个事务开始时,它被赋予一个唯一的,永远增长[^vii]的事务ID`txid`。每当事务向数据库写入任何内容时它所写入的数据都会被标记上写入者的事务ID。
[图7-7]()说明了如何在PostgreSQL中实现基于MVCC的快照隔离【31】(其他实现类似)。当一个事务开始时,它被赋予一个唯一的,永远增长[^vii]的事务ID`txid`。每当事务向数据库写入任何内容时它所写入的数据都会被标记上写入者的事务ID。
[^vii]: 事实上事务ID是32位整数所以大约会在40亿次事务之后溢出。 PostgreSQL的Vacuum过程会清理老旧的事务ID确保事务ID溢出回卷不会影响到数据。
@ -338,7 +336,7 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
表中的每一行都有一个`created_by`字段其中包含将该行插入到表中的的事务ID。此外每行都有一个`deleted_by`字段,最初是空的。如果某个事务删除了一行,那么该行实际上并未从数据库中删除,而是通过将`deleted_by`字段设置为请求删除的事务的ID来标记为删除。在稍后的时间当确定没有事务可以再访问已删除的数据时数据库中的垃圾收集过程会将所有带有删除标记的行移除并释放其空间。
UPDATE操作在内部翻译为DELETE和INSERT。例如在[图7-7]()中事务13从账户2中扣除100美元将余额从500美元改为400美元。实际上包含两条账户2的记录余额为\$500的行被标记为**被事务13删除**,余额为\$400的行**由事务13创建**。
`UPDATE`操作在内部翻译为`DELETE``INSERT`。例如,在[图7-7]()中事务13从账户2中扣除100美元将余额从500美元改为400美元。实际上包含两条账户2的记录余额为\$500的行被标记为**被事务13删除**,余额为\$400的行**由事务13创建**。
#### 观察一致性快照的可见性规则
@ -402,11 +400,11 @@ UPDATE counters SET value = value + 1 WHERE key = 'foo';
类似地像MongoDB这样的文档数据库提供了对JSON文档的一部分进行本地修改的原子操作Redis提供了修改数据结构如优先级队列的原子操作。并不是所有的写操作都可以用原子操作的方式来表达例如维基页面的更新涉及到任意文本编辑[^viii],但是在可以使用原子操作的情况下,它们通常是最好的选择。
[^viii]: 将文本文档的编辑表示为原子的变化流是可能的,尽管相当复杂。参阅第174页上的“自动冲突解决”。
[^viii]: 将文本文档的编辑表示为原子的变化流是可能的,尽管相当复杂。参阅“[自动冲突解决](ch5.md#题外话:自动冲突解决)”。
原子操作通常通过在读取对象时,获取其上的排它锁来实现。以便更新完成之前没有其他事务可以读取它。这种技术有时被称为**游标稳定性cursor stability**【36,37】。另一个选择是简单地强制所有的原子操作在单一线程上执行。
不幸的是ORM框架很容易意外地执行不安全的读取-修改-写入循环,而不是使用数据库提供的原子操作[38]。如果你知道自己在做什么那当然不是问题但它经常产生那种很难测出来的微妙Bug。
不幸的是ORM框架很容易意外地执行不安全的读取-修改-写入序列而不是使用数据库提供的原子操作【38】。如果你知道自己在做什么那当然不是问题但它经常产生那种很难测出来的微妙Bug。
#### 显式锁定
@ -441,7 +439,7 @@ COMMIT;
#### 比较并设置CAS
在不提供事务的数据库中有时会发现一种原子操作比较并设置CAS, Compare And Set先前在“[单对象写入]()”中提到)。此操作的目的是为了避免丢失更新:只有当前值从上次读取时一直未改变,才允许更新发生。如果当前值与先前读取的值不匹配,则更新不起作用,且必须重试读取-修改-写入序列。
在不提供事务的数据库中,有时会发现一种原子操作:**比较并设置CAS, Compare And Set**(先前在“[单对象写入]()”中提到)。此操作的目的是为了避免丢失更新:只有当前值从上次读取时一直未改变,才允许更新发生。如果当前值与先前读取的值不匹配,则更新不起作用,且必须重试读取-修改-写入序列。
例如为了防止两个用户同时更新同一个wiki页面可以尝试类似这样的方式只有当用户开始编辑页面内容时才会发生更新
@ -487,7 +485,7 @@ UPDATE wiki_pages SET content = '新内容'
可以将写入偏差视为丢失更新问题的一般化。如果两个事务读取相同的对象,然后更新其中一些对象(不同的事务可能更新不同的对象),则可能发生写入偏差。在多个事务更新同一个对象的特殊情况下,就会发生脏写或丢失更新(取决于时机)。
我们看到,有各种不同的方法来防止丢失的更新。随着写歪斜,我们的选择更受限制:
我们看到,有各种不同的方法来防止丢失的更新。随着写偏差,我们的选择更受限制:
* 由于涉及多个对象,单对象的原子操作不起作用。
* 不幸的是在一些快照隔离的实现中自动检测丢失更新对此并没有帮助。在PostgreSQL的可重复读MySQL/InnoDB的可重复读Oracle可序列化或SQL Server的快照隔离级别中都不会自动检测写入偏差【23】。自动防止写入偏差需要真正的可序列化隔离请参见“[可序列化](#可序列化)”)。
@ -624,7 +622,7 @@ COMMIT;
在这种交互式的事务方式中,应用程序和数据库之间的网络通信耗费了大量的时间。如果不允许在数据库中进行并发处理,且一次只处理一个事务,则吞吐量将会非常糟糕,因为数据库大部分的时间都花费在等待应用程序发出当前事务的下一个查询。在这种数据库中,为了获得合理的性能,需要同时处理多个事务。
出于这个原因具有单线程串行事务处理的系统不允许交互式的多语句事务。取而代之应用程序必须提前将整个事务代码作为存储过程提交给数据库。这些方法之间的差异如图7-9所示。如果事务所需的所有数据都在内存中则存储过程可以非常快地执行而不用等待任何网络或磁盘I/O。
出于这个原因,具有单线程串行事务处理的系统不允许交互式的多语句事务。取而代之,应用程序必须提前将整个事务代码作为存储过程提交给数据库。这些方法之间的差异如[图7-9](img/fig7-9.png)所示。如果事务所需的所有数据都在内存中则存储过程可以非常快地执行而不用等待任何网络或磁盘I/O。
![](img/fig7-9.png)
@ -715,7 +713,7 @@ VoltDB还使用存储过程进行复制但不是将事务的写入结果从
#### 谓词锁
在前面关于锁的描述中,我们掩盖了一个微妙而重要的细节。在第250页的“[导致写入偏差的幻读](#导致写入偏差的幻读)”中,我们讨论了**幻读phantoms**的问题。即一个事务改变另一个事务的搜索查询的结果。具有可序列化隔离级别的数据库必须防止**幻读**。
在前面关于锁的描述中,我们掩盖了一个微妙而重要的细节。在“[导致写入偏差的幻读](#导致写入偏差的幻读)”中,我们讨论了**幻读phantoms**的问题。即一个事务改变另一个事务的搜索查询的结果。具有可序列化隔离级别的数据库必须防止**幻读**。
在会议室预订的例子中这意味着如果一个事务在某个时间窗口内搜索了一个房间的现有预订见例7-2则另一个事务不能同时插入或更新同一时间窗口与同一房间的另一个预订 (可以同时插入其他房间的预订,或在不影响另一个预定的条件下预定同一房间的其他时间段)。
@ -833,31 +831,31 @@ WHERE room_id = 123 AND
## 本章小结
事务是一个抽象层,允许应用程序假装某些并发问题和某些类型的硬件和软件故障不存在。大量的错误被简化为简单的事务中止,而应用程序只需要再次尝试。
事务是一个抽象层,允许应用程序假装某些并发问题和某些类型的硬件和软件故障不存在。各式各样的错误被简化为一种简单情况:**事务中止transaction abort**,而应用需要的仅仅是重试。
在本章中,我们看到许多事务有助于防止的问题。并非所有的应用程序都容易出现所有这些问题:具有非常简单的访问模式的应用程序(例如只读和写一条记录)可能无需事务管理。但是,对于更复杂的访问模式,事务可以大大减少您需要考虑的潜在错误情况的数量。
在本章中介绍了很多问题,事务有助于防止这些问题发生。并非所有应用都易受此类问题影响:具有非常简单访问模式的应用(例如每次读写单条记录)可能无需事务管理。但是对于更复杂的访问模式,事务可以大大减少需要考虑的潜在错误情景数量。
没有事务处理,各种错误情况(进程崩溃,网络中断,停电,磁盘已满,意外并发等)意味着数据可能以各种方式变得不一致。例如,非规化的数据可能很容易与源数据不同步。如果没有事务处理,就很难推断复杂的交互访问可能对数据库造成的影响。
如果没有事务处理,各种错误情况(进程崩溃,网络中断,停电,磁盘已满,意外并发等)意味着数据可能以各种方式变得不一致。例如,非规化的数据可能很容易与源数据不同步。如果没有事务处理,就很难推断复杂的交互访问可能对数据库造成的影响。
在本章中,我们深入到并发控制的主题。我们讨论了几个广泛使用的隔离级别,特别是读取提交,快照隔离(有时称为可重复读取)和可序列化。我们通过讨论种族条件的各种示例来描述这些竟态条件
本章深入讨论了**并发控制**的话题。我们讨论了几个广泛使用的隔离级别,特别是**读已提交****快照隔离**(有时称为可重复读)和**可序列化**。并通过研究竞争条件的各种例子,来描述这些隔离等级
***脏读***
一个客户端在提交之前读取另一个客户端的写入。读提交的隔离级别和更强的级别防止脏读。
一个客户端读取到另一个客户端尚未提交的写入。**读已提交**或更强的隔离级别可以防止脏读。
***脏写***
一个客户端会覆盖另一个客户端已经写入但尚未提交的数据。几乎所有的事务实现都可以防止脏写。
一个客户端覆盖写入了另一个客户端尚未提交的写入。几乎所有的事务实现都可以防止脏写。
***读取偏(不可重复读)***
***读取偏(不可重复读)***
客户在不同的时间点看到数据库的不同部分。快照隔离最常遇到这个问题它允许事务在一个时间点从一致的快照中读取数据。它通常使用多版本并发控制MVCC来实现。
在同一个事务中,客户端在不同的时间点会看见数据库的不同状态。**快照隔离**经常用于解决这个问题,它允许事务从一个特定时间点的一致性快照中读取数据。快照隔离通常使用**多版本并发控制MVCC**来实现。
***丢失的更新***
***更新丢失***
两个客户端同时执行读取 修改 - 写周期。一个覆盖另一个的写入而不包含其变化,所以数据丢失。快照隔离的一些实现可以自动防止这种异常,而另一些实现则需要手动锁定(`SELECT FOR UPDATE`)。
两个客户端同时执行**读取-修改-写入序列**。其中一个写操作,在没有合并另一个写入变更情况下,直接覆盖了另一个写操作的结果。所以导致数据丢失。快照隔离的一些实现可以自动防止这种异常,而另一些实现则需要手动锁定(`SELECT FOR UPDATE`)。
***写歪斜***
***写偏差***
一个事务读取一些东西,根据它所看到的值作出决定,并将决定写入数据库。但是,写作的时候,决定的前提不再是真实的。只有可序列化的隔离才能防止这种异常。
@ -867,21 +865,21 @@ WHERE room_id = 123 AND
弱隔离级别可以防止这些异常情况,但是让应用程序开发人员手动处理其他应用程序(例如,使用显式锁定)。只有可序列化的隔离才能防范所有这些问题。我们讨论了实现可序列化事务的三种不同方法:
***按照连续顺序从字面上执行事务***
***字面意义的串行执行***
如果您可以使每个事务的执行速度非常快并且事务吞吐量足够低以在单个CPU内核上处理这是一个简单而有效的选择。
如果每个事务的执行速度非常快,并且事务吞吐量足够低,足以在单个CPU内核上处理这是一个简单而有效的选择。
***两锁定***
***两阶段锁定***
数十年来,这一直是实现可串行化的标准方式,但是许多应用程序由于其性能特性而避免使用它。
数十年来,两阶段锁定一直是实现可序列化的标准方式,但是许多应用出于性能问题的考虑避免使用它。
***可串行化快照隔离SSI***
一个相当新的算法,避免了以前方法的大部分缺点。它使用乐观的方法,允许事务进行而不会阻塞。当一个事务想要提交时,它会被检查,如果执行不可序列化,它将被中止。
一个相当新的算法,避免了先前方法的大部分缺点。它使用乐观的方法,允许事务执行而无需阻塞。当一个事务想要提交时,它会进行检查,如果执行不可序列化,事务就会被中止。
本章中的示例使用关系数据模型。但是,正如在讨论中,无论使用哪种数据模型,第231页上的“多对象事务的需要”事务都是有价值的数据库功能。
本章中的示例主要是在关系数据模型的上下文中。使用关系数据模型。但是,正如在讨论中,无论使用哪种数据模型,如“**[多对象事务的需求](#多对象事务的需求)**”中所讨论的,事务都是重要的数据库功能。
在本章中,我们主要探讨了在一台机器上运行数据库的情况下的想法和算法。分布式数据库中的事务开启了一系列新的困难挑战,我们将在接下来的两章中讨论。
本章主要是在单机数据库的上下文中,探讨了各种概念与想法。分布式数据库中的事务,则引入了一系列新的困难挑战,将在接下来的两章中讨论。