├── .gitignore
├── .nojekyll
├── CNAME
├── README.md
├── _config.yml
├── _sidebar.md
├── ch01.md
├── ch02.md
├── ch03.md
├── ch04.md
├── ch05.md
├── ch06.md
├── ch07.md
├── ch08.md
├── ch09.md
├── ch10.md
├── ch11.md
├── css
└── waline.css
├── img
├── ch01-book-software-design.jpeg
├── ch01-data-society.png
├── ch01-fig01.png
├── ch01-fig02.png
├── ch01-fig03.png
├── ch02-06.png
├── ch02-fig01.png
├── ch02-fig02.png
├── ch02-fig05.png
├── ch02-how-mr-works.png
├── ch02-layered-data-models.png
├── ch02-semantic-web-stack.png
├── ch02-spo.png
├── ch03-fig01.png
├── ch03-fig03.png
├── ch03-fig04.png
├── ch03-fig05.png
├── ch03-fig06.png
├── ch03-fig07.png
├── ch03-fig08.png
├── ch03-fig09.png
├── ch03-fig10.png
├── ch03-fig11.png
├── ch03-fig12.png
├── ch03-sized-tiered.png
├── ch04-encodec.png
├── ch04-fig01.png
├── ch04-fig02.png
├── ch04-fig03.png
├── ch04-fig04.png
├── ch04-fig05.png
├── ch04-fig06.png
├── ch04-fig07.png
├── ch05-fig01.png
├── ch05-fig02.png
├── ch05-fig03.png
├── ch05-fig04.png
├── ch05-fig05.png
├── ch05-fig06.png
├── ch05-fig07.png
├── ch05-fig08.png
├── ch05-fig09.png
├── ch05-fig10.png
├── ch05-fig11.png
├── ch05-fig12.png
├── ch05-fig13.png
├── ch05-fig14.png
├── ch05-sloppy-quorum.png
├── ch06-dynamo.png
├── ch06-fig01.png
├── ch06-fig02.png
├── ch06-fig03.png
├── ch06-fig04.png
├── ch06-fig05.png
├── ch06-fig06.png
├── ch06-fig07.png
├── ch06-fig08.png
├── ch07-fig01.png
├── ch07-fig02.png
├── ch07-fig03.png
├── ch07-fig04.png
├── ch07-fig05.png
├── ch07-fig06.png
├── ch07-fig07.png
├── ch07-fig08.png
├── ch07-fig09.png
├── ch07-fig10.png
├── ch07-fig11.png
├── ch08-fig01.png
├── ch08-fig02.png
├── ch08-fig03.png
├── ch08-fig04.png
├── ch08-fig05.png
├── ch09-fig01.png
├── ch09-fig02.png
├── ch09-fig03.png
├── ch09-fig04.png
├── ch09-fig05.png
├── ch09-fig06.png
├── ch09-fig07.png
├── ch09-fig08.png
├── ch09-fig09.png
├── ch09-fig10.png
├── ch10-fig01.png
├── ch10-fig02.png
├── ch10-fig03.png
├── ch11-fig01.png
├── ch11-fig02.png
├── ch11-fig03.png
├── ch11-fig04.png
├── ch11-fig05.png
├── ch11-fig06.png
├── ch11-fig07.png
├── wechat-column.jpg
└── xiaobot-column.png
├── index.html
├── js
├── docsify-pagination.min.js
├── docsify.min.js
├── search.min.js
└── vue.css
└── preface.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
--------------------------------------------------------------------------------
/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/.nojekyll
--------------------------------------------------------------------------------
/CNAME:
--------------------------------------------------------------------------------
1 | ddia.qtmuniao.com
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 《DDIA 逐章精读》小册
2 |
3 | 在理解英文原文的基础上,结合我的一些工作经验,进行一些相应扩展,并参考 [github 上 Vonng 的中文翻译版](https://github.com/Vonng/ddia),对每一章用中文重新组织,作为每次分享的文字稿,在此集结为一本开源小册,并附上每章分享的**录屏视频**([b 站](https://space.bilibili.com/30933812/channel/collectiondetail?sid=240551), [Youtube](https://www.youtube.com/playlist?list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g)),希望可以对有需要的同学有所帮助,水平所限,难免疏漏,如发现有任何有误之处,欢迎[提 issue](https://github.com/DistSysCorp/ddia/issues/new) 和 [PR](https://github.com/DistSysCorp/ddia/compare)。
4 |
5 | 这个小册的电子版在[这里](https://mbd.pub/o/bread/ZJaXlZdt),之后更新完后,电子书也会随之更新。
6 |
7 | ## 目录
8 |
9 | ### [序](preface.md)
10 |
11 | ### 第一部分:数据系统基础
12 |
13 | - [第一章:可靠、可扩展、可维护](ch01.md) [[b 站](https://www.bilibili.com/video/BV1bY411L7HA) [Youtube](https://www.youtube.com/watch?v=geVsm3YGF4A&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=1)]
14 | - [本书为什么以数据系统为主题](ch01.md#本书为什么以数据系统为主题)
15 | - [可靠性](ch01.md#可靠性)
16 | - [可伸缩性](ch01.md#可伸缩性)
17 | - [可维护性](ch01.md#可维护性)
18 | - [第二章:数据模型和查询语言](ch02.md) [b 站 ([上](https://www.bilibili.com/video/BV19a411C7UN) [下](https://www.bilibili.com/video/BV1BZ4y1r79M)) Youtube ([上](https://www.youtube.com/watch?v=1TVdmZ_sJCM&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=2) [下](https://www.youtube.com/watch?v=GdihZOSMuuI&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=3))]
19 | - [概要](ch02.md#概要)
20 | - [关系模型 vs 文档模型](ch02.md#关系模型与文档模型)
21 | - [数据查询语言](ch02.md#数据查询语言)
22 | - [图模型](ch02.md#图模型)
23 | - [第三章:存储与查询](ch03.md) [b 站 ([上](https://www.bilibili.com/video/BV1mL411P72H/) [下](https://www.bilibili.com/video/BV1bL411A7ga)) Youtube([上](https://www.youtube.com/watch?v=tI1BmIZpK-c&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=4) [下](https://www.youtube.com/watch?v=8wiJSECzADc&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=5))]
24 | - [底层数据结构](ch03.md#驱动数据库的底层数据结构)
25 | - [事务型还是分析型](ch03.md#事务型还是分析型)
26 | - [列式存储](ch03.md#列存)
27 | - [第四章:编码和演进](ch04.md) [[b 站](https://www.bilibili.com/video/BV1Aa411q7u9) [Youtube](https://www.youtube.com/watch?v=DqddHDVTkps&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=6)]
28 | - [数据编码的格式](ch04.md#数据编码的格式)
29 | - [几种数据流模型](ch04.md#几种数据流模型)
30 |
31 | ### 第二部分:分布式数据
32 |
33 | - [第五章:冗余](ch05.md) [ b 站 ([上](https://www.bilibili.com/video/BV1VR4y1K7eK) [下](https://www.bilibili.com/video/BV1ou4116779)) Youtube([上](https://www.youtube.com/watch?v=pbURsaoKiYc&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=7) [下](https://www.youtube.com/watch?v=y23oqgIG7Vw&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=8))]
34 | - [领导者与跟随者](ch05.md#领导者与跟随者)
35 | - [复制滞后问题](ch05.md#复制滞后问题)
36 | - [多主模型](ch05.md#多主模型)
37 | - [无主模型](ch05.md#无主模型)
38 | - [第六章:分区](ch06.md) [b 站 ([上](https://www.bilibili.com/video/BV1tY4y157Np) [下](https://www.bilibili.com/video/BV1AA4y1f7Hi)) Youtue ([上](https://www.youtube.com/watch?v=7vvycyhJn1s&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=9) [下](https://www.youtube.com/watch?v=aPeHdQgBmi4&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=10))]
39 | - [分片与复制](ch06.md#分片与复制)
40 | - [键值对集的分片](ch06.md#键值对集的分片)
41 | - [分片和次级索引](ch06.md#分片和次级索引)
42 | - [分片均衡](ch06.md#分片均衡)
43 | - [请求路由](ch06.md#请求路由)
44 | - [第七章:事务](ch07.md) [ b 站 ([上](https://www.bilibili.com/video/BV1d94y117pW) [中](https://www.bilibili.com/video/BV1u3411w765) [下](https://www.bilibili.com/video/BV1Qr4y1M7Zm)) Youtube ([上](https://www.youtube.com/watch?v=gbExnxslpCs&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=11) [中](https://www.youtube.com/watch?v=sDKKhGFyUmk&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=12) [下](https://www.youtube.com/watch?v=Lhs6H6IgFvY&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=13))]
45 | - [棘手的概念](ch07.md#棘手的概念)
46 | - [几种弱隔离级别](ch07.md#几种弱隔离级别)
47 | - [可串行化](ch07.md#可串行化)
48 | - [第八章:分布式系统中的麻烦事](ch08.md) [b 站 ([上](https://www.bilibili.com/video/BV1Ad4y1D7Yy) [中](https://www.bilibili.com/video/BV1114y1L7wU) [下](https://www.bilibili.com/video/BV1uG411A7GK)) Youtube ([上](https://www.youtube.com/watch?v=-q-yY_0aCsg&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=13) [中](https://www.youtube.com/watch?v=mk-QfuBV_NQ&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=14) [下](https://www.youtube.com/watch?v=xhk-X-rkLU4&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=15))]
49 | - [故障和部分失败](ch08.md#故障和部分失败)
50 | - [不可靠的网络](ch08.md#不可靠的网络)
51 | - [不可靠的时钟](ch08.md#不可靠的时钟)
52 | - [知识、真相和谎言](ch08.md#知识、真相和谎言)
53 | - [第九章:一致性和共识协议](ch09.md) [b 站 ([上](https://www.bilibili.com/video/BV1eK411o73Q) [中](https://www.bilibili.com/video/BV1Y14y1P7xG) [下](https://www.bilibili.com/video/BV1r3411S7J2)) Youtube ([上](https://www.youtube.com/watch?v=Hq2gWib5n_I&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=17) [中](https://www.youtube.com/watch?v=Nds2xpnyS8A&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=18) [下](https://youtu.be/5ZCXSDMcerg))]
54 | - [一致性保证](ch09.md#一致性保证)
55 | - [线性一致性](ch09.md#线性一致性)
56 | - [顺序保证](ch09.md#顺序保证)
57 | - [分布式事务和共识协议](ch09.md#分布式事务和共识协议)
58 |
59 | ### 第三部分:衍生数据
60 |
61 | - [第十章:批处理](ch10.md) [b 站 ([上](https://www.bilibili.com/video/BV1bG4y1U7pV) [中](https://www.bilibili.com/video/BV1fY4y1C7Rm) [下](https://www.bilibili.com/video/BV1pp4y177pG) [终](https://www.bilibili.com/video/BV1m94y1H74M)) Youtube ([上](https://www.youtube.com/watch?v=hOciQEiABN8&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=20) [中](https://www.youtube.com/watch?v=kszxdmOdf2g&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=21) [下](https://www.youtube.com/watch?v=q5r1TD_8oAs&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=22) [终](https://www.youtube.com/watch?v=vJlHRc1QRts&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=23))]
62 | - [使用 Unix 工具进行批处理](ch10.md#使用Unix工具进行批处理)
63 | - [MapReduce 和分布式文件系统](ch10.md#MapReduce和分布式文件系统)
64 | - [MapReduce 之外](ch10.md#MapReduce之外)
65 | - [第十一章:流处理](ch11.md) [b站([一](https://www.bilibili.com/video/BV13e41117wW) [二](https://www.bilibili.com/video/BV1Ge411n73W) [三](https://www.bilibili.com/video/BV1Ai4y1p7YL) [四](https://www.bilibili.com/video/BV1dv421y7wo) [五](https://www.bilibili.com/video/BV1G1421f7Nt) [六](https://www.bilibili.com/video/BV1FZ42117mL))YouTube([一](https://www.youtube.com/watch?v=nO0-JzStG6c&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=24) [二](https://www.youtube.com/watch?v=GrT6pMEWwNE&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=25) [三](https://www.youtube.com/watch?v=U2220JtjMK0&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=26) [四](https://www.youtube.com/watch?v=JwL2yEF66Ok&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=27) [五](https://www.youtube.com/watch?v=WZmm7X-ewJE&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=28) [六](https://www.youtube.com/watch?v=YH18Dx-Z2x0&list=PLSISRu2b2N54LGT3Pyef70ae8m0tAAo6g&index=29))]
66 | - [事件流的传输](ch11.md#事件流的传输)
67 | - [数据库和流](ch11.md#数据库和流)
68 | - [处理流](ch11.md#处理流)
69 | - [小结](ch11.md#小结)
70 |
71 | # DDIA 读书会
72 |
73 | DDIA 读书分享会,会逐章进行分享,结合我在工业界分布式存储和数据库的一些经验,补充一些细节。每两周左右分享一次,欢迎加入,Schedule 在[这里](https://distsys.cn/t/ddia)。我们有个对应的分布式&数据库讨论群,每次分享前会在群里通知。如想加入,可以加我的微信号:qtmuniao,简单自我介绍下,并注明:分布式系统群。
74 |
75 | 另外,我的公众号:“**木鸟杂记**”,有更多的分布式系统、存储和数据库相关的文章,欢迎关注。关注公众号后,回复“**资料**”可以获取我多年来总结的一份分布式数据库学习资料。回复“**优惠券**”可以获取我的大规模数据系统付费专栏《[系统日知录](https://xiaobot.net/p/system-thinking)》的八折优惠券:
76 | 
77 |
78 | # 关于我
79 |
80 | 我是青藤木鸟,一个喜欢摄影的分布系统程序员,现主要做数据库方向。可以通过[领英](https://www.linkedin.com/in/qtmuniao/),[b 站](https://space.bilibili.com/30933812),[知乎](https://www.zhihu.com/people/qtmuniao) , 微信号 (qtmuniao) 找到我。更多分布式系统文章欢迎关注我的公众号——[木鸟杂记](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=Mzg5NTcxNzY2OQ==&action=getalbum&album_id=2164896217070206977&scene=126&devicetype=iOS15.4&version=18001d33&lang=zh_CN&nettype=WIFI&ascene=59&session_us=gh_80636260f9f9&fontScale=106&wx_header=3)。同名博客:木鸟杂记 [https://www.qtmuniao.com](https://www.qtmuniao.com)
81 |
82 | 对于 Martin Kleppmann 博士的[DDIA](https://dataintensive.net/),每章大概我自己要读两遍英文版,然后自己翻译一遍,再理顺一遍词句,再找相关资料补充一些背景,最后再讲一遍,因此每章最少都得花十几个小时甚至几十个小时(比如事务那一章)。但即便如此,一个人精力也有限,难免疏漏,欢迎提 issue 和 PR。创作不易,如果感觉感觉对你有帮助的话,不妨订阅下我的分布式系统、存储和数据库的技术专栏《[系统日知录](https://xiaobot.net/p/system-thinking)》 。
83 |
84 | 最后,如果你想对某些章节讨论的话,欢迎来我搭的一个专门的分布式系统论坛:https://distsys.cn/t/ddia 。感谢阅读,感谢关注。
85 |
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-slate
2 | title: DDIA 逐章精读
3 | description: Designing Data-Intensive Applications, 设计数据密集型应用,结合我的一些工程实践,用中文对书籍内容进行重新整理,并补充一些细节,逐章进行分享(视频在 B 站和 YouTube ),希望能够对大家有所帮助.
4 | google_analytics: UA-101943025-1
5 |
--------------------------------------------------------------------------------
/_sidebar.md:
--------------------------------------------------------------------------------
1 | - [序](preface.md)
2 | - 第一部分:数据系统基础
3 | * [第一章:可靠、可扩展、可维护](ch01.md)
4 | * [第二章:数据模型和查询语言](ch02.md)
5 | * [第三章:存储与查询](ch03.md)
6 | * [第四章:编码和演进](ch04.md)
7 | - 第二部分:分布式数据
8 | * [第五章:冗余](ch05.md)
9 | * [第六章:分区](ch06.md)
10 | * [第七章:事务](ch07.md)
11 | * [第八章:分布式系统中的麻烦事](ch08.md)
12 | * [第九章:一致性和共识协议](ch09.md)
13 | - 第三部分:衍生数据
14 | * [第十章:批处理](ch10.md)
15 | * [第十一章:流处理](ch11.md)
--------------------------------------------------------------------------------
/ch01.md:
--------------------------------------------------------------------------------
1 | # DDIA 逐章精读(一):可靠、可扩展、可维护
2 |
3 | # 本书为什么以数据系统为主题
4 |
5 | **数据系统**(data system)是一种模糊的统称。在信息社会中,一切皆可信息化,或者,某种程度上来说——数字化。这些数据的采集、存储和使用,是构成信息社会的基础。我们常见的绝大部分应用背后都有一套数据系统支撑,比如微信、京东、微博等等。
6 |
7 | 
8 |
9 | 因此,作为 IT 从业人员,有必要系统性的了解一下现代的、分布式的数据系统。学习本书,能够学习到数据系统的背后的原理、了解其常见的实践、进而将其应用到我们工作的系统设计中。
10 |
11 | ## 常见的数据系统有哪些
12 |
13 | - 存储数据,以便之后再次使用——**数据库**
14 | - 记住一些非常“重”的操作结果,方便之后加快读取速度——**缓存**
15 | - 允许用户以各种关键字搜索、以各种条件过滤数据——**搜索引擎**
16 | - 源源不断的产生数据、并发送给其他进程进行处理——**流式处理**
17 | - 定期处理累积的大量数据——**批处理**
18 | - 进行消息的传送与分发——**消息队列**
19 |
20 | 这些概念如此耳熟能详以至于我们在设计系统时拿来就用,而不用去想其实现细节,更不用从头进行实现。当然,这也侧面说明这些概念抽象的多么成功。
21 |
22 | ## 数据系统的日益复杂化
23 |
24 | 但这些年来,随着应用需求的进一步复杂化,出现了很多新型的数据采集、存储和处理系统,它们不拘泥于单一的功能,也难以生硬的归到某个类别。随便举几个例子:
25 |
26 | 1. **Kafka**:可以作为存储持久化一段时间日志数据、可以作为消息队列对数据进行分发、可以作为流式处理组件对数据反复蒸馏等等。
27 | 2. **Spark**:可以对数据进行批处理、也可以化小批为流,对数据进行流式处理。
28 | 3. **Redis**:可以作为缓存加速对数据库的访问、也可以作为事件中心对消息的发布订阅。
29 |
30 | 我们面临一个新的场景,以某种组合使用这些组件时,在某种程度上,便是创立了一个新的数据系统。书中给了一个常见的对用户数据进行采集、存储、查询、旁路等操作的数据系统示例。从其示意图中可以看到各种 Web Services 的影子。
31 |
32 | 
33 |
34 | 但就这么一个小系统,在设计时,就可以有很多取舍:
35 |
36 | 1. 使用何种缓存策略?是旁路还是写穿透?
37 | 2. 部分组件机器出现问题时,是保证可用性还是保证一致性?
38 | 3. 当机器一时难以恢复,如何保证数据的正确性和完整性?
39 | 4. 当负载增加时,是增加机器还是提升单机性能?
40 | 5. 设计对外的 API 时,是力求简洁还是追求强大?
41 |
42 | 因此,有必要从根本上思考下如何评价一个好数据系统,如何构建一个好的数据系统,有哪些可以遵循的设计模式?有哪些通常需要考虑的方面?
43 |
44 | 书中用了三个词来回答:**可靠性(Reliability)、可扩展性(Scalability)、可维护性(Maintainability)**
45 |
46 | # 可靠性
47 |
48 | 如何衡量可靠性?
49 |
50 | - **功能上**
51 | 1. 正常情况下,应用行为满足 API 给出的行为
52 | 2. 在用户误输入/误操作时,能够正常处理
53 | - **性能上**
54 | 在给定硬件和数据量下,能够满足承诺的性能指标。
55 | - **安全上**
56 | 能够阻止未授权、恶意破坏。
57 |
58 | 可用性也是可靠性的一个侧面,云服务通常以多少个 9 来衡量可用性。
59 |
60 | ---
61 |
62 | 两个易混淆的概念:**Fault(系统出现问题)** 和 **Failure(系统不能提供服务)**
63 |
64 | 不能进行 Fault-tolerance 的系统,积累的 fault 多了,就很容易 failure。
65 |
66 | 如何预防?混沌测试:如 Netflix 的 [chaosmonkey](https://netflix.github.io/chaosmonkey/)。
67 |
68 | ## 硬件故障
69 |
70 | 在一个大型数据中心中,这是常态:
71 |
72 | 1. 网络抖动、不通
73 | 2. 硬盘老化坏道
74 | 3. 内存故障
75 | 4. 机器过热导致 CPU 出问题
76 | 5. 机房断电
77 |
78 | 数据系统中常见的需要考虑的硬件指标:
79 |
80 | - **MTTF mean time to failure**
81 | 单块盘 平均故障时间 5 ~10 年,如果你有 1w+ 硬盘,则均匀期望下,每天都有坏盘出现。当然事实是硬盘会一波一波坏。
82 |
83 | 解决办法,增加冗余度:机房多路供电,双网络等等。
84 |
85 | 对于数据:
86 |
87 | * **单机**:可以做 RAID 冗余。如:EC 编码。
88 | * **多机**:多副本 或 EC 编码。
89 |
90 | ## 软件错误
91 |
92 | 相比硬件故障的随机性,软件错误的相关性更高:
93 |
94 | 1. 不能处理特定输入,导致系统崩溃。
95 | 2. 失控进程(如循环未释放资源)耗尽 CPU、内存、网络资源。
96 | 3. 系统依赖组件变慢甚至无响应。
97 | 4. 级联故障。
98 |
99 | 在设计软件时,我们通常有一些**环境假设**,和一些**隐性约束**。随着时间的推移、系统的持续运行,如果这些假设不能够继续被满足;如果这些约束被后面维护者增加功能时所破坏;都有可能让一开始正常运行的系统,突然崩溃。
100 |
101 | ## 人为问题
102 |
103 | 系统中最不稳定的是人,因此要在设计层面尽可能消除人对系统影响。依据软件的生命周期,分几个阶段来考虑:
104 |
105 | - **设计编码**
106 | 1. 尽可能消除所有不必要的假设,提供合理的抽象,仔细设计 API
107 | 2. 进程间进行隔离,对尤其容易出错的模块使用沙箱机制
108 | 3. 对服务依赖进行熔断设计
109 | - **测试阶段**
110 | 1. 尽可能引入第三方成员测试,尽量将测试平台自动化
111 | 2. 单元测试、集成测试、e2e 测试、混沌测试
112 | - **运行阶段**
113 | 1. 详细的仪表盘
114 | 2. 持续自检
115 | 3. 报警机制
116 | 4. 问题预案
117 | - **针对组织**
118 | 1. 科学的培训和管理
119 |
120 | ## 可靠性有多重要?
121 |
122 | 事关用户数据安全,事关企业声誉,企业存活和做大的基石。
123 |
124 | # 可扩展性
125 |
126 | 可扩展性,即系统应对负载增长的能力。它很重要,但在实践中又很难做好,因为存在一个基本矛盾:**只有能存活下来的产品才有资格谈扩展,而过早为扩展设计往往活不下去**。
127 |
128 | 但仍是可以了解一些基本的概念,来应对**可能会**暴增的负载。
129 |
130 | ## 衡量负载
131 |
132 | 应对负载之前,要先找到合适的方法来衡量负载,如**负载参数(load parameters)**:
133 |
134 | - 应用日活月活
135 | - 每秒向 Web 服务器发出的请求
136 | - 数据库中的读写比率
137 | - 聊天室中同时活跃的用户数量
138 |
139 | 书中以 Twitter 2012 年 11 月披露的信息为例进行了说明:
140 |
141 | 1. 识别主营业务:发布推文、首页 Feed 流。
142 | 2. 确定其请求量级:发布推文(平均 4.6k 请求/秒,峰值超过 12k 请求/秒),查看其他人推文(300k 请求/秒)
143 |
144 | 
145 |
146 | 单就这个数据量级来说,无论怎么设计都问题不大。但 Twitter 需要根据用户之间的关注与被关注关系来对数据进行多次处理。常见的有推拉两种方式:
147 |
148 | 1. **拉**。每个人查看其首页 Feed 流时,从数据库现**拉取**所有关注用户推文,合并后呈现。
149 | 2. **推**。为每个用户保存一个 Feed 流视图,当用户发推文时,将其插入所有关注者 Feed 流视图中。
150 |
151 | 
152 |
153 | 前者是 Lazy 的,用户只有查看时才会去拉取,不会有无效计算和请求,但每次需要现算,呈现速度较慢。而且流量一大也扛不住。
154 |
155 | 后者事先算出视图,而不管用户看不看,呈现速度较快,但会引入很多无效请求。
156 |
157 | 最终,使用的是一种推拉结合的方式,这也是外国一道经典的系统设计考题。
158 |
159 | ## 描述性能
160 |
161 | 注意和系统负载区分,系统负载是从用户视角来审视系统,是一种**客观指标**。而系统性能则是描述的系统的一种**实际能力**。比如:
162 |
163 | 1. **吞吐量(throughput)**:每秒可以处理的单位数据量,通常记为 QPS。
164 | 2. **响应时间(response time)**:从用户侧观察到的发出请求到收到回复的时间。
165 | 3. **延迟(latency)**:日常中,延迟经常和响应时间混用指代响应时间;但严格来说,延迟只是指请求过程中排队等休眠时间,虽然其在响应时间中一般占大头;但只有我们把请求真正处理耗时认为是瞬时,延迟才能等同于响应时间。
166 |
167 | 响应时间通常以百分位点来衡量,比如 p95,p99 和 p999,它们意味着 95%,99%或 99.9% 的请求都能在该阈值内完成。在实际中,通常使用滑动窗口滚动计算最近一段时间的响应时间分布,并通常以折线图或者柱状图进行呈现。
168 |
169 | ## 应对负载
170 |
171 | 在有了描述和定义负载、性能的手段之后,终于来到正题,如何应对负载的不断增长,即使系统具有可扩展性。
172 |
173 | 1. **纵向扩展(scaling up)或 垂直扩展(vertical scaling)**:换具有更强大性能的机器。e.g. 大型机机器学习训练。
174 | 2. **横向扩展(scaling out)或 水平扩展(horizontal scaling)**:“并联”很多廉价机,分摊负载。e.g. 马斯克造火箭。
175 |
176 | 负载扩展的两种方式:
177 |
178 | - **自动**
179 | 如果负载不好预测且多变,则自动较好。坏处在于不易跟踪负载,容易抖动,造成资源浪费。
180 | - **手动**
181 | 如果负载容易预测且不长变化,最好手动。设计简单,且不容易出错。
182 |
183 | 针对不同应用场景:
184 |
185 | 首先,如果规模很小,尽量还是用性能好一点的机器,可以省去很多麻烦。
186 |
187 | 其次,可以上云,利用云的可扩展性。甚至如 Snowflake 等基础服务提供商也是 All In 云原生。
188 |
189 | 最后,实在不行再考虑自行设计可扩展的分布式架构。
190 |
191 | 两种服务类型:
192 |
193 | - **无状态服务**
194 | 比较简单,多台机器,外层罩一个 gateway 就行。
195 | - **有状态服务**
196 | 根据需求场景,如读写负载、存储量级、数据复杂度、响应时间、访问模式,来进行取舍,设计合乎需求的架构。
197 |
198 | **不可能啥都要,没有万金油架构**!但同时:万变不离其宗,组成不同架构的原子设计模式是有限的,这也是本书稍后要论述的重点。
199 |
200 | # 可维护性
201 |
202 | 从软件的整个生命周期来看,维护阶段绝对占大头。
203 |
204 | 但大部分人都喜欢挖坑,不喜欢填坑。因此有必要,在刚开就把坑开的足够好。有三个原则:
205 |
206 | - **_可维护性(Operability)_**
207 | 便于运维团队无痛接手。
208 | - **_简洁性(Simplicity)_**
209 | 便于新手开发平滑上手:这需要一个合理的抽象,并尽量消除各种复杂度。如,层次化抽象。
210 | - **_可演化性(Evolvability)_**
211 | 便于后面需求快速适配:避免耦合过紧,将代码绑定到某种实现上。也称为**可扩展性(extensibility)**,**可修改性(modifiability)** 或**可塑性(plasticity)**。
212 |
213 | ## **可维护性(Operability):人生苦短,关爱运维**
214 |
215 | 有效的运维绝对是个高技术活:
216 |
217 | 1. 紧盯系统状态,出问题时快速恢复。
218 | 2. 恢复后,复盘问题,定位原因。
219 | 3. 定期对平台、库、组件进行更新升级。
220 | 4. 了解组件间相互关系,避免级联故障。
221 | 5. 建立自动化配置管理、服务管理、更新升级机制。
222 | 6. 执行复杂维护任务,如将存储系统从一个数据中心搬到另外一个数据中心。
223 | 7. 配置变更时,保证系统安全性。
224 |
225 | 系统具有良好的可维护性,意味着将**可定义**的维护过程编写**文档和工具**以自动化,从而解放出人力关注更高价值事情:
226 |
227 | 1. 友好的文档和一致的运维规范。
228 | 2. 细致的监控仪表盘、自检和报警。
229 | 3. 通用的缺省配置。
230 | 4. 出问题时的自愈机制,无法自愈时允许管理员手动介入。
231 | 5. 将维护过程尽可能的自动化。
232 | 6. 避免单点依赖,无论是机器还是人。
233 |
234 | ## **简洁性(Simplicity):复杂度管理**
235 |
236 | 
237 |
238 | 推荐一本书:[A Philosophy of Software Design](https://book.douban.com/subject/30218046/) ,讲述在软件设计中如何定义、识别和降低复杂度。
239 |
240 | 复杂度表现:
241 |
242 | 1. 状态空间的膨胀。
243 | 2. 组件间的强耦合。
244 | 3. 不一致的术语和[命名](https://www.qtmuniao.com/2021/12/12/how-to-write-code-scrutinize-names/)。
245 | 4. 为了提升性能的 hack。
246 | 5. 随处可见的补丁(workaround)。
247 |
248 | 需求很简单,但不妨碍你实现的很复杂 😉:过多的引入了**额外复杂度**(_accidental_ complexity
249 | )——非问题本身决定的,而由实现所引入的复杂度。
250 |
251 | 通常是问题理解的不够本质,写出了“**流水账**”(没有任何**抽象,abstraction**)式的代码。
252 |
253 | 如果你为一个问题找到了合适的抽象,那么问题就解决了一半,如:
254 |
255 | 1. 高级语言隐藏了机器码、CPU 和系统调用细节。
256 | 2. SQL 隐藏了存储体系、索引结构、查询优化实现细节。
257 |
258 | 如何找到合适的抽象?
259 |
260 | 1. 从计算机领域常见的抽象中找。
261 | 2. 从日常生活中常接触的概念找。
262 |
263 | 总之,一个合适的抽象,要么是**符合直觉**的;要么是和你的读者**共享上下文**的。
264 |
265 | 本书之后也会给出很多分布式系统中常用的抽象。
266 |
267 | ## 可演化性:降低改变门槛
268 |
269 | 系统需求没有变化,说明这个行业死了。
270 |
271 | 否则,需求一定是不断在变,引起变化的原因多种多样:
272 |
273 | 1. 对问题阈了解更全面
274 | 2. 出现了之前未考虑到的用例
275 | 3. 商业策略的改变
276 | 4. 客户爸爸要求新功能
277 | 5. 依赖平台的更迭
278 | 6. 合规性要求
279 | 7. 体量的改变
280 |
281 | 应对之道:
282 |
283 | - 项目管理上
284 | 敏捷开发
285 | - 系统设计上
286 | 依赖前两点。合理抽象,合理封装,对修改关闭,对扩展开放。
287 |
--------------------------------------------------------------------------------
/ch02.md:
--------------------------------------------------------------------------------
1 | # DDIA 逐章精读(二):数据模型和查询语言
2 |
3 | # 概要
4 |
5 | 本节围绕两个主要概念来展开。
6 |
7 | 如何分析一个**数据模型**:
8 |
9 | 1. 基本考察点:数据基本元素,和元素之间的对应关系(一对多,多对多)
10 | 2. 利用几种常用模型来比较:(最为流行的)关系模型,(树状的)文档模型,(极大自由度的)图模型。
11 | 3. schema 模式:强 Schema(写时约束);弱 Schema(读时解析)
12 |
13 | 如何考量**查询语言**:
14 |
15 | 1. 如何与数据模型关联、匹配
16 | 2. 声明式(declarative)和命令式(imperative)
17 |
18 | ## 数据模型
19 |
20 | > A **data model** is an [abstract model](https://en.wikipedia.org/wiki/Abstract_model) that organizes elements of [data](https://en.wikipedia.org/wiki/Data) and standardizes how they relate to one another and to the properties of real-world entities.
21 | > —[https://en.wikipedia.org/wiki/Data_model](https://en.wikipedia.org/wiki/Data_model)
22 |
23 | **数据模型**:如何组织数据,如何标准化关系,如何关联现实。
24 |
25 | 它既决定了我们构建软件的方式(**实现**),也左右了我们看待问题的角度(**认知**)。
26 |
27 | 作者开篇以计算机的不同抽象层次来让大家对**泛化的**数据模型有个整体观感。
28 |
29 | 大多数应用都是通过不同的数据模型层级累进构建的。
30 |
31 | 
32 |
33 | 每层模型核心问题:如何用下一层的接口来对本层进行建模?
34 |
35 | 1. 作为**应用开发者,** 你将现实中的具体问题抽象为一组对象、**数据结构(data structure)** 以及作用于其上的 API。
36 | 2. 作为**数据库管理员(DBA)**,为了持久化上述数据结构,你需要将他们表达为通用的**数据模型(data model)**,如文档数据库中的 XML/JSON、关系数据库中的表、图数据库中的图。
37 | 3. 作为**数据库系统开发者**,你需要将上述数据模型组织为内存中、硬盘中或者网络中的**字节(Bytes)** 流,并提供多种操作数据集合的方法。
38 | 4. 作为**硬件工程师**,你需要将字节流表示为二极管的电位(内存)、磁场中的磁极(磁盘)、光纤中的光信号(网络)。
39 |
40 | > 在每一层,通过对外暴露简洁的**数据模型**,我们**隔离**和**分解**了现实世界的**复杂度**。
41 |
42 | 这也反过来说明了,好的数据模型需有两个特点:
43 |
44 | 1. 简洁直观
45 | 2. 具有组合性
46 |
47 | 第二章首先探讨了关系模型、文档模型及其对比,其次是相关查询语言,最后探讨了图模型。
48 |
49 | # 关系模型与文档模型
50 |
51 | ## 关系模型
52 |
53 | 关系模型无疑是当今最流行的数据库模型。
54 |
55 | 关系模型是 [埃德加·科德(](https://zh.wikipedia.org/wiki/%E5%9F%83%E5%BE%B7%E5%8A%A0%C2%B7%E7%A7%91%E5%BE%B7)[E. F. Codd](https://en.wikipedia.org/wiki/E._F._Codd))于 1969 年首先提出,并用“[科德十二定律](https://zh.wikipedia.org/wiki/%E7%A7%91%E5%BE%B7%E5%8D%81%E4%BA%8C%E5%AE%9A%E5%BE%8B)”来解释。但是商业落地的数据库基本没有能完全遵循的,因此关系模型后来通指这一类数据库。特点如下:
56 |
57 | 1. 将数据以**关系**呈现给用户(比如:一组包含行列的二维表)。
58 | 2. 提供操作数据集合的**关系算子**。
59 |
60 | **常见分类**
61 |
62 | 1. 事务型(TP):银行交易、火车票
63 | 2. 分析型(AP):数据报表、监控表盘
64 | 3. 混合型(HTAP):
65 |
66 | 关系模型诞生很多年后,虽有不时有各种挑战者(比如上世纪七八十年代的**网状模型** network model 和**层次模型** hierarchical model),但始终仍未有根本的能撼动其地位的新模型。
67 |
68 | 直到近十年来,随着移动互联网的普及,数据爆炸性增长,各种处理需求越来越精细化,催生了数据模型的百花齐放。
69 |
70 | ## NoSQL 的诞生
71 |
72 | NoSQL(最初表示 Non-SQL,后来有人转解为 Not only SQL),是对不同于传统的关系数据库的数据库管理系统的统称。根据 [DB-Engines 排名](https://db-engines.com/en/ranking),现在最受欢迎的 NoSQL 前几名为:MongoDB,Redis,ElasticSearch,Cassandra。
73 |
74 | 其催动因素有:
75 |
76 | 1. 处理更大数据集:更强伸缩性、更高吞吐量
77 | 2. 开源免费的兴起:冲击了原来把握在厂商的标准
78 | 3. 特化的查询操作:关系数据库难以支持的,比如图中的多跳分析
79 | 4. 表达能力更强:关系模型约束太严,限制太多
80 |
81 | ## 面向对象和关系模型的不匹配
82 |
83 | 核心冲突在于面向对象的**嵌套性**和关系模型的**平铺性**(?我随便造的)。
84 |
85 | 当然有 ORM 框架可以帮我们搞定这些事情,但仍是不太方便。
86 |
87 | 
88 |
89 | 换另一个角度来说,关系模型很难直观的表示**一对多**的关系。比如简历上,一个人可能有多段教育经历和多段工作经历。
90 |
91 | **文档模型**:使用 Json 和 XML 的天然嵌套。
92 |
93 | **关系模型**:使用 SQL 模型就得将职位、教育单拎一张表,然后在用户表中使用外键关联。
94 |
95 | 在简历的例子中,文档模型还有几个优势:
96 |
97 | 1. **模式灵活**:可以动态增删字段,如工作经历。
98 | 2. **更好的局部性**:一个人的所有属性被集中访问的同时,也被集中存储。
99 | 3. **结构表达语义**:简历与联系信息、教育经历、职业信息等隐含一对多的树状关系可以被 JSON 的树状结构明确表达出来。
100 |
101 | ## 多对一和多对多
102 |
103 | 是一个对比各种数据模型的切入角度。
104 |
105 | region 在存储时,为什么不直接存储纯字符串:“Greater Seattle Area”,而是先存为 region_id → region name,其他地方都引用 region_id?
106 |
107 | 1. **统一样式**:所有用到相同概念的地方都有相同的拼写和样式
108 | 2. **避免歧义**:可能有同名地区
109 | 3. **易于修改**:如果一个地区改名了,我们不用去逐一修改所有引用他的地方
110 | 4. **本地化支持**:如果翻译成其他语言,可以只翻译名字表。
111 | 5. **更好搜索**:列表可以关联地区,进行树形组织
112 |
113 | 类似的概念还有:面向抽象编程,而非面向细节。
114 |
115 | 关于用 ID 还是文本,作者提到了一点:ID 对人类是**无意义**的,无意义的意味着不会随着现实世界的将来的改变而改动。
116 |
117 | 这在关系数据库表设计时需要考虑,即如何控制**冗余(duplication)**。会有几种**范式(normalization)** 来消除冗余。
118 |
119 | 文档型数据库很擅长处理一对多的树形关系,却不擅长处理多对多的图形关系。如果其不支持 Join,则处理多对多关系的复杂度就从数据库侧移动到了应用侧。
120 |
121 | 如,多个用户可能在同一个组织工作过。如果我们想找出在同一个学校和组织工作过的人,如果数据库不支持 Join,则需要在应用侧进行循环遍历来 Join。
122 |
123 | 
124 |
125 | 文档 vs 关系
126 |
127 | 1. 对于一对多关系,文档型数据库将嵌套数据放在父节点中,而非单拎出来放另外一张表。
128 | 2. 对于多对一和多对多关系,本质上,两者都是使用外键(文档引用)进行索引。查询时需要进行 join 或者动态跟随。
129 |
130 | ## 文档模型是否在重复历史?
131 |
132 | ### 层次模型 **(hierarchical model)**
133 |
134 | 20 世纪 70 年代,IBM 的信息管理系统 IMS。
135 |
136 | > A **hierarchical database model** is a [data model](https://en.wikipedia.org/wiki/Data_model) in which the data are organized into a [tree](https://en.wikipedia.org/wiki/Tree_data_structure)-like structure. The data are stored as **records** which are connected to one another through **links.** A record is a collection of fields, with each field containing only one value. The **type** of a record defines which fields the record contains. — wikipedia
137 |
138 | 几个要点:
139 |
140 | 1. 树形组织,每个子节点只允许有一个父节点
141 | 2. 节点存储数据,节点有类型
142 | 3. 节点间使用类似指针方式连接
143 |
144 | 可以看出,它跟文档模型很像,也因此很难解决多对多的关系,并且不支持 Join。
145 |
146 | 为了解决层次模型的局限,人们提出了各种解决方案,最突出的是:
147 |
148 | 1. 关系模型
149 | 2. 网状模型
150 |
151 | ### 网状模型(network model)
152 |
153 | network model 是 hierarchical model 的一种扩展:允许一个节点有多个父节点。它被数据系统语言会议(CODASYL)的委员会进行了标准化,因此也被称为 CODASYL 模型。
154 |
155 | 多对一和多对多都可以由路径来表示。访问记录的唯一方式是顺着元素和链接组成的链路进行访问,这个链路叫**访问路径** (access path)。难度犹如在 n-维空间中进行导航。
156 |
157 | 内存有限,因此需要严格控制遍历路径。并且需要事先知道数据库的拓扑结构,这就意味着得针对不同应用写大量的专用代码。
158 |
159 | ### 关系模型
160 |
161 | 在关系模型中,数据被组织成**元组(tuples)**,进而集合成**关系(relations)**;在 SQL 中分别对应行(rows)和表(tables)。
162 |
163 | - 不知道大家好奇过没,明明看起来更像表模型,为什叫**关系模型**?
164 | 表只是一种实现。
165 | 关系(relation)的说法来自集合论,指的是几个集合的笛卡尔积的子集。
166 | R ⊆ (D1×D2×D3 ··· ×Dn)
167 | (关系用符号 R 表示,属性用符号 Ai 表示,属性的定义域用符号 Di 表示)
168 |
169 | 其主要目的和贡献在于提供了一种**声明式**的描述数据和构建查询的方法。
170 |
171 | 即,相比网络模型,关系模型的查询语句和执行路径相解耦,**查询优化器**(Query Optimizer 自动决定执行顺序、要使用的索引),即将逻辑和实现解耦。
172 |
173 | 举个例子:如果想使用新的方式对你的数据集进行查询,你只需要在新的字段上建立一个索引。那么在查询时,你并不需要改变的你用户代码,查询优化器便会动态的选择可用索引。
174 |
175 | ## 文档型 vs 关系型
176 |
177 | 根据数据类型来选择数据模型
178 |
179 | | | 文档型 | 关系型 |
180 | | ---------- | -------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
181 | | 对应关系 | 数据有天然的一对多、树形嵌套关系,如简历。 | 通过外键 + Join 可以处理 多对一,多对多关系 |
182 | | 代码简化 | 数据具有文档结构,则文档模型天然合适,用关系模型会使得建模繁琐、访问复杂。但不宜嵌套太深,因为只能手动指定访问路径,或者范围遍历 | 主键,索引,条件过滤 |
183 | | Join 支持 | 对 Join 支持的不太好 | 支持的还可以,但 Join 的实现会有很多难点 |
184 | | 模式灵活性 | 弱 schema,支持动态增加字段 | 强 schema,修改 schema 代价很大 |
185 | | 访问局部性 | 1. 一次性访问整个文档,较优
2. 只访问文档一部分,较差 | 分散在多个表中 |
186 |
187 | 对于高度关联的数据集,使用文档型表达比较奇怪,使用关系型可以接受,使用图模型最自然。
188 |
189 | ### 文档模型中 Schema 的灵活性
190 |
191 | 说文档型数据库是 schemaless 不太准确,更贴切的应该是 **schema-on-read。**
192 |
193 | | 数据模型 | | 编程语言 | | 性能 & 空间 |
194 | | --------------- | -------------------------------------- | -------- | ------------------ | ---------------------------------------------------------------------- |
195 | | schema-on-read | 写入时不校验,而在读取时进行动态解析。 | 弱类型 | 动态,在运行时解析 | 读取时动态解析,性能较差。写入时无法确定类型,无法对齐,空间利用率较差。 |
196 | | schema-on-write | 写入时校验,数据对齐到 schema | 强类型 | 静态,编译时确定 | 性能和空间使用都较优。 |
197 |
198 | 文档型数据库使用场景特点:
199 |
200 | 1. 有多种类型的数据,但每个放一张表又不合适。
201 | 2. 数据类型和结构由外部决定,你没办法控制数据的变化。
202 |
203 | ### 查询时的数据局部性
204 |
205 | 如果你同时需要文档中所有内容,把文档顺序存储,访问会效率比较高。
206 |
207 | 但如果你只需要访问文档中的某些字段,则文档仍需要将文档全部加载出。
208 |
209 | 但运用这种局部性不局限于文档型数据库。不同的数据库,会针对不同场景,调整数据物理分布以适应常用访问模式的局部性。
210 |
211 | - Spanner 中允许表被声明为嵌入到父表中——常用关联内嵌(获得类似文档模型的结构)
212 | - HBase 和 Cassandra 使用列族来聚集数据——分析型
213 | - 图数据库中,将点和出边存在一个机器上——图遍历
214 |
215 | ### 关系型和文档型的融合
216 |
217 | - MySQL 和 PostgreSQL 开始支持 JSON
218 | 原生支持 JSON 可以理解为,MySQL 可以理解 JSON 类型。如 Date 这种复杂格式一样,可以让某个字段为 JSON 类型、可以修改 Join 字段的某个属性、可以在 Json 字段中某个属性建立索引。
219 | - RethinkDB 在查询中支持 relational-link Joins
220 |
221 | 科德(Codd):**nonsimple domains**,记录中的值除了简单类型(数字、字符串),还可以一个嵌套关系(表)。这很像 SQL 对 XML、JSON 的支持。
222 |
223 | # 数据查询语言
224 |
225 | 获取动物表中所有鲨鱼类动物。
226 |
227 | ```jsx
228 | function getSharks() {
229 | var sharks = [];
230 | for (var i = 0; i < animals.length; i++) {
231 | if (animals[i].family === 'Sharks') {
232 | sharks.push(animals[i]);
233 | }
234 | }
235 | return sharks;
236 | }
237 | ```
238 |
239 | ```sql
240 | SELECT * FROM animals WHERE family = 'Sharks';
241 | ```
242 |
243 | | | 声明式(declarative)语言 | 命令式(imperative)语言 |
244 | | -------- | --------------------------------------------------------------- | -------------------------------------------------------------------------- |
245 | | 概念 | 描述控制逻辑而非执行流程 | 描述命令的执行过程,用一系列语句来不断改变状态 |
246 | | 举例 | SQL,CSS,XSL | IMS,CODASYL,通用语言如 C,C++,JS |
247 | | 抽象程度 | 高 | 低 |
248 | | 解耦程度 | 与实现解耦。
可以持续优化查询引擎性能; | 与实现耦合较深。 |
249 | | 解析执行 | 词法分析 → 语法分析 → 语义分析
生成执行计划 → 执行计划优化 | 词法分析 → 语法分析 → 语义分析
中间代码生成 → 代码优化 → 目标代码生成 |
250 | | 多核并行 | 声明式更具多核潜力,给了更多运行时优化空间 | 命令式由于指定了代码执行顺序,编译时优化空间较小。 |
251 |
252 | > Q:相对声明式语言,命令式语言有什么优点?
253 | >
254 | > 1. 当描述的目标变得复杂时,声明式表达能力不够。
255 | > 2. 实现命令式的语言往往不会和声明式那么泾渭分明,通过合理抽象,通过一些编程范式(函数式),可以让代码兼顾表达力和清晰性。
256 |
257 | ## 数据库以外:Web 中的声明式
258 |
259 | **需求**:选中页背景变蓝。
260 |
261 | ```html
262 |
263 | -
264 |
Sharks
265 |
266 | - Great White Shark
267 | - Tiger Shark
268 | - Hammerhead Shark
269 |
270 |
271 | -
272 |
Whales
273 |
274 | - Blue Whale
275 | - Humpback Whale
276 | - Fin Whale
277 |
278 |
279 |
280 | ```
281 |
282 | 如果使用 CSS,则只需(CSS selector):
283 |
284 | ```css
285 | li.selected > p {
286 | background-color: blue;
287 | }
288 | ```
289 |
290 | 如果使用 XSL,则只需(XPath selector):
291 |
292 | ```css
293 |
294 |
295 |
296 |
297 |
298 | ```
299 |
300 | 但如果使用 JavaScript(而不借助上述 selector 库):
301 |
302 | ```jsx
303 | var liElements = document.getElementsByTagName('li');
304 | for (var i = 0; i < liElements.length; i++) {
305 | if (liElements[i].className === 'selected') {
306 | var children = liElements[i].childNodes;
307 | for (var j = 0; j < children.length; j++) {
308 | var child = children[j];
309 | if (child.nodeType === Node.ELEMENT_NODE && child.tagName === 'P') {
310 | child.setAttribute('style', 'background-color: blue');
311 | }
312 | }
313 | }
314 | }
315 | ```
316 |
317 | ## MapReduce 查询
318 |
319 | **Google 的 MapReduce 模型**
320 |
321 | 1. 借鉴自函数式编程。
322 | 2. 一种相当简单的编程模型,或者说原子的抽象,现在不太够用。
323 | 3. 但在大数据处理工具匮乏的蛮荒时代(03 年以前),谷歌提出的这套框架相当有开创性。
324 |
325 | 
326 |
327 | **MongoDB 的 MapReduce 模型**
328 |
329 | MongoDB 使用的 MapReduce 是一种介于
330 |
331 | 1. **声明式**:用户不必显式定义数据集的遍历方式、shuffle 过程等执行过程。
332 | 2. **命令式**:用户又需要定义针对单条数据的执行过程。
333 |
334 | 两者间的混合数据模型。
335 |
336 | **需求**:统计每月观察到鲨类鱼的次数。
337 |
338 | **查询语句**:
339 |
340 | **PostgresSQL**
341 |
342 | ```sql
343 | SELECT date_trunc('month', observation_timestamp) AS observation_month,
344 | sum(num_animals) AS total_animals
345 | FROM observations
346 | WHERE family = 'Sharks' GROUP BY observation_month;
347 | ```
348 |
349 | **MongoDB**
350 |
351 | ```jsx
352 | db.observations.mapReduce(
353 | function map() {
354 | // 2. 对所有符合条件 doc 执行 map
355 | var year = this.observationTimestamp.getFullYear();
356 | var month = this.observationTimestamp.getMonth() + 1;
357 | emit(year + '-' + month, this.numAnimals); // 3. 输出一个 kv pair
358 | },
359 | function reduce(key, values) {
360 | // 4. 按 key 聚集
361 | return Array.sum(values); // 5. 相同 key 加和
362 | },
363 | {
364 | query: { family: 'Sharks' }, // 1. 筛选
365 | out: 'monthlySharkReport', // 6. reduce 结果集
366 | }
367 | );
368 | ```
369 |
370 | 上述语句在执行时,经历了:筛选(filter) → 遍历并执行 map → 对输出按 key 聚集(shuffle)→ 对聚集的数据逐一 reduce → 输出结果集。
371 |
372 | MapReduce 一些特点:
373 |
374 | 1. **要求 Map 和 Reduce 是纯函数**。即无任何副作用,在任意地点、以任意次序执行任何多次,对相同的输入都能得到相同的输出。因此容易并发调度。
375 | 2. **非常底层、但表达力强大的编程模型**。可基于其实现 SQL 等高级查询语言,如 Hive。
376 |
377 | 但要注意:
378 |
379 | 1. 不是所有的分布式 SQL 都基于 MapReduce 实现。
380 | 2. 不是只有 MapReduce 才允许嵌入通用语言(如 js)模块。
381 | 3. MapReduce 是有一定**理解成本**的,需要非常熟悉其执行原理才能让两个函数紧密配合。
382 |
383 | MongoDB 2.2+ 进化版,_aggregation pipeline:_
384 |
385 | ```jsx
386 | db.observations.aggregate([
387 | { $match: { family: 'Sharks' } },
388 | {
389 | $group: {
390 | _id: {
391 | year: { $year: '$observationTimestamp' },
392 | month: { $month: '$observationTimestamp' },
393 | },
394 | totalAnimals: { $sum: '$numAnimals' },
395 | },
396 | },
397 | ]);
398 | ```
399 |
400 | # 图模型
401 |
402 | - 文档模型的适用场景?
403 | 你的建模场景中存在着大量**一对多**(one-to-many)的关系。
404 | - 图模型的适用场景?
405 | 你的建模场景中存在大量的**多对多**(many-to-many)的关系。
406 |
407 | ## 基本概念
408 |
409 | 图数据模型(属性图)的基本概念一般有三个:**点**,**边**和附着于两者之上的**属性**。
410 |
411 | 常见的可以用图建模的场景:
412 |
413 | | 例子 | 建模 | 应用 |
414 | | -------- | --------------------------- | ---------------------- |
415 | | 社交图谱 | 人是点,follow 关系是边 | 六度分隔,信息流推荐 |
416 | | 互联网 | 网页是点,链接关系是边 | PageRank |
417 | | 路网 | 交通枢纽是点,铁路/公路是边 | 路径规划,导航最短路径 |
418 | | 洗钱 | 账户是点,转账关系是边 | 判断是否有环 |
419 | | 知识图谱 | 概念是点,关联关系是边 | 启发式问答 |
420 |
421 | > 同构(homogeneous)数据和异构数据
422 | > 图模型中的点变可以像关系中的表一样都具有相同类型;但是,一张图中的点和变也可以具有不同类型,能够容纳异构数据是图模型善于处理多对多关系的一大原因。
423 |
424 | 本节都会以下图为例,它表示了一对夫妇,来自美国爱达荷州的 Lucy 和来自法国 的 Alain:他们已婚,住在伦敦。
425 |
426 | 
427 |
428 | 有多种对图的建模方式:
429 |
430 | 1. **属性图(property graph)**:比较主流,如 Neo4j、Titan、InfiniteGraph
431 | 2. **三元组(triple-store)**:如 Datomic、AllegroGraph
432 |
433 | ## 属性图(PG,Property Graphs)
434 |
435 | | 点 (vertices, nodes, entities) | 边 (edges, relations, arcs) |
436 | | ------------------------------ | --------------------------- |
437 | | 全局唯一 ID | 全局唯一 ID |
438 | | 出边集合 | 起始点 |
439 | | 入边集合 | 终止点 |
440 | | 属性集(kv 对表示) | 属性集(kv 对表示) |
441 | | 表示点类型的 type? | 表示边类型的 label |
442 |
443 | > Q:有一个疑惑点,为什么书中对于 PG 点的定义中没有 Type?
444 | > 因为属性图具体实现时也可以分为强类型和弱类型,NebulaGraph 是强类型,好处在于效率高,但灵活性差;Neo4j 是弱类型。书中应该是用的弱类型,此时每个点都是一组属性集,不需要 type。
445 |
446 | 如果感觉不直观,可以使用我们熟悉的 SQL 语义来构建一个图模型,如下图。(Facebook TAO 论文中的单机存储引擎便是 MySQL)
447 |
448 | ```sql
449 | // 点表
450 | CREATE TABLE vertices (
451 | vertex_id integer PRIMARYKEY, properties json
452 | );
453 |
454 | // 边表
455 | CREATE TABLE edges (
456 | edge_id integer PRIMARY KEY,
457 | tail_vertex integer REFERENCES vertices (vertex_id),
458 | head_vertex integer REFERENCES vertices (vertex_id),
459 | label text,
460 | properties json
461 | );
462 |
463 | // 对点的反向索引,图遍历时用。给定点,找出点的所有入边和出边。
464 | CREATE INDEX edges_tails ON edges (tail_vertex);
465 | CREATE INDEX edges_heads ON edges (head_vertex);
466 | ```
467 |
468 | 图是一种很灵活的建模方式:
469 |
470 | 1. 任何两点间都可以插入边,没有任何模式限制。
471 | 2. 对于任何顶点都可以高效(思考:如何高效?)找到其入边和出边,从而进行图遍历。
472 | 3. 使用多种**标签**来标记不同类型边(关系)。
473 |
474 | 相对于关系型数据来说,**可以在同一个图中保存异构类型的数据和关系,给了图极大的表达能力!**
475 |
476 | 这种表达能力,根据图中的例子,包括:
477 |
478 | 1. 对同样的概念,可以用不同结构表示。如不同国家的行政划分。
479 | 2. 对同样的概念,可以用不同粒度表示。比如 Lucy 的现居住地和诞生地。
480 | 3. 可以很自然的进行演化。
481 |
482 | 将异构的数据容纳在一张图中,可以通过**图遍历**,轻松完成关系型数据库中需要**多次 Join** 的操作。
483 |
484 | ## Cypher 查询语言
485 |
486 | Cypher 是 Neo4j 创造的一种查询语言。
487 |
488 | Cypher 和 Neo 名字应该都是来自《黑客帝国》(The Matrix)。想想 Oracle。
489 |
490 | Cypher 的一大特点是可读性强,尤其在表达路径模式(Path Pattern)时。
491 |
492 | 结合前图,看一个 Cypher 插入语句的例子:
493 |
494 | ```sql
495 | CREATE
496 | (NAmerica:Location {name:'North America', type:'continent'}),
497 | (USA:Location {name:'United States', type:'country' }),
498 | (Idaho:Location {name:'Idaho', type:'state' }),
499 | (Lucy:Person {name:'Lucy' }),
500 | (Idaho) -[:WITHIN]-> (USA) -[:WITHIN]-> (NAmerica),
501 | (Lucy) -[:BORN_IN]-> (Idaho)
502 | ```
503 |
504 | 如果我们要进行一个这样的查询:找出所有从美国移居到欧洲的人名。
505 |
506 | 转化为图语言,即为:给定条件,BORN_IN 指向美国的地点,并且 LIVING_IN 指向欧洲的地点,找到所有符合上述条件的点,并且返回其名字属性。
507 |
508 | 用 Cypher 语句可表示为:
509 |
510 | ```sql
511 | MATCH
512 | (person) -[:BORN_IN]-> () -[:WITHIN*0..]-> (us:Location {name:'United States'}),
513 | (person) -[:LIVES_IN]-> () -[:WITHIN*0..]-> (eu:Location {name:'Europe'})
514 | RETURN person.name
515 | ```
516 |
517 | 注意到:
518 |
519 | 1. 点 `()`,边 `-[]→`,标签\类型 `:`,属性 `{}`。
520 | 2. 名字绑定或者说变量:`person`
521 | 3. 0 到多次通配符: `*0...`
522 |
523 | 正如声明式查询语言的一贯特点,你只需描述问题,不必担心执行过程。但与 SQL 的区别在于,SQL 基于关系代数,Cypher 类似正则表达式。
524 |
525 | 无论是 BFS、DFS 还是剪枝等实现细节,一般来说(但是不同厂商通常都会有不同的最佳实践),用户都不需要关心。
526 |
527 | ## 使用 SQL 进行图查询
528 |
529 | 前面看到可以用 SQL 存储点和边,以表示图。
530 |
531 | 那可以用 SQL 进行图查询吗?
532 |
533 | Oracle 的 [PGQL](https://docs.oracle.com/en/database/oracle/property-graph/20.4/spgdg/property-graph-query-language-pgql.html):
534 |
535 | ```sql
536 | CREATE PROPERTY GRAPH bank_transfers
537 | VERTEX TABLES (persons KEY(account_number))
538 | EDGE TABLES(
539 | transactions KEY (from_acct, to_acct, date, amount)
540 | SOURCE KEY (from_account) REFERENCES persons
541 | DESTINATION KEY (to_account) REFERENCES persons
542 | PROPERTIES (date, amount)
543 | )
544 | ```
545 |
546 | 其中有一个难点,就是如何表达图中的路径模式(graph pattern),如**多跳查询**,对应到 SQL 中,就是不确定次数的 Join:
547 |
548 | ```sql
549 | () -[:WITHIN*0..]-> ()
550 | ```
551 |
552 | 使用 SQL:1999 中 recursive common table expressions(PostgreSQL, IBM DB2, Oracle, and SQL Server 支持)的可以满足。但是,相当冗长和笨拙。
553 |
554 | ## **Triple-Stores and SPARQL**
555 |
556 | **Triple-Stores**,可以理解为三元组存储,即用三元组存储图。
557 |
558 | 
559 |
560 | 其含义如下:
561 |
562 | | Subject | 对应图中的一个点 |
563 | | --------- | -------------------------------------------------------------------------------------------------------------------------------------- |
564 | | Object | 1. 一个原子数据,如 string 或者 number。
2. 另一个 Subject。 |
565 | | Predicate | 1. 如果 Object 是原子数据,则 对应点附带的 KV 对。
2. 如果 Object 是另一个 Object,则 Predicate 对应图中的边。 |
566 |
567 | 仍是上边例子,用 Turtle triples (一种 **Triple-Stores** 语法)**表达为**:
568 |
569 | ```scheme
570 | @prefix : .
571 | _:lucy a :Person.
572 | _:lucy :name "Lucy".
573 | _:lucy :bornIn _:idaho.
574 | _:idaho a :Location.
575 | _:idaho :name "Idaho".
576 | _:idaho :type "state".
577 | _:idaho :within _:usa.
578 | _:usa a :Location
579 | _:usa :name "United States"
580 | _:usa :type "country".
581 | _:usa :within _:namerica.
582 | _:namerica a :Location.
583 | _:namerica :name "North America".
584 | _:namerica :type "continent".
585 | ```
586 |
587 | 一种更紧凑的写法:
588 |
589 | ```scheme
590 | @prefix : .
591 | _:lucy a: Person; :name "Lucy"; :bornIn _:idaho
592 | _:idaho a: Location; :name "Idaho"; :type "state"; :within _:usa.
593 | _:usa a: Location; :name "United States"; :type "country"; :within _:namerica.
594 | _:namerica a :Location; :name "North America"; :type "continent".
595 | ```
596 |
597 | ### 语义网(The **Semantic Web**)
598 |
599 | 万维网之父 Tim Berners Lee 于 1998 年提出,知识图谱前身。其目的在于对网络中的资源进行结构化,从而让计算机能够**理解**网络中的数据。即不是以文本、二进制流等非结构数据呈现内容,而是以某种标准结构化互联网上通过超链接而关联的数据。
600 |
601 | **语义**:提供一种统一的方式对所有资源进行描述和**结构化**(机器可读)。
602 |
603 | **网**:将所有资源勾连起来。
604 |
605 | 下面是**语义网技术栈**(Semantic Web Stack):
606 |
607 | 
608 |
609 | 其中 **RDF** ( *ResourceDescription Framework,资源描述框架* )提供了一种结构化网络中数据的标准。使发布到网络中的任何资源(文字、图片、视频、网页),都能以统一的形式被计算机理解。从另一个角度来理解,即,不需要资源使用方通过深度学习等方式来抽取语义,而是靠资源提供方通过 RDF 主动提供其资源语义。
610 |
611 | 感觉有点理想主义,但互联网、开源社区都是靠这种理想主义、分享精神发展起来的!
612 |
613 | 虽然语义网没有发展起来,但是其**中间数据交换**格式 RDF 所定义的 SPO 三元组 (Subject-Predicate-Object) 却是一种很好用的数据模型,也就是上面提到的 **Triple-Stores。**
614 |
615 | ### RDF 数据模型
616 |
617 | 上面提到的 Turtle 语言(SPO 三元组)是一种简单易读的描述 RDF 数据的方式,RDF 也可以基于 XML 表示,但是要冗余难读的多(嵌套太深):
618 |
619 | ```xml
620 |
622 |
623 | Idaho
624 | state
625 |
626 |
627 | United States
628 | country
629 |
630 |
631 | North America
632 | continent
633 |
634 |
635 |
636 |
637 |
638 |
639 | Lucy
640 |
641 |
642 |
643 | ```
644 |
645 | 为了标准化和去除二义性,一些看起来比较奇怪的点是:无论 subject,predicate 还是 object 都是由 URI 定义,如
646 |
647 | ```json
648 | lives_in 会表示为
649 | ```
650 |
651 | 其前缀只是一个 namespace,让定义唯一化,并且在网络上可访问。当然,一个简化的方法是可以在文件头声明一个公共前缀。
652 |
653 | ### **SPARQL 查询语言**
654 |
655 | 有了语义网,自然需要在语义网中进行遍历查询,于是有了 RDF 的查询语言:SPARQL Protocol and RDF Query Language, pronounced“sparkle.”
656 |
657 | ```
658 | PREFIX :
659 | SELECT ?personName WHERE {
660 | ?person :name ?personName.
661 | ?person :bornIn / :within* / :name "United States".
662 | ?person :livesIn / :within* / :name "Europe".
663 | }
664 | ```
665 |
666 | 他是 Cypher 的前驱,因此结构看起来很像:
667 |
668 | ```
669 | (person) -[:BORN_IN]-> () -[:WITHIN*0..]-> (location) # Cypher
670 | ?person :bornIn / :within* ?location. # SPARQL
671 | ```
672 |
673 | 但 **SPARQL** 没有区分边和属性的关系,都用了 Predicates。
674 |
675 | ```
676 | (usa {name:'United States'}) # Cypher
677 | ?usa :name "United States". # SPARQL
678 | ```
679 |
680 | 虽然语义网没有成功落地,但其技术栈影响了后来的知识图谱和图查询语言。
681 |
682 | ### 图模型和网络模型
683 |
684 | 图模型是网络模型旧瓶装新酒吗?
685 |
686 | 否,他们在很多重要的方面都不一样。
687 |
688 | | 模型 | 图模型(Graph Model) | 网络模型(Network Model) |
689 | | -------- | --------------------------------------------------------- | ------------------------------------------------------ |
690 | | 连接方式 | 任意两个点之间都有可以有边 | 指定了嵌套约束 |
691 | | 记录查找 | 1. 使用全局 ID
2. 使用属性索引。
3. 使用图遍历。 | 只能使用路径查询 |
692 | | 有序性 | 点和边都是无序的 | 记录的孩子们是有序集合,在插入时需要考虑维持有序的开销 |
693 | | 查询语言 | 即可命令式,也可以声明式 | 命令式的 |
694 |
695 | ## 查询语言前驱:Datalog
696 |
697 | 有点像 triple-store,但是变了下次序:(_subject_, _predicate_, _object_) → _predicate_(_subject_, _object_).
698 | 之前数据用 Datalog 表示为:
699 |
700 | ```
701 | name(namerica, 'North America').
702 | type(namerica, continent).
703 |
704 | name(usa, 'United States').
705 | type(usa, country).
706 | within(usa, namerica).
707 |
708 | name(idaho, 'Idaho').
709 | type(idaho, state).
710 | within(idaho, usa).
711 |
712 | name(lucy, 'Lucy').
713 | born_in(lucy, idaho).
714 | ```
715 |
716 | 查询从*美国迁移到欧洲的人*可以表示为:
717 |
718 | ```
719 | within_recursive(Location, Name) :- name(Location, Name). /* Rule 1 */
720 | within_recursive(Location, Name) :- within(Location, Via), /* Rule 2 */
721 | within_recursive(Via, Name).
722 |
723 | migrated(Name, BornIn, LivingIn) :- name(Person, Name), /* Rule 3 */
724 | born_in(Person, BornLoc),
725 | within_recursive(BornLoc, BornIn),
726 | lives_in(Person, LivingLoc),
727 | within_recursive(LivingLoc, LivingIn).
728 | ?- migrated(Who, 'United States', 'Europe'). /* Who = 'Lucy'. */
729 | ```
730 |
731 | 1. 代码中以大写字母开头的元素是**变量**,字符串、数字或以小写字母开头的元素是**常量**。下划线(\_)被称为匿名变量
732 | 2. 可以使用基本 Predicate 自定义 Predicate,类似于使用基本函数自定义函数。
733 | 3. 逗号连接的多个谓词表达式为且的关系。
734 |
735 | 
736 |
737 | 基于集合的逻辑运算:
738 |
739 | 1. 根据基本数据子集选出符合条件集合。
740 | 2. 应用规则,扩充原集合。
741 | 3. 如果可以递归,则递归穷尽所有可能性。
742 |
743 | Prolog(Programming in Logic 的缩写)是一种逻辑编程语言。它创建在逻辑学的理论基础之上。
744 |
745 | ## 参考
746 |
747 | 1. 声明式 (declarative) vs 命令式 (imperative)**:**[https://lotabout.me/2020/Declarative-vs-Imperative-language/](https://lotabout.me/2020/Declarative-vs-Imperative-language/)
748 | 2. **[SimmerChan](https://www.zhihu.com/people/simmerchan)** 知乎专栏,知识图谱,语义网,RDF:[https://www.zhihu.com/column/knowledgegraph](https://www.zhihu.com/column/knowledgegraph)
749 | 3. MySQL 为什么叫“关系”模型:[https://zhuanlan.zhihu.com/p/64731206](https://zhuanlan.zhihu.com/p/64731206)
750 |
--------------------------------------------------------------------------------
/ch03.md:
--------------------------------------------------------------------------------
1 | # DDIA 逐章精读(三):存储和查询
2 |
3 | 第二章讲了上层抽象:数据模型和查询语言。
4 | 本章下沉一些,聚焦数据库底层如何处理查询和存储。这其中,有个**逻辑链条**:
5 |
6 | > 使用场景 → 查询类型 → 存储格式。
7 |
8 | 查询类型主要分为两大类:
9 |
10 | | 引擎类型 | 请求数量 | 数据量 | 瓶颈 | 存储格式 | 用户 | 场景举例 | 产品举例 |
11 | | -------- | ---------------------- | ------------------------ | -------------- | ------------ | ---------------------------- | -------- | ---------- |
12 | | OLTP | 相对频繁,侧重在线交易 | 总体和单次查询都相对较小 | Disk Seek | 多用行存 | 比较普遍,一般应用用的比较多 | 银行交易 | MySQL |
13 | | OLAP | 相对较少,侧重离线分析 | 总体和单次查询都相对巨大 | Disk Bandwidth | 列存逐渐流行 | 多为商业用户 | 商业分析 | ClickHouse |
14 |
15 | 其中,OLTP 侧,常用的存储引擎又有两种流派:
16 |
17 | | 流派 | 主要特点 | 基本思想 | 代表 |
18 | | ------------------ | ---------------------------------------------------- | ---------------- | ------------------------------------------------ |
19 | | log-structured 流 | 只允许追加,所有修改都表现为文件的追加和文件整体增删 | 变随机写为顺序写 | Bitcask、LevelDB、RocksDB、Cassandra、Lucene |
20 | | update-in-place 流 | 以页(page)为粒度对磁盘数据进行修改 | 面向页、查找树 | B 族树,所有主流关系型数据库和一些非关系型数据库 |
21 |
22 | 此外,针对 OLTP,还探索了常见的建索引的方法,以及一种特殊的数据库——全内存数据库。
23 |
24 | 对于数据仓库,本章分析了它与 OLTP 的主要不同之处。数据仓库主要侧重于聚合查询,需要扫描很大量的数据,此时,索引就相对不太有用。需要考虑的是存储成本、带宽优化等,由此引出列式存储。
25 |
26 | # 驱动数据库的底层数据结构
27 |
28 | 本节由一个 shell 脚本出发,到一个相当简单但可用的存储引擎 Bitcask,然后引出 LSM-tree,他们都属于日志流范畴。之后转向存储引擎另一流派——B 族树,之后对其做了简单对比。最后探讨了存储中离不开的结构——索引。
29 |
30 | 首先来看,世界上“最简单”的数据库,由两个 Bash 函数构成:
31 |
32 | ```bash
33 | #!/bin/bash
34 | db_set () {
35 | echo "$1,$2" >> database
36 | }
37 |
38 | db_get () {
39 | grep "^$1," database | sed -e "s/^$1,//" | tail -n 1
40 | }
41 | ```
42 |
43 | 这两个函数实现了一个基于字符串的 KV 存储(只支持 get/set,不支持 delete):
44 |
45 | ```bash
46 | $ db_set 123456 '{"name":"London","attractions":["Big Ben","London Eye"]}'
47 | $ db_set 42 '{"name":"San Francisco","attractions":["Golden Gate Bridge"]}'
48 | $ db_get 42
49 | {"name":"San Francisco","attractions":["Golden Gate Bridge"]}
50 | ```
51 |
52 | 来分析下它为什么 work,也反映了日志结构存储的最基本原理:
53 |
54 | 1. set:在文件末尾追加一个 KV 对。
55 | 2. get:匹配所有 Key,返回最后(也即最新)一条 KV 对中的 Value。
56 |
57 | 可以看出:写很快,但是读需要全文逐行扫描,会慢很多。典型的以读换写。为了加快读,我们需要构建**索引**:一种允许基于某些字段查找的额外数据结构。
58 |
59 | 索引从原数据中构建,只为加快查找。因此索引会耗费一定额外空间,和插入时间(每次插入要更新索引),即,重新以空间和写换读取。
60 |
61 | 这便是数据库存储引擎设计和选择时最常见的**权衡(trade off)**:
62 |
63 | 1. 恰当的**存储格式**能加快写(日志结构),但是会让读取很慢;也可以加快读(查找树、B 族树),但会让写入较慢。
64 | 2. 为了弥补读性能,可以构建索引。但是会牺牲写入性能和耗费额外空间。
65 |
66 | 存储格式一般不好动,但是索引构建与否,一般交予用户选择。
67 |
68 | ## 哈希索引
69 |
70 | 本节主要基于最基础的 KV 索引。
71 |
72 | 依上小节的例子,所有数据顺序追加到磁盘上。为了加快查询,我们在内存中构建一个哈希索引:
73 |
74 | 1. Key 是查询 Key
75 | 2. Value 是 KV 条目的起始位置和长度。
76 |
77 | 
78 |
79 | 看来很简单,但这正是 [Bitcask](https://docs.riak.com/riak/kv/2.2.3/setup/planning/backend/bitcask/index.html 'Bitcask') 的基本设计,但关键是,他 Work(在小数据量时,即所有 key 都能存到内存中时):能提供很高的读写性能:
80 |
81 | 1. 写:文件追加写。
82 | 2. 读:一次内存查询,一次磁盘 seek;如果数据已经被缓存,则 seek 也可以省掉。
83 |
84 | 如果你的 key 集合很小(意味着能全放内存),但是每个 key 更新很频繁,那么 Bitcask 便是你的菜。举个栗子:频繁更新的视频播放量,key 是视频 url,value 是视频播放量。
85 |
86 | > 但有个很重要问题,单个文件越来越大,磁盘空间不够怎么办?
87 | >
88 | > 在文件到达一定尺寸后,就新建一个文件,将原文件变为只读。同时为了回收多个 key 多次写入的造成的空间浪费,可以将只读文件进行紧缩(compact),将旧文件进行重写,挤出“水分”(被覆写的数据)以进行垃圾回收。
89 |
90 | 
91 |
92 | 当然,如果我们想让其**工业可用**,还有很多问题需要解决:
93 |
94 | 1. **文件格式**。对于**日志**来说,CSV 不是一种紧凑的数据格式,有很多空间浪费。比如,可以用 length + record bytes。
95 | 2. **记录删除**。之前只支持 put\get,但实际还需要支持 delete。但日志结构又不支持更新,怎么办呢?一般是写一个特殊标记(比如墓碑记录,tombstone)以表示该记录已删除。之后 compact 时真正删除即可。
96 | 3. **宕机恢复**。在机器重启时,内存中的哈希索引将会丢失。当然,可以全盘扫描以重建,但通常一个小优化是,对于每个 segment file,将其索引条目和数据文件一块持久化,重启时只需加载索引条目即可。
97 | 4. **记录写坏、少写**。系统任何时候都有可能宕机,由此会造成记录写坏、少写。为了识别错误记录,我们需要增加一些校验字段,以识别并跳过这种数据。为了跳过写了部分的数据,还要用一些特殊字符来标识记录间的边界。
98 | 5. **并发控制**。由于只有一个活动(追加)文件,因此写只有一个天然并发度。但其他的文件都是不可变的(compact 时会读取然后生成新的),因此读取和紧缩可以并发执行。
99 |
100 | 乍一看,基于日志的存储结构存在折不少浪费:需要以追加进行更新和删除。但日志结构有几个原地更新结构无法做的优点:
101 |
102 | 1. **以顺序写代替随机写**。对于磁盘和 SSD,顺序写都要比随机写快几个数量级。
103 | 2. **简易的并发控制**。由于大部分的文件都是**不可变(immutable)** 的,因此更容易做并发读取和紧缩。也不用担心原地更新会造成新老数据交替。
104 | 3. **更少的内部碎片**。每次紧缩会将垃圾完全挤出。但是原地更新就会在 page 中留下一些不可用空间。
105 |
106 | 当然,基于内存的哈希索引也有其局限:
107 |
108 | 1. **所有 Key 必须放内存**。一旦 Key 的数据量超过内存大小,这种方案便不再 work。当然你可以设计基于磁盘的哈希表,但那又会带来大量的随机写。
109 | 2. **不支持范围查询**。由于 key 是无序的,要进行范围查询必须全表扫描。
110 |
111 | 后面讲的 LSM-Tree 和 B+ 树,都能部分规避上述问题。
112 |
113 | - 想想,会如何进行规避?
114 |
115 | ## SSTables 和 LSM-Trees
116 |
117 | 这一节层层递进,步步做引,从 SSTables 格式出发,牵出 LSM-Trees 全貌。
118 |
119 | 对于 KV 数据,前面的 BitCask 存储结构是:
120 |
121 | 1. 外存上日志片段
122 | 2. 内存中的哈希表
123 |
124 | 其中外存上的数据是简单追加写而形成的,并没有按照某个字段有序。
125 |
126 | 假设加一个限制,让这些文件按 key 有序。我们称这种格式为:SSTable(Sorted String Table)。
127 |
128 | 这种文件格式有什么优点呢?
129 |
130 | **高效的数据文件合并**。即有序文件的归并外排,顺序读,顺序写。不同文件出现相同 Key 怎么办?
131 |
132 | 
133 |
134 | **不需要在内存中保存所有数据的索引**。仅需要记录下每个文件界限(以区间表示:[startKey, endKey],当然实际会记录的更细)即可。查找某个 Key 时,去所有包含该 Key 的区间对应的文件二分查找即可。
135 |
136 | 
137 |
138 | **分块压缩,节省空间,减少 IO**。相邻 Key 共享前缀,既然每次都要批量取,那正好一组 key batch 到一块,称为 block,且只记录 block 的索引。
139 |
140 | ### 构建和维护 SSTables
141 |
142 | SSTables 格式听起来很美好,但须知数据是乱序的来的,我们如何得到有序的数据文件呢?
143 |
144 | 这可以拆解为两个小问题:
145 |
146 | 1. 如何构建。
147 | 2. 如何维护。
148 |
149 | **构建 SSTable 文件**。将乱序数据在外存(磁盘 or SSD)中上整理为有序文件,是比较难的。但是在内存就方便的多。于是一个大胆的想法就形成了:
150 |
151 | 1. 在内存中维护一个有序结构(称为 **MemTable**)。红黑树、AVL 树、跳表。
152 | 2. 到达一定阈值之后全量 dump 到外存。
153 |
154 | **维护 SSTable 文件**。为什么需要维护呢?首先要问,对于上述复合结构,我们怎么进行查询:
155 |
156 | 1. 先去 MemTable 中查找,如果命中则返回。
157 | 2. 再去 SSTable 按时间顺序由新到旧逐一查找。
158 |
159 | 如果 SSTable 文件越来越多,则查找代价会越来越大。因此需要将多个 SSTable 文件合并,以减少文件数量,同时进行 GC,我们称之为**紧缩**( Compaction)。
160 |
161 | **该方案的问题**:如果出现宕机,内存中的数据结构将会消失。解决方法也很经典:WAL。
162 |
163 | ### 从 SSTables 到 LSM-Tree
164 |
165 | 将前面几节的一些碎片有机的组织起来,便是时下流行的存储引擎 LevelDB 和 RocksDB 后面的存储结构:LSM-Tree:
166 |
167 | 
168 |
169 | 这种数据结构是 Patrick O’Neil 等人,在 1996 年提出的:[The Log-Structured Merge-Tree](https://www.cs.umb.edu/~poneil/lsmtree.pdf 'The Log-Structured Merge-Tree')。
170 |
171 | Elasticsearch 和 Solr 的索引引擎 Lucene,也使用类似 LSM-Tree 存储结构。但其数据模型不是 KV,但类似:word → document list。
172 |
173 | ### 性能优化
174 |
175 | 如果想让一个引擎工程上可用,还会做大量的性能优化。对于 LSM-Tree 来说,包括:
176 |
177 | **优化 SSTable 的查找**。常用 [**Bloom Filter**](https://www.qtmuniao.com/2020/11/18/leveldb-data-structures-bloom-filter/)。该数据结构可以使用较少的内存为每个 SSTable 做一些指纹,起到一些初筛的作用。
178 |
179 | **层级化组织 SSTable**。以控制 Compaction 的顺序和时间。常见的有 size-tiered 和 leveled compaction。LevelDB 便是支持后者而得名。前者比较简单粗暴,后者性能更好,也因此更为常见。
180 |
181 | 
182 |
183 | 对于 RocksDB 来说,工程上的优化和使用上的优化就更多了。在其 [Wiki](https://github.com/facebook/rocksdb/wiki 'rocksdb wiki') 上随便摘录几点:
184 |
185 | 1. Column Family
186 | 2. 前缀压缩和过滤
187 | 3. 键值分离,BlobDB
188 |
189 | 但无论有多少变种和优化,LSM-Tree 的核心思想——**保存一组合理组织、后台合并的 SSTables** ——简约而强大。可以方便的进行范围遍历,可以变大量随机为少量顺序。
190 |
191 | ## B 族树
192 |
193 | 虽然先讲的 LSM-Tree,但是它要比 B+ 树新的多。
194 |
195 | B 树于 1970 年被 R. Bayer and E. McCreight [提出](https://dl.acm.org/doi/10.1145/1734663.1734671 'b tree paper')后,便迅速流行了起来。现在几乎所有的关系型数据中,它都是数据索引标准一般的实现。
196 |
197 | 与 LSM-Tree 一样,它也支持高效的**点查**和**范围查**。但却使用了完全不同的组织方式。
198 |
199 | 其特点有:
200 |
201 | 1. 以页(在磁盘上叫 page,在内存中叫 block,通常为 4k)为单位进行组织。
202 | 2. 页之间以页 ID 来进行逻辑引用,从而组织成一颗磁盘上的树。
203 |
204 | 
205 |
206 | **查找**。从根节点出发,进行二分查找,然后加载新的页到内存中,继续二分,直到命中或者到叶子节点。查找复杂度,树的高度—— O(lgn),影响树高度的因素:分支因子(分叉数,通常是几百个)。
207 |
208 | 
209 |
210 | **插入 or 更新**。和查找过程一样,定位到原 Key 所在页,插入或者更新后,将页完整写回。如果页剩余空间不够,则分裂后写入。
211 |
212 | **分裂 or 合并**。级联分裂和合并。
213 |
214 | - 一个记录大于一个 page 怎么办?
215 | 树的节点是逻辑概念,page or block 是物理概念。一个逻辑节点可以对应多个物理 page。
216 |
217 | ### 让 B 树更可靠
218 |
219 | B 树不像 LSM-Tree,会在原地修改数据文件。
220 |
221 | 在树结构调整时,可能会级联修改很多 Page。比如叶子节点分裂后,就需要写入两个新的叶子节点,和一个父节点(更新叶子指针)。
222 |
223 | 1. 增加预写日志(WAL),将所有修改操作记录下来,预防宕机时中断树结构调整而产生的混乱现场。
224 | 2. 使用 latch 对树结构进行并发控制。
225 |
226 | ### B 树的优化
227 |
228 | B 树出来了这么久,因此有很多优化:
229 |
230 | 1. 不使用 WAL,而在写入时利用 Copy On Write 技术。同时,也方便了并发控制。如 LMDB、BoltDB。
231 | 2. 对中间节点的 Key 做压缩,保留足够的路由信息即可。以此,可以节省空间,增大分支因子。
232 | 3. 为了优化范围查询,有的 B 族树将叶子节点存储时物理连续。但当数据不断插入时,维护此有序性的代价非常大。
233 | 4. 为叶子节点增加兄弟指针,以避免顺序遍历时的回溯。即 B+ 树的做法,但远不局限于 B+ 树。
234 | 5. B 树的变种,分形树,从 LSM-tree 借鉴了一些思想以优化 seek。
235 |
236 | ## B-Trees 和 LSM-Trees 对比
237 |
238 | | 存储引擎 | B-Tree | LSM-Tree | 备注 |
239 | | -------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- |
240 | | 优势 | 读取更快 | 写入更快 | |
241 | | 写放大 | 1. 数据和 WAL
2. 更改数据时多次覆盖整个 Page | 1. 数据和 WAL
2. Compaction | SSD 不能过多擦除。因此 SSD 内部的固件中也多用日志结构来减少随机小写。 |
242 | | 写吞吐 | 相对较低:
1. 大量随机写。 | 相对较高:
1. 较低的写放大(取决于数据和配置)
2. 顺序写入。
3. 更为紧凑。 | |
243 | | 压缩率 | 1. 存在较多内部碎片。 | 1. 更加紧凑,没有内部碎片。
2. 压缩潜力更大(共享前缀)。 | 但紧缩不及时会造成 LSM-Tree 存在很多垃圾 |
244 | | 后台流量 | 1. 更稳定可预测,不会受后台 compaction 突发流量影响。 | 1. 写吞吐过高,compaction 跟不上,会进一步加重读放大。
2. 由于外存总带宽有限,compaction 会影响读写吞吐。
3. 随着数据越来越多,compaction 对正常写影响越来越大。 | RocksDB 写入太过快会引起 write stall,即限制写入,以期尽快 compaction 将数据下沉。 |
245 | | 存储放大 | 1. 有些 Page 没有用满 | 1. 同一个 Key 存多遍 | |
246 | | 并发控制 | 1. 同一个 Key 只存在一个地方
2. 树结构容易加范围锁。 | 同一个 Key 会存多遍,一般使用 MVCC 进行控制。 | |
247 |
248 | ## 其他索引结构
249 |
250 | **次级索引(secondary indexes)**。即,非主键的其他属性到该元素(SQL 中的行,MongoDB 中的文档和图数据库中的点和边)的映射。
251 |
252 | ### **聚集索引和非聚集索引(cluster indexes and non-cluster indexes)**
253 |
254 | 对于存储数据和组织索引,我们可以有多种选择:
255 |
256 | 1. 数据本身**无序**的存在文件中,称为 **堆文件(heap file)**,索引的值指向对应数据在 heap file 中的位置。这样可以避免多个索引时的数据拷贝。
257 | 2. 数据本身按某个字段有序存储,该字段通常是主键。则称基于此字段的索引为**聚集索引**(clustered index),从另外一个角度理解,即将索引和数据存在一块。则基于其他字段的索引为**非聚集索引**,在索引中仅存数据的引用。
258 | 3. 一部分列内嵌到索引中存储,一部分列数据额外存储。称为**覆盖索引(covering index)**
259 | 或 **包含列的索引(index with included columns)**。
260 |
261 | 索引可以加快查询速度,但需要占用额外空间,并且牺牲了部分更新开销,且需要维持某种一致性。
262 |
263 | ### **多列索引**(**Multi-column indexes**)
264 |
265 | 现实生活中,多个字段联合查询更为常见。比如查询某个用户周边一定范围内的商户,需要经度和纬度二维查询。
266 |
267 | ```sql
268 | SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
269 | AND longitude > -0.1162 AND longitude < -0.1004;
270 | ```
271 |
272 | 可以:
273 |
274 | 1. 将二维编码为一维,然后按普通索引存储。
275 | 2. 使用特殊数据结构,如 R 树。
276 |
277 | ### **全文索引和模糊索引(Full-text search and fuzzy indexes)**
278 |
279 | 前述索引只提供全字段的精确匹配,而不提供类似搜索引擎的功能。比如,按字符串中包含的单词查询,针对笔误的单词查询。
280 |
281 | 在工程中常用 [Apace Lucene](https://lucene.apache.org/ 'Apace Lucene') 库,和其包装出来的服务:[Elasticsearch](https://www.elastic.co/cn/ 'Elasticsearch')。他也使用类似 LSM-tree 的日志存储结构,但使用其索引进行模糊匹配的过程,本质上是一个有限状态自动机,在行为上类似 Trie 树。
282 |
283 | ### 全内存数据结构
284 |
285 | 随着单位内存成本下降,甚至支持持久化(_non-volatile memory_,NVM,如 Intel 的 [傲腾](https://www.intel.cn/content/www/cn/zh/products/details/memory-storage/optane-dc-persistent-memory.html '傲腾')),全内存数据库也逐渐开始流行。
286 |
287 | 根据是否需要持久化,内存数据大概可以分为两类:
288 |
289 | 1. **不需要持久化**。如只用于缓存的 Memcached。
290 | 2. **需要持久化**。通过 WAL、定期 snapshot、远程备份等等来对数据进行持久化。但使用内存处理全部读写,因此仍是内存数据库。
291 |
292 | > VoltDB, MemSQL, and Oracle TimesTen 是提供关系模型的内存数据库。RAMCloud 是提供持久化保证的 KV 数据库。Redis and Couchbase 仅提供弱持久化保证。
293 |
294 | 内存数据库存在优势的原因不仅在于不需要读取磁盘,而在更于不需要对数据结构进行**序列化、编码**后以适应磁盘所带来的**额外开销**。
295 |
296 | 当然,内存数据库还有以下优点:
297 |
298 | 1. **提供更丰富的数据抽象**。如 set 和 queue 这种只存在于内存中的数据抽象。
299 | 2. **实现相对简单**。因为所有数据都在内存中。
300 |
301 | 此外,内存数据库还可以通过类似操作系统 swap 的方式,提供比物理机内存更大的存储空间,但由于其有更多数据库相关信息,可以将换入换出的粒度做的更细、性能做的更好。
302 |
303 | 基于**非易失性存储器**(non-volatile memory,NVM)的存储引擎也是这些年研究的一个热点。
304 |
305 | # 事务型还是分析型
306 |
307 | 术语 **OL**(Online)主要是指交互式的查询。
308 |
309 | 术语**事务**(transaction)由来有一些历史原因。早期的数据库使用方多为商业交易(commercial),比如买卖、发工资等等。但是随着数据库应用不断扩大,交易\事务作为名词保留了下来。
310 |
311 | > 事务不一定具有 ACID 特性,事务型处理多是随机的以较低的延迟进行读写,与之相反,分析型处理多为定期的批处理,延迟较高。
312 |
313 | 下表是一个对比:
314 |
315 | | 属性 | OLTP | OLAP |
316 | | ------------ | ------------------------------- | -------------------------------------- |
317 | | 主要读取模式 | 小数据量的随机读,通过 key 查询 | 大数据量的聚合(max,min,sum, avg)查询 |
318 | | 主要写入模式 | 随机访问,低延迟写入 | 批量导入(ETL)或者流式写入 |
319 | | 主要应用场景 | 通过 web 方式使用的最终用户 | 互联网分析,为了辅助决策 |
320 | | 如何看待数据 | 当前时间点的最新状态 | 随着时间推移的 |
321 | | 数据尺寸 | 通常 GB 到 TB | 通常 TB 到 PB |
322 |
323 | 一开始对于 AP 场景,仍然使用的传统数据库。在模型层面来说,SQL 足够灵活,能够基本满足 AP 查询需求。但在实现层面,传统数据库在 AP 负载中的表现(大数据量吞吐较低)不尽如人意,因此大家开始转向在专门设计的数据库中进行 AP 查询,我们称之为**数据仓库**(Data Warehouse)。
324 |
325 | ## 数据仓库
326 |
327 | 对于一个企业来说,一般都会有很多偏交易型的系统,如用户网站、收银系统、仓库管理、供应链管理、员工管理等等。通常要求**高可用**与**低延迟**,因此直接在原库进行业务分析,会极大影响正常负载。因此需要一种手段将数据从原库导入到专门的**数仓**。
328 |
329 | 我们称之为 **ETL:extract-transform-load**。
330 |
331 | 
332 |
333 | 一般企业的数据量达到一定的量级才会需要进行 AP 分析,毕竟在小数据量尺度下,用 Excel 进行聚合查询都够了。当然,现在一个趋势是,随着移动互联网、物联网的普及,接入终端的类型和数量越来越多,产生的数据增量也越来越大,哪怕初创不久的公司可能也会积存大量数据,进而也需要 AP 支持。
334 |
335 | AP 场景下的**聚合查询**分析和传统 TP 型有所不同。因此,需要构建索引的方式也多有不同。
336 |
337 | ### 同样接口后的不同
338 |
339 | TP 和 AP 都可以使用 SQL 模型进行查询分析。但是由于其负载类型完全不同,在查询引擎实现和存储格式优化时,做出的设计决策也就大相径庭。因此,在同一套 SQL 接口的表面下,两者对应的数据库实现结构差别很大。
340 |
341 | 虽然有的数据库系统号称两者都支持,比如之前的 Microsoft SQL Server 和 SAP HANA,但是也正日益发展成两种独立的查询引擎。近年来提的较多的 HTAP 系统也是类似,其为了 serve 不同类型负载底层其实有两套不同的存储,只不过系统内部会自动的做数据的冗余和重新组织,对用户透明。
342 |
343 | ## AP 建模:星状型和雪花型
344 |
345 | AP 中的处理模型相对较少,比较常用的有**星状模型**,也称为**维度模型**。
346 |
347 | 
348 |
349 | 如上图所示,星状模型通常包含一张**事件表(_fact table_)** 和多张**维度表(_dimension tables_)**。事件表以事件流的方式将数据组织起来,然后通过外键指向不同的维度。
350 |
351 | 星状模型的一个变种是雪花模型,可以类比雪花(❄️)图案,其特点是在维度表中会进一步进行二次细分,讲一个维度分解为几个子维度。比如品牌和产品类别可能有单独的表格。星状模型更简单,雪花模型更精细,具体应用中会做不同取舍。
352 |
353 | 在典型的数仓中,事件表可能会非常宽,即有很多的列:一百到数百列。
354 |
355 | # 列存
356 |
357 | 前一小节提到的**分维度表**和**事实表**,对于后者来说,有可能达到数十亿行和数 PB 大。虽然事实表可能通常有几十上百列,但是单次查询通常只关注其中几个维度(列)。
358 |
359 | 如查询**人们是否更倾向于在一周的某一天购买新鲜水果或糖果**:
360 |
361 | ```sql
362 | SELECT
363 | dim_date.weekday,
364 | dim_product.category,
365 | SUM(fact_sales.quantity) AS quantity_sold
366 | FROM fact_sales
367 | JOIN dim_date ON fact_sales.date_key = dim_date.date_key
368 | JOIN dim_product ON fact_sales.product_sk = dim_product.product_sk
369 | WHERE
370 | dim_date.year = 2013 AND
371 | dim_product.category IN ('Fresh fruit', 'Candy')
372 | GROUP BY
373 | dim_date.weekday, dim_product.category;
374 | ```
375 |
376 | 由于传统数据库通常是按行存储的,这意味着对于属性(列)很多的表,哪怕只查询一个属性,也必须从磁盘上取出很多属性,无疑浪费了 IO 带宽、增大了读放大。
377 |
378 | 于是一个很自然的想法呼之欲出:每一个列分开存储好不好?
379 |
380 | 
381 |
382 | 不同列之间同一个行的字段可以通过下标来对应。当然也可以内嵌主键来对应,但那样存储成本就太高了。
383 |
384 | ## 列压缩
385 |
386 | 将所有数据分列存储在一块,带来了一个意外的好处,由于同一属性的数据相似度高,因此更易压缩。
387 |
388 | 如果每一列中值阈相比行数要小的多,可以用**位图编码(_[bitmap encoding](https://en.wikipedia.org/wiki/Bitmap_index 'bitmap encoding')_)**。举个例子,零售商可能有数十亿的销售交易,但只有 100,000 个不同的产品。
389 |
390 | 
391 |
392 | 上图中,是一个列分片中的数据,可以看出只有 {29, 30, 31, 68, 69, 74} 六个离散值。针对每个值出现的位置,我们使用一个 bit array 来表示:
393 |
394 | 1. bit map 下标对应列的下标
395 | 2. 值为 0 则表示该下标没有出现该值
396 | 3. 值为 1 则表示该下标出现了该值
397 |
398 | 如果 bit array 是稀疏的,即大量的都是 0,只要少量的 1。其实还可以使用 **[游程编码](https://zh.wikipedia.org/zh/%E6%B8%B8%E7%A8%8B%E7%BC%96%E7%A0%81 '游程编码')(RLE,Run-length encoding)** 进一步压缩:
399 |
400 | 1. 将连续的 0 和 1,改写成 `数量+值`,比如 `product_sk = 29` 是 `9 个 0,1 个 1,8 个 0`。
401 | 2. 使用一个小技巧,将信息进一步压缩。比如将同值项合并后,肯定是 0 1 交错出现,固定第一个值为 0,则交错出现的 0 和 1 的值也不用写了。则 `product_sk = 29` 编码变成 `9,1,8`
402 | 3. 由于我们知道 bit array 长度,则最后一个数字也可以省掉,因为它可以通过 `array len - sum(other lens)` 得到,则 `product_sk = 29` 的编码最后变成:`9,1`
403 |
404 | 位图索引很适合应对查询中的逻辑运算条件,比如:
405 |
406 | ```sql
407 | WHERE product_sk IN(30,68,69)
408 | ```
409 |
410 | 可以转换为 `product_sk = 30`、`product_sk = 68`和 `product_sk = 69`这三个 bit array 按位或(OR)。
411 |
412 | ```sql
413 | WHERE product_sk = 31 AND store_sk = 3
414 | ```
415 |
416 | 可以转换为 `product_sk = 31`和 `store_sk = 3`的 bit array 的按位与,就可以得到所有需要的位置。
417 |
418 | ### 列族
419 |
420 | 书中特别提到**列族(column families)**。它是 Cassandra 和 HBase 中的的概念,他们都起源于自谷歌的 [BigTable](https://en.wikipedia.org/wiki/Bigtable 'BigTable') 。注意到他们和**列式(column-oriented)存储**有相似之处,但绝不完全相同:
421 |
422 | 1. 同一个列族中多个列是一块存储的,并且内嵌行键(row key)。
423 | 2. 并且列不压缩(存疑?)
424 |
425 | 因此 BigTable 在用的时候主要还是面向行的,可以理解为每一个列族都是一个子表。
426 |
427 | ### 内存带宽和向量化处理
428 |
429 | 数仓的超大规模数据量带来了以下瓶颈:
430 |
431 | 1. 内存处理带宽
432 | 2. CPU 分支预测错误和[流水线停顿](https://zh.wikipedia.org/wiki/%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%81%9C%E9%A1%BF '流水线停顿')
433 |
434 | 关于内存的瓶颈可已通过前述的数据压缩来缓解。对于 CPU 的瓶颈可以使用:
435 |
436 | 1. 列式存储和压缩可以让数据尽可能多地缓存在 L1 中,结合位图存储进行快速处理。
437 | 2. 使用 SIMD 用更少的时钟周期处理更多的数据。
438 |
439 | ## 列式存储的排序
440 |
441 | 由于数仓查询多集中于聚合算子(比如 sum,avg,min,max),列式存储中的存储顺序相对不重要。但也免不了需要对某些列利用条件进行筛选,为此我们可以如 LSM-Tree 一样,对所有行按某一列进行排序后存储。
442 |
443 | > 注意,不可能同时对多列进行排序。因为我们需要维护多列间的下标间的对应关系,才可能按行取数据。
444 |
445 | 同时,排序后的那一列,压缩效果会更好。
446 |
447 | ### 不同副本,不同排序
448 |
449 | 在分布式数据库(数仓这么大,通常是分布式的)中,同一份数据我们会存储多份。对于每一份数据,我们可以按不同列有序存储。这样,针对不同的查询需求,便可以路由到不同的副本上做处理。当然,这样也最多只能建立副本数(通常是 3 个左右)列索引。
450 |
451 | 这一想法由 C-Store 引入,并且为商业数据仓库 Vertica 采用。
452 |
453 | ## 列式存储的写入
454 |
455 | 上述针对数仓的优化(列式存储、数据压缩和按列排序)都是为了解决数仓中常见的读写负载,读多写少,且读取都是超大规模的数据。
456 |
457 | > 我们针对读做了优化,就让写入变得相对困难。
458 |
459 | 比如 B 树的**原地更新流**是不太行的。举个例子,要在中间某行插入一个数据,**纵向**来说,会影响所有的列文件(如果不做 segment 的话);为了保证多列间按下标对应,**横向**来说,又得更新该行不同列的所有列文件。
460 |
461 | 所幸我们有 LSM-Tree 的追加流。
462 |
463 | 1. 将新写入的数据在**内存**中 Batch 好,按行按列,选什么数据结构可以看需求。
464 | 2. 然后达到一定阈值后,批量刷到**外存**,并与老数据合并。
465 |
466 | 数仓 Vertica 就是这么做的。
467 |
468 | ## 聚合:数据立方和物化视图
469 |
470 | 不一定所有的数仓都是列式存储,但列式存储的种种好处让其变得流行了起来。
471 |
472 | 其中一个值得一提的是**物化聚合(materialized aggregates,或者物化汇总)**。
473 |
474 | > 物化,可以简单理解为持久化。本质上是一种空间换时间的 tradeoff。
475 |
476 | 数据仓库查询通常涉及聚合函数,如 SQL 中的 COUNT、SUM、AVG、MIN 或 MAX。如果这些函数被多次用到,每次都即时计算显然存在巨大浪费。因此一个想法就是,能不能将其缓存起来。
477 |
478 | 其与关系数据库中的**视图**(View)区别在于,视图是**虚拟的、逻辑存在**的,只是对用户提供的一种抽象,是一个查询的中间结果,并没有进行持久化(有没有缓存就不知道了)。
479 |
480 | 物化视图本质上是对数据的一个摘要存储,如果原数据发生了变动,该视图要被重新生成。因此,如果**写多读少**,则维持物化视图的代价很大。但在数仓中往往反过来,因此物化视图才能较好的起作用。
481 |
482 | 物化视图一个特化的例子,是**数据立方**(data cube,或者 OLAP cube):按不同维度对量化数据进行聚合。
483 |
484 | 
485 |
486 | 上图是一个按**日期和产品分类**两个维度进行加和的数据立方,当针对日期和产品进行汇总查询时,由于该表的存在,就会变得非常快。
487 |
488 | 当然,现实中,一个表中常常有多个维度,比如 3-9 中有日期、产品、商店、促销和客户五个维度。但构建数据立方的意义和方法都是相似的。
489 |
490 | 但这种构建出来的视图只能针对固定的查询进行优化,如果有的查询不在此列,则这些优化就不再起作用。
491 |
492 | > 在实际中,需要针对性的识别(或者预估)每个场景查询分布,针对性的构建物化视图。
493 |
--------------------------------------------------------------------------------
/ch04.md:
--------------------------------------------------------------------------------
1 | # DDIA 逐章精读(四):编码和演进
2 |
3 | 第三章讲了存储引擎,本章继续下探,探讨编码相关问题。
4 |
5 | 所有涉及跨进程通信的地方,都需要对数据进行**编码**(**Encoding**),或者说**序列化**(**Serialization**)。因为持久化存储和网络传输都是面向字节流的。序列化本质上是一种“**降维**”操作,将内存中高维的数据结构降维成单维的字节流,于是底层硬件和相关协议,只需要处理一维信息即可。
6 |
7 | 编码主要涉及两方面问题:
8 |
9 | 1. 如何编码能够节省空间、提高性能。
10 | 2. 如何编码以适应数据的演化和兼容。
11 |
12 | 第一小节,以几种常见的编码工具(JSON,XML,Protocol Buffers 和 Avro)为例,逐一探讨了其如何进行编码、如何进行多版本兼容。这里引出了两个非常重要的概念:
13 |
14 | 1. 向后兼容 (backward compatibility):当前代码可以读取旧版本代码写入的数据。
15 | 2. 向前兼容 (forward compatibility):当前代码可以读取新版本代码写入的数据。
16 |
17 | > 翻译成中文后,很容易混淆,主要原因在于“后”的歧义性,到底指**身后**(过去),还是指**之后**(将来),私以为还不如翻译为,*兼容过去*和*兼容将来*。但为了习惯,后面行文仍然用向后/前兼容。
18 |
19 | 其中,向后兼容比较常见,因为时间总是向前流逝,版本总是升级,那么升级之后的代码总要处理历史积压的数据,自然会产生向后兼容的问题。向前兼容比较少见,书中给出的例子是多实例滚动升级,但其持续时间也很短。
20 |
21 | 第二小节,结合几个具体的应用场景:数据库、服务和消息系统,来分别谈了相关数据流中涉及到的编码与演化。
22 |
23 | # 数据编码的格式
24 |
25 | 
26 |
27 | 编码(Encoding)有多种称谓,如**序列化(serialization)** 或 **编组(marshalling)**。对应的,解码(Decoding)也有多种别称,**解析(Parsing)**,**反序列化(deserialization)**,**反编组 (unmarshalling)。**
28 |
29 | - 为什么内存中数据和外存、网络中的会有如此不同呢?
30 | 在内存中,借助编译器,我们可以将内存解释为各种数据结构;但在文件系统和网络中,我们只能通过 seek\read 等几个有限的操作来流式的读取字节流。那 mmap 呢?
31 | - 编码和序列化撞车了?
32 | 在事务中,也有序列化相关的术语,所以这里专用编码,以避免歧义。
33 | - 编码(encoding)和加密(**encryption**)?
34 | 研究的范畴不太一样,编码是为了持久化或者传输,着重点在格式和演化;而加密是为了安全,着重点在于安全、防破解。
35 |
36 | ## 编程语言内置
37 |
38 | 很多编程语言内置了一些缺省的编码方法:
39 |
40 | 1. Java 有 `java.io.Serializable`
41 | 2. Ruby 有 `Marshal`
42 | 3. Python 有 `pickle`
43 |
44 | 如果你确定你的数据只会被某种特定的语言所读取,那么这种内置的编码方法很好用。比如深度学习研究员因为基本都用 Python,所以常会把数据以 [pickle](https://docs.python.org/zh-cn/3/library/pickle.html 'pickle 官方文档') 的格式传来传去。
45 |
46 | 但这些编程语言内置的编码格式有以下缺点:
47 |
48 | 1. 和特定语言绑定
49 | 2. 安全问题
50 | 3. 兼容性支持不够
51 | 4. 效率不高
52 |
53 | ## JSON、XML 及其二进制变体
54 |
55 | JSON,XML 和 CSV 属于常用的**文本编码**格式,其好处在于肉眼可读,坏处在于不够紧凑,占空间较多。
56 |
57 | JSON 最初由 JavaScript 引入,因此在 Web Service 中用的较多,当然随着 web 的火热,现在成为了比较通用的编码格式,比如很多日志格式就是 JSON 的。
58 |
59 | XML 比较古老了,比 JSON 冗余度还高,有时候配置文件中会用,但总体而言用的越来越少了。
60 |
61 | CSV(以逗号\TAB、换行符分割)还算紧凑,但是表达能力有限。数据库表导出有时会用。
62 |
63 | 除了不够紧凑外,**文本编码(text encoding)** 还有以下缺点:
64 |
65 | 1. 对**数值类型支持不够**。CSV 和 XML 直接不支持,万物皆字符串。JSON 虽区分字符串和数值,但是不进一步区分细分数值类型。可以理解,毕竟文本编码嘛,主要还是面向字符串。
66 | 2. **对二进制数据支持不够**。支持 Unicode,但是对二进制串支持不够,可能会显示为乱码。虽然可以通过 Base64 编码来绕过,但有点做无用功的感觉。
67 | 3. **XML 和 JSON 支持额外的模式**。模式会描述数据的类型,告诉你如何理解数据。配合这些模式语言,虽然可以让 XML 和 JSON 变得强大,但是大大增加了复杂度。
68 | 4. **CSV 没有任何模式**。
69 |
70 | 凡事讲究够用,很多场景下需要数据可读,并且不关心编码效率,那么这几种编码格式就够用了。
71 |
72 | ### 二进制编码
73 |
74 | 如果数据只被单一程序读取,不需要进行交换,不需要考虑易读性等问题。则可以用二进制编码,在数据量到达一定程度后,二进制编码所带来的空间节省、速度提高都很可观。
75 |
76 | 因此,JSON 有很多二进制变种:MessagePack、BSON、BJSON、UBJSON、BISON 和 Smile 等。
77 |
78 | 对于下面例子,
79 |
80 | ```json
81 | {
82 | "userName": "Martin",
83 | "favoriteNumber": 1337,
84 | "interests": ["daydreaming", "hacking"]
85 | }
86 | ```
87 |
88 | 如果用 MessagePack 来编码,则为:
89 |
90 | 
91 |
92 | 可以看出其基本编码策略为:使用类型,长度,bit 串,顺序编码,去掉无用的冒号、引号、花括号。
93 |
94 | 从而将 JSON 编码的 81 字节缩小到了 66 字节,微有提高。
95 |
96 | ## Thrift 和 Protocol Buffers
97 |
98 | Thrift 最初由 Facebook,ProtoBuf 由 Google 在 07~08 年左右开源。他们都有对应的 RPC 框架和编解码工具。表达能力类似,语法也类似,在编码前都需要由接口定义语言(IDL)来描述模式:
99 |
100 | ```protobuf
101 | struct Person {
102 | 1: required string userName,
103 | 2: optional i64 favoriteNumber,
104 | 3: optional list interests
105 | }
106 | ```
107 |
108 | ```protobuf
109 | message Person {
110 | required string user_name = 1;
111 | optional int64 favorite_number = 2;
112 | repeated string interests = 3;
113 | }
114 | ```
115 |
116 | IDL 是编程语言无关的,可以利用相关代码生成工具,可以将上述 IDL 翻译为指定语言的代码。即,集成这些生成的代码,无论什么样的语言,都可以使用同样的格式编解码。
117 |
118 | 这也是不同 service 可以使用不同编码语言,且能够互相通信的基础。
119 |
120 | 此外,Thrift 还支持多种不同的编码格式,常用的有:Binary、Compact、JSON。可以让用户自行在:编码速度、占用空间、可读性方便进行取舍。
121 |
122 | 
123 |
124 | 可以看出其特点:
125 |
126 | 1. **使用 field tag 编码**。field tag 其实蕴含了字段**类型**和**名字**。
127 | 2. 使用类型、tag、长度、bit 数组的顺序编码。
128 |
129 | 
130 |
131 | 相比 Binary Protocol,Compact Protocol 由以下优化:
132 |
133 | 1. filed tag 只记录 delta。
134 | 2. 从而将 field tag 和 type 压缩到一个字节中。
135 | 3. 对数字使用变长编码和[Zigzag 编码](img/ch04-fig04.png)。
136 |
137 | ProtoBuf 与 Thrift Compact Protocol 编码方式很类似,也用了变长编码和 Zigzag 编码。但 ProtoBuf 对于数组的处理与 Thrift 显著不同,使用了 repeated 前缀而非真数组,好处后面说。
138 |
139 | 
140 |
141 | ### 字段标号和模式演变
142 |
143 | **模式**,即有哪些字段,字段分别为什么类型。
144 |
145 | 随着时间的推移,业务总会发生变化,我们也不可避免的**增删字段**,**修改字段类型**,即**模式演变**。
146 |
147 | 在模式发生改变后,需要:
148 |
149 | 1. **向后兼容**:新的代码,在处理新的增量数据格式的同时,也得处理旧的存量数据。
150 | 2. **向前兼容**:旧的代码,如果遇到新的数据格式,不能 crash。
151 |
152 | - ProtoBuf 和 Thrift 是怎么解决这两个问题的呢?
153 | **字段标号** + **限定符**(optional、required)
154 | 向后兼容:新加的字段需为 optional。这样在解析旧数据时,才不会出现字段缺失的情况。
155 | 向前兼容:字段标号不能修改,只能追加。这样旧代码在看到不认识的标号时,省略即可。
156 |
157 | ### 数据类型和模式演变
158 |
159 | 修改数据类型比较麻烦:只能够在相容类型中进行修改。
160 |
161 | 如不能将字符串修改为整形,但是可以在整形内修改:32 bit 到 64 bit 整形。
162 |
163 | ProtoBuf 没有列表类型,而有一个 repeated 类型。其好处在于**兼容数组类型**的同时,支持将可选(optional)**单值字段**,修改为**多值字段**。修改后,旧代码在看到新的多值字段时,只会使用最后一个元素。
164 |
165 | Thrift 列表类型虽然没这个灵活性,但是可以**嵌套**呀。
166 |
167 | ## Avro
168 |
169 | Apache Avro 是 Apache Hadoop 的一个子项目,专门为数据密集型场景设计,对模式演变支持的很好。支持 **Avro IDL** 和 **JSON** 两种模式语言,前者适合人工编辑,后者适合机器读取。
170 |
171 | ```protobuf
172 | record Person {
173 | string userName;
174 | union { null, long } favoriteNumber = null;
175 | array interests;
176 | }
177 | ```
178 |
179 | ```json
180 | {
181 | "type": "record",
182 | "name": "Person",
183 | "fields": [
184 | { "name": "userName", "type": "string" },
185 | { "name": "favoriteNumber", "type": ["null", "long"], "default": null },
186 | { "name": "interests", "type": { "type": "array", "items": "string" } }
187 | ]
188 | }
189 | ```
190 |
191 | 可以看到 Avro 没有使用字段标号。
192 |
193 | - 仍是编码之前例子,Avro 只用了 32 个字节,为什么呢?
194 | 他没有编入类型。
195 |
196 | 
197 |
198 | 因此,Avro 必须配合模式定义来解析,如 Client-Server 在通信的握手阶段会先交换数据模式。
199 |
200 | ### 写入模式和读取模式
201 |
202 | - 没有字段标号,Avro 如何支持模式演进呢?
203 | 答案是**显式的**使用两种模式。
204 |
205 | 即,在对数据进行编码(写入文件或者进行传输)时,使用模式 A,称为**写入模式**(writer schema);在对数据进行解码(从文件或者网络读取)时,使用模式 B,称为**读取模式**(reader schema),而两者不必相同,只需兼容。
206 |
207 | 也就是说,只要模式在演进时,是**兼容**的,那么 Avro 就能够处理向后兼容和向前兼容。
208 |
209 | **向后兼容**:新代码读取旧数据。即读取时首先得到旧数据的写入模式(即旧模式),然后将其与读取模式(即新模式)对比,得到转换映射,即可拿着此映射去解析旧数据。
210 |
211 | **向前兼容**:旧代码读取新数据。原理类似,只不过是需要得到一个逆向映射。
212 |
213 | 在由写入模式到读取模式建立映射时有一些规则:
214 |
215 | 1. **使用字段名来进行匹配**。因此写入模式和读取模式字段名顺序不一样无所谓。
216 | 2. **忽略多出的字段**。
217 | 3. **对缺少字段填默认值**。
218 |
219 | 
220 |
221 | ### 模式演化规则
222 |
223 | - 那么如何保证写入模式的兼容呢?
224 | 1. 在增删字段时,只能添加或删除具有默认值的字段。
225 | 2. 在更改字段类型时,需要 Avro 支持相应的类型转换。
226 |
227 | Avro 没有像 ProtoBuf、Thrift 那样的 optional 和 required 限定符,是通过 union 的方式,里指定默认值,甚至多种类型:
228 |
229 | ```c
230 | union {null, long, string} field;
231 | ```
232 |
233 | **注**:默认值必须是联合的第一个分支的类型。
234 |
235 | 更改字段名和在 union 中添加类型,都是向后兼容,但是不能向前兼容的,想想为什么?
236 |
237 | ### 如何从编码中获取写入模式
238 |
239 | 对于一段给定的 Avro 编码数据,Reader 如何从其中获得其对应的写入模式?
240 |
241 | 这取决于不同的应用场景。
242 |
243 | - **所有数据条目同构的大文件**
244 | 典型的就是 Hadoop 生态中。如果一个大文件所有记录都使用相同模式编码,则在文件头包含一次写入模式即可。
245 | - **支持模式变更的数据库表**
246 | 由于数据库表允许模式修改,其中的行可能写入于不同模式阶段。对于这种情况,可以在编码时额外记录一个模式版本号(比如自增),然后在某个地方存储所有的模式版本。
247 | 解码时,通过版本去查询对应的写入模式即可。
248 | - **网络中发送数据**
249 | 在两个进程通信的握手阶段,交换写入模式。比如在一个 session 开始时交换模式,然后在整个 session 生命周期内都用此模式。
250 |
251 | ### 动态生成数据中的模式
252 |
253 | Avro 没有使用字段标号的一个好处是,不需要手动维护字段标号到字段名的映射,这对于动态生成的数据模式很友好。
254 |
255 | 书中给的例子是对数据库做导出备份,注意和数据库本身使用 Avro 编码不是一个范畴,此处是指导出的数据使用 Avro 编码。
256 |
257 | 在数据库表模式发生改变前后,Avro 只需要在导出时依据当时的模式,做相应的转换,生成相应的模式数据即可。但如果使用 PB,则需要自己处理多个备份文件中,字段标号到字段名称的映射关系。其本质在于,Avro 的数据模式可以和数据存在一块,但是 ProtoBuf 的数据模式只能体现在生成的代码中,需要手动维护新旧版本备份数据与 PB 生成的代码间的映射。
258 |
259 | ### 代码生成和动态语言
260 |
261 | Thrift 和 Protobuf 会依据语言无关的 IDL 定义的模式,生成给定语言的编解码的代码。这对静态语言很有用,因为它允许利用 IDE 和编译器进行类型检查,并且能够提高编解码效率。
262 |
263 | **上述思路本质上在于,将模式内化到了生成的代码中。**
264 |
265 | 但对于动态语言,或者说解释型语言,如 JavaScript、Ruby 或 Python,由于没有了编译期检查,生成代码的意义没那么大,反而会有一定的冗余。这时 Avro 这种支持不生成代码的框架就节省一些,它可以将模式写入数据文件,读取时利用 Avro 进行动态解析即可。
266 |
267 | ## 模式的优点
268 |
269 | **模式的本质是显式类型约束,即,先有模式,才能有数据。**
270 |
271 | 相比于没有任何类型约束的文本编码 JSON,XML 和 CSV,Protocol Buffers,Thrift 和 Avro 这些基于显式定义二进制编码优点有:
272 |
273 | 1. 省去字段名,从而更加紧凑。
274 | 2. 模式是数据的注释或者文档,并且总是最新的。
275 | 3. 数据模式允许不读取数据,仅比对模式来做低成本的兼容性检查。
276 | 4. 对于静态类型来说,可以利用代码生成做编译时的类型检查。
277 |
278 | 模式演化 vs 读时模式
279 |
280 | # 几种数据流模型
281 |
282 | 数据可以以很多种形式从一个系统流向另一个系统,但不变的是,流动时都需要编码与解码。
283 |
284 | 在数据流动时,会涉及编解码双方模式匹配问题,上一小节已经讨论,本小节主要探讨几种进程间典型的数据流方式:
285 |
286 | 1. 通过数据库
287 | 2. 通过服务调用
288 | 3. 通过异步消息传递
289 |
290 | ## 经由数据库的数据流
291 |
292 | 访问数据库的程序,可能:
293 |
294 | 1. **只由同一个进程访问**。则数据库可以理解为该进程向将来发送数据的中介。
295 | 2. **由多个进程访问**。则多个进程可能有的是旧版本,有的是新版本,此时数据库需要考虑向前和向后兼容的问题。
296 |
297 | 还有一种比较棘手的情况:在某个时刻,你给一个表增加了一个字段,较新的代码写入带有该字段的行,之后又被较旧的代码覆盖成缺少该字段的行。这时候就会出现一个问题:我们更新了一个字段 A,更新完后,却发现字段 B 没了。
298 |
299 | 
300 |
301 | ### 不同时间写入的数据
302 |
303 | 对于应用程序,可能很短时间就可以由旧版本替换为新版本。但是对于数据,旧版本的代码写入的数据量,经年累月,可能很大。在变更了模式之后,由于这些旧模式的数据量很大,全部更新对齐到新版本的代价很高。
304 |
305 | 这种情况我们称之为:**数据的生命周期超过了其对应代码的生命周期**。
306 |
307 | 在读取时,数据库一般会对缺少对应列的旧数据:
308 |
309 | 1. 填充新版本字段的**默认值**(default value)
310 | 2. 如果没有默认值则填充**空值**(nullable)
311 |
312 | 后返回给用户。一般来说,在更改模式时(比如 alter table),数据库不允许增加既没有默认值、也不允许为空的列。
313 |
314 | ### 存储归档
315 |
316 | 有时候需要对数据库做备份到外存。在做备份(或者说快照)时,虽然会有不同时间点生成的数据,但通常会将各种版本数据转化、对齐到最新版本。毕竟,总是要全盘拷贝数据,那就顺便做下转换好了。
317 |
318 | 之前也提到了,对于这种场景,生成的是一次性的不可变的备份或者快照数据,使用 Avro 比较合适。此时也是一个很好地契机,可以将数据按需要的格式输出,比如面向分析的按列存储格式:[Parquet](https://parquet.apache.org/docs/file-format/ 'Parquet')。
319 |
320 | ## 经由服务的数据流:REST 和 RPC
321 |
322 | 通过网络通信时,通常涉及两种角色:服务器(server)和客户端(client)。
323 |
324 | 通常来说,暴露于公网的多为 HTTP 服务,而 RPC 服务常在内部使用。
325 |
326 | 服务器也可以同时是客户端:
327 |
328 | 1. 作为客户端访问数据库。
329 | 2. 作为客户端访问其他服务。
330 |
331 | 对于后者,是因为我们常把一个大的服务拆成一组功能独立、相对解耦的服务,这就是 **面向服务的架构(service-oriented architecture,SOA)**,或者最近比较火的**微服务架构(micro-services architecture)**。这两者有一些不同,但这里不再展开。
332 |
333 | 服务在某种程度上和数据库类似:允许客户端以某种方式存储和查询数据。但不同的是,数据库通常提供某种灵活的查询语言,而服务只能提供相对死板的 API。
334 |
335 | ### web 服务
336 |
337 | 当服务使用 HTTP 作为通信协议时,我们通常将其称为 **web 服务**。但其并不局限于 web,还包括:
338 |
339 | 1. 用户终端(如移动终端)通过 HTTP 向服务器请求。
340 | 2. 同组织内的一个服务向另一个服务发送 HTTP 请求(微服务架构,其中的一些组件有时被称为中间件)。
341 | 3. 不同组织的服务进行数据交换。一般要通过某种手段进行验证,比如 OAuth。
342 |
343 | 有两种设计 HTTP API 的方法:REST 和 SOAP。
344 |
345 | 1. **REST 并不是一种协议,而是一种设计哲学**。它强调简单的 API 格式,使用 URL 来标识资源,使用 HTTP 的动作(GET、POST、PUT、DELETE)来对资源进行增删改查。由于其简洁风格,越来越受欢迎。
346 | 2. SOAP 是基于 XML 的协议。虽然使用 HTTP,但目的在于独立于 HTTP。现在提的比较少了。
347 |
348 | ### RPC 面临的问题
349 |
350 | RPC 想让调用远端服务像调用本地(同进程中)函数一样自然,虽然设想比较好、现在用的也比较多,但也存在一些问题:
351 |
352 | 1. 本地函数调用要么成功、要么不成功。但是 RPC 由于经过网络,可能会有各种复杂情况,比如请求丢失、响应丢失、hang 住以至于超时等等。因此,可能需要重试。
353 | 2. 如果重试,需要考虑**幂等性**问题。因为上一次的请求可能已经到达了服务端,只是请求没有成功返回。那么多次调用远端函数,就要保证不会造成额外副作用。
354 | 3. 远端调用延迟不可用,受网络影响较大。
355 | 4. 客户端与服务端使用的编程语言可能不同,但如果有些类型不是两种语言都有,就会出一些问题。
356 |
357 | REST 相比 RPC 的好处在于,它不试图隐去网络,更为显式,让使用者不易忽视网络的影响。
358 |
359 | ### RPC 当前方向
360 |
361 | 尽管有上述问题,但其实在工程中,大部分情况下,上述情况都在容忍范围内:
362 |
363 | 1. 比如局域网的网络通常比较快速、可控。
364 | 2. 多次调用,使用幂等性来解决。
365 | 3. 跨语言,可以使用 RPC 框架的 IDL 来解决。
366 |
367 | 但 RPC 程序需要考虑上面提到的极端情况,否则可能会偶然出一个很难预料的 BUG。
368 |
369 | 另外,基于二进制编码的 RPC 通常比基于 HTTP 服务效率更高。但 HTTP 服务,或者更具体一点,RESTful API 的好处在于,生态好、有大量的工具支持。而 RPC 的 API 通常和 RPC 框架生成的代码高度相关,因此很难在不同组织中无痛交换和升级。
370 |
371 | 因此,如本节开头所说:暴露于公网的多为 HTTP 服务,而 RPC 服务常在内部使用。
372 |
373 | ### 数据编码和 RPC 的演化
374 |
375 | 通过服务的数据流通常可以假设:所有的服务端先更新,然后客户端再更新。因此,只需要在请求里考虑后向兼容性,在响应中考虑前向兼容性:
376 |
377 | 1. Thrift、gRPC(Protobuf)和 Avro RPC 可以根据编码格式的兼容性规则进行演变。
378 | 2. RESTful API 通常使用 JSON 作为请求响应的格式,JSON 比较容易添加新的字段来进行演进和兼容。
379 | 3. SOAP 按下不表。
380 |
381 | 对于 RPC,服务的兼容性比较困难,因为一旦 RPC 服务的 SDK 提供出去之后,你就无法对其生命周期进行控制:总有用户因为各种原因,不会进行主动升级。因此可能需要长期保持兼容性,或者提前通知和不断预告,或者维护多个版本 SDK 并逐渐对早期版本进行淘汰。
382 |
383 | 对于 RESTful API,常用的兼容方法是,将版本号做到 URL 或者 HTTP 请求头中。
384 |
385 | ## 经由消息传递的数据流
386 |
387 | 前面研究了编码解码的不同方式:
388 |
389 | 1. 数据库:一个进程写入(编码),将来一个进程读取(解码)
390 | 2. RPC 和 REST:一个进程通过网络(发送前会编码)向另一个进程发送请求(收到后会解码)并同步等待响应。
391 |
392 | 本节研究介于数据库和 RPC 间的**异步消息系统**:一个存储(消息 broker、消息队列来临时存储消息)+ 两次 RPC(生产者一次,消费者一次)。
393 |
394 | 与 RPC 相比,使用消息队列的优点:
395 |
396 | 1. 如果消费者暂时不可用,可以充当暂存系统。
397 | 2. 当消费者宕机重启后,自动地重新发送消息。
398 | 3. 生产者不必知道消费者 IP 和端口。
399 | 4. 能将一条消息发送给多个消费者。
400 | 5. 将生产者和消费者解耦。
401 |
402 | ### 消息队列
403 |
404 | 书中用的是**消息代理**(Message Broker),但另一个名字,消息队列,可能更为大家熟知,因此,本小节之后行文都用消息队列。
405 |
406 | 过去,消息队列为大厂所垄断。但近年来,开源的消息队列越来越多,可以适应不同场景,如 RabbitMQ、ActiveMQ、HornetQ、NATS 和 Apache Kafka 等等。
407 |
408 | 消息队列的**送达保证**因实现和配置而异,包括:
409 |
410 | 1. **最少一次(at-least-once)**:同一条数据可能会送达多次给消费者。
411 | 2. **最多一次(at-most-once)**:同一条数据最多会送达一次给消费者,有可能丢失。
412 | 3. **严格一次(exactly-once)**:同一条数据保证会送达一次,且最多一次给消费者。
413 |
414 | 消息队列的逻辑抽象叫做 **Queue** 或者 **Topic**,常用的消费方式两种:
415 |
416 | 1. 多个消费者互斥消费一个 Topic
417 | 2. 每个消费者独占一个 Topic
418 |
419 | **注**:我们有时会区分这两个概念:将点对点的互斥消费称为 Queue,多点对多点的发布订阅称为 Topic,但这并不通用,或者说没有形成共识。
420 |
421 | 一个 Topic 提供一个单向数据流,但可以组合多个 Topic,形成复杂的数据流拓扑。
422 |
423 | 消息队列通常是面向**字节数组**的,因此你可以将消息按任意格式进行编码。如果编码是前后向兼容的,同一个主题的消息格式,便可以进行灵活演进。
424 |
425 | ### 分布式的 Actor 框架
426 |
427 | **Actor 模型**是一种基于消息传递的并发编程模型。Actor 通常是由状态(State)、行为(Behavior)和信箱(MailBox,可以认为是一个消息队列)三部分组成:
428 |
429 | 1. 状态:Actor 中包含的状态信息。
430 | 2. 行为:Actor 中对状态的计算逻辑。
431 | 3. 信箱:Actor 接受到的消息缓存地。
432 |
433 | 由于 Actor 和外界交互都是通过消息,因此本身可以并行的,且不需要加锁。
434 |
435 | 分布式的 Actor 框架,本质上是将消息队列和 actor 编程模型集成到一块。自然,在 Actor 滚动升级是,也需要考虑前后向兼容问题。
436 |
--------------------------------------------------------------------------------
/ch05.md:
--------------------------------------------------------------------------------
1 | # DDIA 逐章精读(五): 冗余(Replication)
2 |
3 | 本书第一部分讲单机数据系统,第二部分讲多机数据系统。
4 |
5 | **冗余(Replication)** 是指将同一份数据复制多份,放到通过网络互联的多个机器上去。其好处有:
6 |
7 | 1. **降低延迟**:可以在地理上同时接近不同地区的用户。
8 | 2. **提高可用性**:当系统部分故障时仍然能够正常提供服务。
9 | 3. **提高读吞吐**:平滑扩展可用于查询的机器。
10 |
11 | > 本章假设我们的数据系统中所有数据能够存放到一台机器中,则本章只需考虑多机冗余的问题。如果数据超过单机尺度该怎么办?那是下一章要解决的事情。
12 |
13 | 如果数据是**只读**的,则冗余很好做,直接复制到多机即可。我们有时可以利用这个特性,使用分治策略,将数据分为只读部分和读写部分,则只读部分的冗余就会容易处理的多,甚至可以用 [EC](https://zh.wikipedia.org/wiki/%E7%BA%A0%E9%94%99%E7%A0%81) 方式做冗余,减小存储放大的同时,还提高了可用性。
14 |
15 | - 想想 EC 牺牲了什么?
16 | 以计算换存储。
17 |
18 | 但难点就在于,数据允许数据变更时,如何维护多机冗余且一致。常用的冗余控制算法有:
19 |
20 | 1. 单领导者(single leader)
21 | 2. 多领导者(multi-leader)
22 | 3. 无领导者(leaderless)
23 |
24 | 这需要在多方面做取舍:
25 |
26 | 1. 使用同步复制还是异步复制
27 | 2. 如何处理失败的副本
28 |
29 | 数据库冗余问题在学术界不是一个新问题了,但在工业界,大部分人都是新手——分布式数据库是近些年才大规模的在工业界落地的。
30 |
31 | # 领导者与跟随者
32 |
33 | 冗余存储的每份数据称为**副本**(replica)。多副本所带来的最主要的一个问题是:如何保证所有数据被同步到了所有副本上?
34 |
35 | 基于**领导者(leader-based)** 的同步算法,是最常用解决办法。
36 |
37 | 1. 其中一个副本称为**领导者**(leader),别称**主副本**(primary、master)。主副本作为写入的协调者,所有写入都要发给主副本。
38 | 2. 其他副本称为**跟随者**(follower),也称为**只读副本**(read replicas)、**从副本**(slaves)、**次副本**(secondaries)、**热备**(hot-standby)。主副本将改动写到本地后,将其发送给各个从副本,从副本收变动到后应用到自己状态机,这个过程称为**日志同步**(replication log)、**变更流**(change steam)。
39 | 3. 对于读取,客户端可以从主副本和从副本中读取;但写入,客户端只能将请求发到主副本。
40 |
41 | 
42 |
43 | 根据我的习惯,下面通称主副本和从副本。
44 |
45 | 有很多**数据系统**都用了此模式:
46 |
47 | 1. **关系型数据库**:PostgreSQL(9.0+)、MySQL 和 Oracle Data Guard 和 SQL Server 的 AlwaysOn
48 | 2. **非关系型数据库**:MonogoDB、RethinkDB 和 Espresso
49 | 3. **消息队列**:Kafka 和 RabbitMQ。
50 |
51 | ## 同步复制和异步复制
52 |
53 | **同步(synchronously)复制**和**异步(asynchronously)复制**和关键区别在于:请求何时返回给客户端。
54 |
55 | 1. 如果等待某副本写完成后,则该副本为同步复制。
56 | 2. 如果不等待某副本写完成,则该副本为异步复制。
57 |
58 | 
59 |
60 | 两者的对比如下:
61 |
62 | 1. 同步复制牺牲了**响应延迟**和**部分可用性**(在某些副本有问题时不能完成写入操作),换取了所有副本的一致性(但并不能严格保证)。
63 | 2. 异步复制放松了**一致性**,而换来了较低的写入延迟和较高的可用性。
64 |
65 | 在实践中,会根据对一致性和可用性的要求,进行取舍。针对所有从副本来说,可以有以下选择:
66 |
67 | 1. **全同步**:所有的从副本都同步写入。如果副本数过多,可能性能较差,当然也可以做并行化、流水线化处理。
68 | 2. **半同步**:(**semi-synchronous**),有一些副本为同步,另一些副本为异步。
69 | 3. **全异步**:所有的从副本都异步写入。网络环境比较好的话,可以这么配置。
70 |
71 | > 异步复制可能会造成副本丢失等严重问题,为了能兼顾一致性和性能,学术界也在不断研究新的复制方法。如,**链式复制(chain-replication)**。
72 |
73 | > 多副本的一致性和共识性有诸多联系,本书后面章节会讨论。
74 |
75 | ## 新增副本
76 |
77 | 在很多情况下,需要给现有系统新增副本。
78 |
79 | 如果原副本是只读(read-only)的,只需要简单拷贝即可。但是如果是可写副本,则问题要复杂很多。因此,比较简单的一种解决方法是:禁止写入,然后拷贝。这在某些情况下很有用,比如夜间没有写入流量,同时一晚上肯定能复制完。
80 |
81 | 如果要不停机,可以:
82 |
83 | 1. 主副本在本地做**一致性**快照。何谓一致性?
84 | 2. 将快照复制到从副本节点。
85 | 3. 从主副本拉取快照之后的操作日志,应用到从副本。如何知道快照与其后日志的对应关系?序列号。
86 | 4. 当从副本赶上主副本进度后,就可以正常跟随主副本了。
87 |
88 | 这个过程一般是自动化的,比如 Raft 中;当然也可以手动化,比如写一些脚本。
89 |
90 | ## 宕机处理
91 |
92 | 系统中任何节点都可能在计划内或者计划外宕机。那么如何应对这些宕机情况,保持整个系统的可用性呢?
93 |
94 | ### **从副本宕机:追赶恢复**。
95 |
96 | 类似于新增从副本。如果落后的多,可以直接向主副本拉取快照 + 日志;如果落后的少,可以仅拉取缺失日志。
97 |
98 | ### **主副本宕机:故障转移。**
99 |
100 | 处理相对麻烦,首先要选出新的主副本,然后要通知所有客户端主副本变更。具体来说,包含下面步骤:
101 |
102 | 1. **确认主副本故障**。要防止由于网络抖动造成的误判。一般会用心跳探活,并设置合理超时(timeout)阈值,超过阈值后没有收到该节点心跳,则认为该节点故障。
103 | 2. **选择新的主副本**。新的主副本可以通过**选举**(共识问题)或者**指定**(外部控制程序)来产生。选主时,要保证备选节点数据尽可能的新,以最小化数据损失。
104 | 3. **让系统感知新主副本**。系统其他参与方,包括从副本、客户端和旧主副本。前两者不多说,旧主副本在恢复时,需要通过某种手段,让其知道已经失去领导权,避免**脑裂**。
105 |
106 | 主副本切换时,会遇到很多问题:
107 |
108 | 1. **新老主副本数据冲突**。新主副本在上位前没有同步完所有日志,旧主副本恢复后,可能会发现和新主副本数据冲突。
109 | 2. **相关外部系统冲突**。即新主副本,和使用该副本数据的外部系统冲突。书中举了 github 数据库 MySQL 和缓存系统 redis 冲突的例子。
110 | 3. **新老主副本角色冲突**。即新老主副本都以为自己才是主副本,称为**脑裂(split brain)**。如果他们两个都能接受写入,且没有冲突解决机制,数据会丢失或者损坏。有的系统会在检测到脑裂后,关闭其中一个副本,但设计的不好可能将两个主副本都关闭调。
111 | 4. **超时阈值选取**。如果超时阈值选取的过小,在不稳定的网络环境中(或者主副本负载过高)可能会造成主副本频繁的切换;如果选取过大,则不能及时进行故障切换,且恢复时间也增长,从而造成服务长时间不可用。
112 |
113 | 所有上述问题,在不同需求、不同环境、不同时间点,都可能会有不同的解决方案。因此在系统上线初期,不少运维团队更愿意手动进行切换;等积累一定经验后,再进行逐步自动化。
114 |
115 | 节点故障;不可靠网络;在一致性、持久化、可用性和延迟间的取舍;等等问题,都是设计分布式系统时,所面临的的基本问题。根据实际情况,对这些问题进行艺术化的取舍,便是分布式系统之美。
116 |
117 | ## 日志复制
118 |
119 | 在数据库中,基于领导者的多副本是如何实现的?在不同层次有多种方法,包括:
120 |
121 | 1. **语句层面的复制。**
122 | 2. **预写日志的复制**。
123 | 3. **逻辑日志的复制**。
124 | 4. **触发器的复制**。
125 |
126 | 对于一个**系统**来说,多副本同步的是什么?**增量修改**。
127 |
128 | 具体到一个由数据库构成的**数据系统**,通常由数据库外部的**应用层**、数据库内部**查询层**和**存储层**组成。**修改**在查询层表现为:语句;在存储层表现为:存储引擎相关的预写日志、存储引擎无关的逻辑日志;修改完成后,在应用层表现为:触发器逻辑。
129 |
130 | ### 基于语句的复制
131 |
132 | 主副本记录下所有更新语句:`INSERT`、`UPDATE` 或 `DELETE` 然后发给从库。主副本在这里类似于充当其他从副本的**伪客户端**。
133 |
134 | 但这种方法有一些问题:
135 |
136 | 1. **非确定性函数(nondeterministic)** 的语句可能会在不同副本造成不同改动。如 NOW()、RAND()
137 | 2. **使用自增列,或依赖于现有数据**。则不同用户的语句需要完全按相同顺序执行,当有并发事务时,可能会造成不同的执行顺序,进而导致副本不一致。
138 | 3. **有副作用**(触发器、存储过程、UDF)的语句,可能不同副本由于上下文不同,产生的副作用不一样。除非副作用是确定的输出。
139 |
140 | 当然也有解决办法:
141 |
142 | 1. 识别所有产生非确定性结果的语句。
143 | 2. 对于这些语句同步值而非语句。
144 |
145 | 但是 Corner Case 实在太多,步骤 1 需要考虑的情况太多。
146 |
147 | ### 传输预写日志(WAL)
148 |
149 | 我们发现主流的存储引擎都有**预写日志**(WAL,为了宕机恢复):
150 |
151 | 1. 对于日志流派(LSM-Tree,如 LevelDB),每次修改先写入 log 文件,防止写入 MemTable 中的数据丢失。
152 | 2. 对于原地更新流派(B+ Tree),每次修改先写入 WAL,以进行崩溃恢复。
153 |
154 | 所有用户层面的改动,最终都要作为状态落到存储引擎里,而存储引擎通常会维护一个:
155 |
156 | 1. 追加写入
157 | 2. 可重放
158 |
159 | 这种结构,天然适合备份同步。本质是因为磁盘的读写特点和网络类似:**磁盘是顺序写比较高效,网络是只支持流式写**。具体来说,主副本在写入 WAL 时,会同时通过网络发送对应的日志给所有从副本。
160 |
161 | 书中提到一个数据库版本升级的问题:
162 |
163 | 1. 如果允许旧版本代码给新版本代码(应该会自然做到后向兼容)发送日志(前向兼容)。则在升级时可以先升级从库,再切换升级主库。
164 | 2. 否则,只能进行停机升级软件版本。
165 |
166 | ### 逻辑日志复制(基于行)
167 |
168 | 为了和具体的存储引擎物理格式解耦,在做数据同步时,可以使用不同的日志格式:**逻辑日志**。
169 |
170 | 对于关系型数据库来说,行是一个合适的粒度:
171 |
172 | 1. **对于插入行**:日志需包含所有列值。
173 | 2. **对于删除行**:日志需要包含待删除行标识,可以是主键,也可以是其他任何可以唯一标识行的信息。
174 | 3. **对于更新行**:日志需要包含待更新行的标志,以及所有列值(至少是要更新的列值)
175 |
176 | 对于多行修改来说,比如事务,可以在修改之后增加一条事务提交的记录。MySQL 的 binlog 就是这么干的。
177 |
178 | 使用逻辑日志的**好处**有:
179 |
180 | 1. 方便新旧版本的代码兼容,更好的进行滚动升级。
181 | 2. 允许不同副本使用不同的存储引擎。
182 | 3. 允许导出变动做各种**变换**。如导出到数据仓库进行离线分析、建立索引、增加缓存等等。
183 |
184 | 之前分析过一种基于日志,统一各种数据系统的[文章](https://zhuanlan.zhihu.com/p/458683164),很有意思。
185 |
186 | ### 基于触发器的复制
187 |
188 | 前面所说方法,都是在**数据库内部**对数据进行多副本同步。
189 |
190 | 但有些情况下,可能需要用户决策,如何对数据进行复制:
191 |
192 | 1. 对需要复制的数据进行过滤,只复制一个子集。
193 | 2. 将数据从一种数据库复制到另外一种数据库。
194 |
195 | 有些数据库如 Oracle 会提供一些工具。但对于另外一些数据库,可以使用**触发器和存储过程**。即,将用户代码 hook 到数据库中去执行。
196 |
197 | 基于触发器的复制,性能较差且更易出错;但是给了用户更多的灵活性。
198 |
199 | # 复制滞后问题
200 |
201 | 如前所述,使用多副本的好处有:
202 |
203 | 1. **可用性**:容忍部分节点故障
204 | 2. **可伸缩性**:增加读副本处理更多读请求
205 | 3. **低延迟**:让用户选择一个就近的副本访问
206 |
207 | ### **引出**
208 |
209 | 对于读多写少的场景,想象中,可以通过使劲增加读副本来均摊流量。但有个**隐含**的条件是,多副本间的同步得做成**异步**的,否则,读副本一多,某些副本就很容易出故障,进而阻塞写入。
210 |
211 | 但若是异步复制,就会引入不一致问题:某些副本进度落后于主副本。
212 |
213 | 如果此时不再有写入,经过一段时间后,多副本最终会达到一致:**最终一致性**。
214 |
215 | 在实际中,网络通常比较快,**副本滞后(replication lag)**不太久,也即这个\*最终**\*通常**不会太久,比如 ms 级别,最多 s 级别。但是,对于分布式系统,谁都不敢打包票,由于网络分区、机器高负载等等软硬件问题,在极端情况下,这个*最终*可能会非常久。
216 |
217 | > 总之,**最终**是一个非常不精确的限定词。
218 |
219 | 对于这种最终一致的系统,在工程中,要考虑到由于副本滞后所带来的一致性问题。
220 |
221 | ## 读你所写
222 |
223 | 
224 |
225 | 上图问题在于,在一个**异步复制**的分布式数据库里,同一个客户端,写入**主副本**后返回;稍后再去读一个落后的**从副本**,就会发现:读不到自己刚写的内容!
226 |
227 | 为了避免这种反直觉的事情发生,我们引入一种新的一致性:**读写一致性(read-after-write consistency)**,或者 **读你所写一致性(read-your-writes consistency)**。
228 |
229 | 若数据库提供这种一致性保证,对于**单个客户端**来说,就一定能够读到其所写变动。也即,这种一致性是从**单个客户端**角度来看的一种因果一致性。
230 |
231 | 那么如何提供这种保证,或者说,实现这种一致性呢?列举几种方案:
232 |
233 | 1. **按内容分类**。对于客户端可能修改的内容集,**只从主副本读取**。如社交网络上的个人资料,读自己的资料时,从主副本读取;但读其他人资料时,可以向从副本读。
234 | 2. **按时间分类**。如果每个客户端都能访问基本所有数据,则方案一就会退化成所有数据都要从主副本读取,这显然不可接受。此时,可以按时间分情况讨论,近期内有过改动的数据,从主副本读,其他的,向从副本读。那这个区分是否最近的**时间阈值**(比如一分钟)如何选取呢?可以监控从副本一段时间内的最大延迟这个经验值,来设置。
235 | 3. **利用时间戳**。客户端记下本客户端上次改动时的时间戳,在读从副本时,利用此时间戳来看某个从副本是否已经同步了改时间戳之前内容。可以在所有副本中找到一个已同步了的;或者阻塞等待某个副本同步到改时间戳后再读取。时间戳可以是逻辑时间戳,也可以是物理时间戳(此时多机时钟同步非常重要)。
236 |
237 | 会有一些实际的复杂 case:
238 |
239 | 1. **数据分布在多个物理中心**。所有需要发送给主副本的请求都要首先路由到主副本所在的数据中心。
240 | 2. **一个逻辑用户有多个物理客户端**。比如一个用户通过电脑、手机多终端同时访问,此时就不能用设备 id,而需要使用用户 id,来保证用户角度的读写一致性。但不同设备有不同物理时间戳,不同设备访问时可能会路由到不同数据中心。
241 |
242 | ## 单调读
243 |
244 | 异步复制可能带来的另外一个问题:对于一个客户端来说,系统可能会发生**时光倒流(moving backward in time)**。
245 |
246 | 
247 |
248 | 于是,我们再引入一种一致性保证:**单调读(Monotonic reads)**。
249 |
250 | - 读写一致性和单调读有什么区别?
251 | 写后读保证的是写后读顺序,单调读保证的是**多次读**之间的顺序。
252 |
253 | 如何实现单调读?
254 |
255 | 1. 只从一个副本读数据。
256 | 2. 前面提到的时间戳机制。
257 |
258 | ## 一致前缀读
259 |
260 | 
261 |
262 | 异步复制所带来的第三个问题:有时候会违反因果关系。
263 |
264 | 本质在于:如果数据库由多个分区(Partition)组成,而分区间的事件顺序无法保证。此时,如果有因果关系的两个事件落在了不同分区,则有可能会出现**果在前,因在后**。
265 |
266 | 为了防止这种问题,我们又引入了一种一致性:**一致前缀读(consistent prefix reads)**。奇怪的名字。
267 |
268 | 实现这种一致性保证的方法:
269 |
270 | 1. 不分区。
271 | 2. 让所有有因果关系的事件路由到一个分区。
272 |
273 | 但如何追踪因果关系是个难题。
274 |
275 | ## 副本滞后的终极解决方案
276 |
277 | 事务!
278 |
279 | 多副本异步复制所带来的一致性问题,都可以通过**事务(transaction)** 来解决。单机事务已经存在了很长时间,但在数据库走向分布式时代,一开始很多 NoSQL 系统抛弃了事务。
280 |
281 | - 这是为什么?
282 | 1. 更容易的实现。2. 更好的性能。3. 更好的可用性。
283 |
284 | 于是复杂度被转移到了应用层。
285 |
286 | 这是数据库系统刚大规模步入分布式(**多副本、多分区**)时代的一种妥协,在经验积累的够多之后,事务必然会被引回。
287 |
288 | 于是近年来越来越多的分布式数据库开始支持事务,是为**分布式事务**。
289 |
290 | # 多主模型
291 |
292 | **单主模型一个最大问题**:所有写入都要经过它,如果由于任何原因,客户端无法连接到主副本,就无法向数据库写入。
293 |
294 | 于是自然产生一种想法:多主行不行?
295 |
296 | **多主复制(multi-leader replication)**:有多个可以接受写入的主副本,每个主副本在接收到写入之后,都要转给所有其他副本。即一个系统,有多个**写入点**。
297 |
298 | ## 多主模型应用场景
299 |
300 | 单个数据中心,多主模型意义不大:复杂度超过了收益。总体而言,由于一致性等问题,多主模型应用场景较少,但有一些场景,很适合多主:
301 |
302 | 1. 数据库横跨多个数据中心
303 | 2. 需要离线工作的客户端
304 | 3. 协同编辑
305 |
306 | ### 多个数据中心
307 |
308 | 假设一个数据库的副本,横跨多个数据中心,如果使用单主模型,在写入时的延迟会很大。那么每个数据中心能不能各配一个主副本?
309 |
310 | 
311 |
312 | 单主和多主,在多数据中心场景下的对比:
313 |
314 | | 对比项 | 单主模型 | 多主模型 |
315 | | ------ | ------------------------------------------ | ------------------------------------ |
316 | | 性能 | 所有写入都要路由到一个数据中心 | 写入可以就近 |
317 | | 可用性 | 主副本所在数据中心故障,需要有个切主的过程 | 每个数据中心可以独立工作 |
318 | | 网络 | 跨数据中心,写入对网络抖动更敏感 | 数据中心间异步复制,对公网容错性更高 |
319 |
320 | 但是多主模型在一致性方面有很大缺陷:如果两个数据中心同时修改同样的数据,必须合理解决写冲突。另外,对于数据库来说,多主很难保证一些自增主键、触发器和完整性约束的一致性。因此在工程实践中,多主用的相对较少。
321 |
322 | ### 离线工作的客户端
323 |
324 | 离线工作的一个应用的多个设备上的客户端,如果也允许继续写入数据。如:日历应用。在电脑上和手机上离线时如果也支持添加日程。则在各个设备联网时,需要互相同步数据。
325 |
326 | 则离线后还继续工作的多个副本,本质上就是一个多主模型:每个主都可以独立的写入数据,然后在网络连通后解决冲突。
327 |
328 | 但,如何支持离线后正常地工作,联网后优雅的解决冲突,是一个难题。
329 |
330 | Apache CouchDB 的一个特点便是支持多主模型。
331 |
332 | ### 协同编辑
333 |
334 | Google Docs 等类似 SaaS 模式的在线协同应用越来越流行。
335 |
336 | 这种应用允许多人在线同时编辑文档或者电子表格,其背后的原理,与上一节离线工作的客户端很像。
337 |
338 | 为了实现协同,并解决冲突,可以:
339 |
340 | 1. **悲观方式**。加锁以避免冲突,但粒度需要尽可能小,否则无法允许多人同时编辑一个文档。
341 | 2. **乐观方式**。允许每个用户无脑写入,然后如果有冲突,交由用户解决。
342 |
343 | git 也是一个类似的协议。
344 |
345 | ## 处理写入冲突
346 |
347 | 多主模型最大的问题是:如何解决冲突。
348 |
349 | 
350 |
351 | 考虑 wiki 一个页面标题的修改:
352 |
353 | 1. 用户 1 将该页面标题从 A 修改到 B
354 | 2. 用户 2 将该页面标题从 A 修改到 C
355 |
356 | 两个操作在本地都修改成功,然后**异步同步**时,会出现冲突。
357 |
358 | ### 冲突检测
359 |
360 | **有同步**或者**异步**的方式进行冲突检测。
361 |
362 | 对于**单主模型**,当检测到冲突时,由于只有一个主副本,可以同步的检测冲突,从而解决冲突:
363 |
364 | 1. 让第二个写入阻塞,直到第一个写完成。
365 | 2. 让第二个写入失败,进行重试。
366 |
367 | 但对于**多主模型**,两个写入可能会在不同主副本立即成功。然后异步同步时,发现冲突,但为时已晚(没有办法简单决定如何解决冲突)。
368 |
369 | 虽然,可以在多主间使用同步方式写入所有副本后,再返回请求给客户端。但这会失掉多主模型的主要优点:**允许多个主副本独立接受写入**。此时,蜕化成单主模型。
370 |
371 | ### 冲突避免
372 |
373 | **解决冲突最好的方式是在设计上避免冲突**。
374 |
375 | 由于多主模型在冲突发生后再去解决会有很大的复杂度,因此常使用冲突避免的设计。
376 |
377 | 假设你的数据集可以分成多个分区,让不同分区的主副本放在不同数据中心中,那么从任何一个分区的角度来看,变成了单主模型。
378 |
379 | 举个栗子:对于服务全球用户的应用,每个用户就近固定路由到附近的数据中心。则,每个用户信息都有唯一的主副本。
380 |
381 | 但如果:
382 |
383 | 1. 用户从一个地点迁移到了另一个地点
384 | 2. 某个数据中心损坏,导致路由变化
385 |
386 | 就会对该设计提出一些挑战。
387 |
388 | ### 冲突收敛
389 |
390 | 在单主模型中,所有事件比较容易进行**定序**,因此我们总可以用后一个写入覆盖前一个写入。
391 |
392 | 但在多主模型中,很多冲突无法定序:**从每个主副本来看,事件顺序是不一致的**,并且没有哪个更权威一些,那么就无法让所有副本最终**收敛(convergent)**。
393 |
394 | 此时,我们就需要一些规则,来让其收敛:
395 |
396 | 1. **给每个写入一个序号,并且后者胜**。本质上是使用外部系统对所有事件进行定序。但可能会产生数据丢失。举个例子,对于一个账户,原有 10 元,客户端 A - 8,客户端 B - 3,任何一个单独成功都有问题。
397 | 2. **给每个副本一个序号,序号更高的副本有更高的优先级**。这也会造成低序号副本的数据丢失。
398 | 3. **提供一种自动的合并冲突的方式**。如,假设结果是字符串,则可以将其排序后,使用连接符进行链接,如在之前 Wiki 的冲突中,合并后的标题为“B/C”
399 | 4. **使用程序定制一种保留所有冲突值信息的冲突解决策略**。也可以将这个定制权,交给用户。
400 |
401 | ### 自定义解决
402 |
403 | 由于只有用户知道数据本身的信息,因此较好的方式是,将如何解决冲突交给用户。即,允许用户编写回调代码,提供冲突解决逻。该回调可以在:
404 |
405 | 1. **写时执行**。在写入时发现冲突,调用回调代码,解决冲突后写入。这些代码通常在后台执行,并且不能阻塞,因此不能在调用时同步的通知用户。但打个日志之类的还是可以的。
406 | 2. **读时执行**。在写入冲突时,所有冲突都会被保留(如使用多版本)。下次读取时,系统会将所有数据本版本返回给用户,进行交互式的或者自动的解决冲突,并将结果写回系统。
407 |
408 | 上述冲突解决只限于单个记录、行、文档层面。
409 |
410 | TODO(自动冲突解决)
411 |
412 | ### 界定冲突
413 |
414 | 有些冲突显而易见:并发写同一个 Key。
415 |
416 | 有些冲突则更隐晦,考虑一个会议室预定系统。预定同一个会议室不一定会发生冲突,只有预定时间段有交叠,才会有冲突。
417 |
418 | ## 多主复制拓扑
419 |
420 | **复制拓扑**(replication topology)描述了数据写入从一个节点到另一个节点的传播路径。
421 |
422 | 在只有两个主副本时,拓扑是确定的,如图 5-7。Leader1 和 Leader 都得把数据发给对方。但随着副本数的增多,数据复制拓扑就会有多种选择,如下图:
423 |
424 | 
425 |
426 | 上图表示了 ≥ 4 个主副本时,常见的复制拓扑:
427 |
428 | 1. **环形拓扑**。通信跳数少,但是在转发时需要带上拓扑中前驱节点信息。如果一个节点故障,则可能中断复制链路。
429 | 2. **星型拓扑**。中心节点负责接受并转发数据。如果中心节点故障,则会使得整个拓扑瘫痪。
430 | 3. **全连接拓扑**。每个主库都要把数据发给剩余主库。通信链路冗余度较高,能较好的容错。
431 |
432 | 对于环形拓扑和星型拓扑,为了防止广播风暴,需要对每个节点打上一个唯一标志(ID),在收到他人发来的自己的数据时,及时丢弃并终止传播。
433 |
434 | 全连接拓扑也有自己问题:**尤其是所有复制链路速度不一致时**。考虑下面一个例子:
435 |
436 | 
437 |
438 | 两个有因果依赖的(先插入,后更新)的语句,在复制到 Leader 2 时,由于速度不同,导致其接收到的数据违反了因果一致性。
439 |
440 | 要想对这些写入事件进行全局排序,仅用每个 Leader 的物理时钟是不够的,因为物理时钟:
441 |
442 | 1. 可能不能够充分同步
443 | 2. 同步时可能会发生回退
444 |
445 | 可以用一种叫做**版本向量(version vectors)** 的策略,对多个副本的事件进行排序,解决因果一致性问题。下一节会详细讨论。
446 |
447 | 最后忠告:如果你要使用基于多主模型的系统,一定要知晓上面提到的问题,多做测试,确保其提供的保证符合你的使用场景。
448 |
449 | # 无主模型
450 |
451 | 有主模型中,由主副本决定写入顺序,从副本在写入上不直接和客户端打交道,只是重放其对应的主副本的写入顺序(也可以理解为主副本为从副本的客户端)。
452 |
453 | 而无主模型,则允许任何副本接受写入。
454 |
455 | 在关系数据库时代,无主模型已经快被忘却。从 Amazon 的 Dynamo 论文开始,无主模型又重新大放异彩,Riak,Cassandra 和 Voldemort 都受其启发,可以统称为 **Dynamo 流(Dynamo-style)**。
456 |
457 | > 奇特的是,Amazon 的一款数据库产品 DynamoDB,和 Dynamo 并不是一个东西。
458 |
459 | 通常来说,在无主模型中,写入时可以:
460 |
461 | 1. 由客户端直接写入副本。
462 | 2. 由**协调者(coordinator)** 接收写入,转发给多副本。但与主副本不同,协调者并不负责定序。
463 |
464 | ## 有节点故障时的写入
465 |
466 | 基于主副本(leader-based)的模型,在有副本故障时,需要进行故障切换。
467 |
468 | 但在无主模型中,简单忽略它就行。
469 |
470 | 
471 |
472 | 多数派写入,多数派读取,以及读时修复。
473 |
474 | 由于写入时,简单的忽略了宕机副本;在读取时,就要多做些事情了:**同时读取多个副本,选取最新*版本*的值**。
475 |
476 | ### 读时修复和反熵
477 |
478 | 无主模型也需要维持多个副本数据的一致性。在某些节点宕机重启后,如何让其弥补错过的数据?
479 |
480 | Dynamo 流派的存储中通常有两种机制:
481 |
482 | 1. **读时修复(read repair)**,本质上是一种捎带修复,在读取时发现旧的就顺手修了。
483 | 2. **反熵过程(Anti-entropy process)**,本质上是一种兜底修复,读时修复不可能覆盖所有过期数据,因此需要一些后台进程,持续进行扫描,寻找陈旧数据,然后更新。[这个博文](https://www.influxdata.com/blog/eventual-consistency-anti-entropy/)对该词有展开描述。
484 |
485 | ### Quorum 读写
486 |
487 | 如果副本总数为 n,写入 w 个副本才认定写入成功,并且在查询时最少需要读取 r 个节点。只要满足 w + r > n,我们就能读到最新的数据(**鸽巢原理**)。此时 r 和 w 的值称为 **quorum 读写**。即这个约束是保证数据有效所需的最低(法定)票数。
488 |
489 | 
490 |
491 | 在 Dynamo 流派的存储中,n、r 和 w 通常是可以配置的:
492 |
493 | 1. n 越大冗余度就越高,也就越可靠。
494 | 2. r 和 w 都常都选择超过半数,如 `(n+1)/2`
495 | 3. w = n 时,可以让 r = 1。此时是牺牲写入性能换来读取性能。
496 |
497 | 考量满足 w+r > n 系统对节点故障的容忍性:
498 |
499 | 1. 如果 w < n,则有节点不可用时,仍然能正常写入。
500 | 2. 如果 r < n,则有节点不可用时,仍然能正常读取。
501 |
502 | 特化一下:
503 |
504 | 1. 如果 n = 3,r = w = 2,则系统可以容忍最多一个节点宕机。
505 | 2. 如果 n = 5,r = w = 3,则系统可以容忍最多两个节点宕机。
506 |
507 | 通常来说,我们会将读或者写并行的发到全部 n 个副本,但是只要等到法定个副本的结果,就可以返回。
508 |
509 | 如果由于某种原因,可用节点数少于 r 或者 w,则读取或者写入就会出错。
510 |
511 | ## quorum 一致性的局限
512 |
513 | 由于 w + r > n 时,总会至少有一个节点(读写子集至少有一个节点的交集)保存了最新的数据,因此总是期望能读到最新的。
514 |
515 | 当 w + r ≤ n 时,则很可能会读到过期的数据。
516 |
517 | 但在 w + r > n 时,有一些边角情况(corner case),也会导致客户端读不到最新数据:
518 |
519 | 1. 使用宽松的 Quorum 时(n 台机器范围可以发生变化),w 和 r 可能并没有交集。
520 | 2. 对于写入并发,如果处理冲突不当时。比如使用 last-win 策略,根据本地时间戳挑选时,可能由于时钟偏差造成数据丢失。
521 | 3. 对于读写并发,写操作仅在部分节点成功就被读取,此时不能确定应当返回新值还是旧值。
522 | 4. 如果写入节点数 < w 导致写入失败,但并没有对数据进行回滚时,客户端读取时,仍然会读到旧的数据。
523 | 5. 虽然写入时,成功节点数 > w,但中间有故障造成了一些副本宕机,导致成功副本数 < w,则在读取时可能会出现问题。
524 | 6. 即使都正常工作,也有可能出现一些关于时序(timing)的边角情况。
525 |
526 | 因此,虽然 Quorum 读写看起来能够保证返回最新值,但在工程实践中,有很多细节需要处理。
527 |
528 | 如果数据库不遵守之前副本滞后小节引入的几个一致性保障,前面提到的异常仍然可能会发生。
529 |
530 | ### 一致性监控
531 |
532 | 对副本数据陈旧性监控,能够让你了解副本的健康情况,当其落后太多时,可以及时调查原因。
533 |
534 | 基于领导者的多副本模型,由于每个副本复制顺序一致,则可以方便的给出每个副本的落后(lag)进度。
535 |
536 | 但对于无主模型,由于没有固定写入顺序,副本的落后进度变得难以界定。如果系统只使用读时修复策略,则对于一个副本的落后程度是没有限制的。读取频率很低数据可能版本很老。
537 |
538 | 最终一致性是一种很模糊的保证,但通过监控能够量化“最终”(比如到一个阈值),也是很棒的。
539 |
540 | ## 放松的 Quorum 和提示转交
541 |
542 | 正常的 Quorum 能够容忍一些副本节点的宕机。但在大型集群(总节点数目 > n)中,可能最初选中的 n 台机器,由于种种原因(宕机、网络问题),导致无法达到法定读写数目,则此时有两种选择:
543 |
544 | 1. 对于所有无法达到 r 或 w 个法定数目的读写,直接报错。
545 | 2. 仍然接受写入,并且将新的写入暂时交给一些正常节点。
546 |
547 | 后者被认为是一种**宽松的法定数目** (**sloppy quorum**):写和读仍然需要 w 和 r 个成功返回,但是其所在节点集合可以发生变化。
548 |
549 | 
550 |
551 | 一旦问题得到解决,数据将会根据线索移回其应该在的节点(D—> B),我们称之为**提示移交**(hinted handoff)。这个移交过程是由反熵 anti-entropy 后台进程完成的。
552 |
553 | 这是一种典型的牺牲部分一致性,换取更高可用性的做法。在常见的 Dynamo 实现中,放松的法定人数是可选的。在 Riak 中,它们默认是启用的,而在 Cassandra 和 Voldemort 中它们默认是禁用的
554 |
555 | ### 多数据中心
556 |
557 | 无主模型也适用于系统多数据中心部署。
558 |
559 | 为了同时兼顾**多数据中心**和**写入的低延迟**,有一些不同的基于无主模型的多数据中心的策略:
560 |
561 | 1. 其中 Cassandra 和 Voldemort 将 n 配置到所有数据中心,但写入时只等待本数据中心副本完成就可以返回。
562 | 2. Riak 将 n 限制在一个数据中心内,因此所有客户端到存储节点的通信可以限制到单个数据中心内,而数据复制在后台异步进行。
563 |
564 | ## 并发写入检测
565 |
566 | 由于 Dynamo 允许多个客户端并发写入相同 Key,则即使使用严格的 Quorum 读写,也会产生冲突:**对于时间间隔很短(并发)的相同 key 两个写入,不同副本上收到的顺序可能不一致**。
567 |
568 | 此外,读时修复和提示移交时,也可能产生冲突。
569 |
570 | 
571 |
572 | 如上图,如果每个节点不去检查顺序,而是简单的接受写入请求,就落到本地,不同副本间可能就会出现永久不一致:上图 Node1 和 Node3 上副本 X 的值是 A,Node2 上副本 X 的值是 B。
573 |
574 | 为了使所有副本最终一致,需要有一种手段来解决并发冲突。
575 |
576 | ### 后者胜(Last-Write-Win)
577 |
578 | 后者胜(LWW,last write wins)的策略是,通过某种手段确定一种全局唯一的顺序,然后让后面的修改覆盖之前的修改。
579 |
580 | 如,为所有写入附加一个全局时间戳,如果对于某个 key 的写入有冲突,可以挑选具有最大时间戳的数据保留,并丢弃较早时间戳的写入。
581 |
582 | LWW 有一个问题,就是多个并发写入的客户端,可能都认为自己成功了,但是最终只有一个值被保留了,其他都在后台被丢弃了。即,其迅速再读,会发现不是自己写入的数据。
583 |
584 | 使用 LWW 唯一安全的方法是:key 是一次可写,后变为只读。如 Cassandra 建议使用一个 UUID 作为主键,则每个写操作都只会有一个唯一的键。
585 |
586 | ### 发生于之前(Happens-before)和并发关系
587 |
588 | 考虑之前的两个图:
589 |
590 | 1. 在 5-9 中,由于 client B 的更新依赖于 client A 的插入,因此他们是因果关系。
591 | 2. 在 5-12 中,set X = A 和 set X = B 是并发的,因为他们都互相不知道对方存在,也不存在因果关系。
592 |
593 | 系统中任意的两个写入 A 和 B,只可能存在三种关系:
594 |
595 | 1. A happens before B
596 | 2. B happens before A
597 | 3. A B 并发
598 |
599 | 从另外一个角度来说(集合运算),
600 |
601 | ```c
602 | A 和 B 并发 < === > A 不 happens-before B && B 不 happens-before A
603 | ```
604 |
605 | 如果两个操作可以定序,则 last write win;如果两个操作并发,则需要进行冲突解决。
606 |
607 | > 并发、时间和相对性
608 |
609 | > Lamport 时钟相关论文中有详细推导相关概念关系。为了定义并发,事件发生的绝对时间先后并不重要,只要两个事件都意识不到对方的存在,则称两个操作“并发”。从狭义相对论上来说,只要两个事件发生的时间差,小于光在两者距离传播所用时间,则两个事件不可能互相影响。推广到计算机网络中,只要由于网络问题导致,在事件发生时间差内,两者不能互相意识到,则称其是并发的。
610 |
611 | ### 确定 Happens-Before 关系
612 |
613 | 我们可以用某种算法来确定系统中任意两个事件,是否存在 happens-before 关系,还是并发关系。以一个两个 client 并发添加购物车例子来看:
614 |
615 | 
616 |
617 | 需要注意:
618 |
619 | 1. 不会主动读取,只有主动写入,通过写入的返回值读取数据库当前状态。
620 | 2. 客户端下一次写入,**依赖于**(因果关系)**本客户端**上一次写入后获取的返回值。
621 | 3. 对于并发,数据库不会覆盖,而是保留多个**并发值**(每个 client 一个)。
622 |
623 | 上图中的数据流,如下图所示。箭头表示 happens-before 关系。本例中,客户端永远没办法完全获知服务器数据,因为总有另外的客户端进行并发操作。但是旧版本的值会被覆盖,并且不会丢失写入。
624 |
625 | 
626 |
627 | 总结下,该算法如下:
628 |
629 | 1. 服务器为每个键分配一个版本号 V,每次该键有写入时,将 V + 1,并将版本号与写入的值一块保存。
630 | 2. 当客户端读取该键时,服务器将返回所有未被覆盖的值以及最新的版本号。
631 | 3. 客户端在进行下次写入时,必须**包含**之前读到的版本号 Vx(说明基于哪个版本进行新的写入),并将读取的值合并到一块。
632 | 4. 当服务器收到特定版本号 Vx 的写入时,可以用其值覆盖所有 V ≤ Vx 的值。、
633 |
634 | 如果又来一个新的写入,不基于任何版本号,则该写入不会覆盖任何内容。
635 |
636 | ### 合并并发值
637 |
638 | 该算法可以保证所有数据都不会被无声的丢弃。但,需要客户端在随后写入时合并之前的值来清理多个值。如果简单基于时间戳进行 LWW,则有些数据又会被丢掉。
639 |
640 | 因此需要根据实际情况,选择一些策略来解决冲突,合并数据。
641 |
642 | 1. 对于上述购物车中只增加物品的例子,可以使用“并集”来合并冲突数据。
643 | 2. 如果购物车汇总还有删除操作,就不能简单并了,但是可以将删除变为增加(写一个 tombstone 标记)。
644 |
645 | ### 版本向量
646 |
647 | 上面例子只有单个副本。将该算法扩展到无主多副本模型时,只使用一个版本值显然不够,这时需要给每个副本的键都引入版本号,对于同一个键来说,不同副本的版本会构成**版本向量(version vector)**。
648 |
649 | ```c
650 | key1
651 | A Va
652 | B Vb
653 | C Vc
654 |
655 | key1: [Va, Vb, Vc]
656 |
657 | [Va-x, Vb-y, Vc-z] <= [Va-x1, Vb-y1, Vc-z1] <==>
658 | x <= x1 && y <= y1 && z <= z1
659 | ```
660 |
661 | 每个副本在遇到写入时,会增加对应键的版本号,同时跟踪从其他副本中看到的版本号,通过比较版本号大小,来决定哪些值要覆盖哪些值要保留。
662 |
--------------------------------------------------------------------------------
/ch06.md:
--------------------------------------------------------------------------------
1 | # DDIA 逐章精读(六): 分区(Partition)
2 |
3 | 上一章主要讲复制,本章转向分片。这是两个相对正交但勾连的两个概念:
4 |
5 | 1. **分片(Partition)**:解决数据集尺度与单机容量、负载不匹配的问题,分片之后可以利用多机容量和负载。
6 | 2. **复制(Replication**):系统机器一多,单机故障概率便增大,为了防止数据丢失以及服务高可用,需要做多副本。
7 |
8 | > 分片,Partition,有很多别称。通用的有 Shard;具体到实际系统,HBase 中叫 Region,Bigtable 中叫 tablet,等等。本质上是对数据集的一种逻辑划分,后面行文,分片和分区可能混用,且有时为名词,有时为动词。
9 |
10 | 通常来说,数据系统在分布式系统中会有三级划分:数据集(如 Database、Bucket)——分片(Partition)——数据条目(Row、KV)。通常,每个分片只属于一个数据集,每个数据条目只属于一个分片。单个分片,就像一个小点的数据库。但是,跨分区的操作的,就要复杂的多。
11 |
12 | 本章首先会介绍数据集**切分的方法**,并讨论索引和分片的配合;然后将会讨论分片**再平衡**(rebalancing),集群节点增删会引起数据再平衡;最后,会探讨数据库如何将请求**路由**到相应的分片并执行。
13 |
14 | # 分片和复制
15 |
16 | 分片通常和复制结合使用。每个分片有多个副本,可以分散到多机上去(更泛化一点:多个容错阈);同时,每个机器含有多个分片,但通常不会有一个分片的两个副本放到一个机器上。
17 |
18 | 如果使用多副本使用主从模型,则分片、副本、机器关系如下:
19 |
20 | 1. 从一个分片的角度看,主副本在一个机器上,从副本们在另外机器上。
21 | 2. 从一个机器的角度看,既有一些主副本分片,也有一些从副本分片。实践中,也会尽量保证主副本在集群中均匀分布,避免过多的集中到一台机器上。想想为什么?
22 |
23 | 
24 |
25 | 由于分区方式和复制策略相对正交,本章会暂时忽略复制策略(在上章讲过),专注分析分区方式。
26 |
27 | # 键值对集的分片
28 |
29 | 键值对是数据的一种最通用、泛化的表示,其他种类数据库都可以转化为键值对表示:
30 |
31 | 1. 关系型数据库,primary key → row
32 | 2. 文档型数据库,document id → document
33 | 3. 图数据库,vertex id → vertex props, edge id → edge props
34 |
35 | 因此,接下来主要针对键值对集合的分区方式,则其他数据库在构建存储层时,可以首先转化为 KV 对,然后进行分区。
36 |
37 | **分片(Partition)** 的本质是对数据集合的划分。但在实践中,可以细分为两个步骤:
38 |
39 | 1. 对数据集进行**逻辑**划分
40 | 2. 将逻辑分片调度到**物理**节点
41 |
42 | 因此,在分片时,有一些基本要求:
43 |
44 | 1. 分片过程中,要保证每个分片的数据量多少尽量均匀,否则会有**数据偏斜**(**skew**),甚而形成**数据热点**。
45 | 2. 分片后,需要保存路由信息,给一个 KV 条目,能知道去**哪个**机器上去查;稍差一些,可以知道去**哪几个**机器上去找;最差的,如果需要去所有机器逐一查询,但性能一般不可接受。
46 |
47 | 这两条是互相依赖和制约的。比如说,假设分片数目确定,为了分片均匀,每来一条数据,我们可以等概率随机选择一个分片;但在查询每个数据条目时,就得去所有机器上都查一遍。
48 |
49 | 保存所有数据条目路由信息,有三种常用的策略:
50 |
51 | 1. 通过某种固定规则,比如哈希,算出一个位置。
52 | 2. 使用内存,保存所有数据条目到机器的映射。
53 | 3. 结合两种,首先通过规则算出到逻辑分片的映射,然后通过内存保存逻辑分片到物理节点的映射。
54 |
55 | 本节主要讨论根据数据条目(Data Item)算出逻辑分区(Partition),常见的有两种方式:按键范围分区,按键哈希分区。
56 |
57 | ## 按键范围(Key Range)分区
58 |
59 | 对于 KV 数据来说,Key 通常会有个定义域,且在定义域内可(按某种维度)排序。则,将该连续的定义域进行切分,保存每个切分的上下界,在给出某个 Key 时,就能通过比较,定位其所在分区。
60 |
61 | 如,百科全书系列,通常是按照名词的字母序来分册的,每个分册可理解为该系列的一个分区,查阅时,可根据字母排序来首先找到所在分册,再使用分册目录查阅。图书馆图书的索引编号也是类似道理。
62 |
63 | 
64 |
65 | 由于键并不一定在定义域内均匀分布,因此简单按照定义域等分,并不能将数据等分。因此,需要按照数据的分布,动态调整分区的界限,保证分区间数据大致均匀。这个调整的过程,既可以手动完成,也可以自动进行。
66 |
67 | 按键范围分区好处在于可以进行**快速的范围查询(Rang Query)**。如,某个应用是保存传感器数据,并将时间戳作为键进行分区,则可轻松获取一段时间内(如某年,某月)的数据。
68 |
69 | 但坏处在于,数据分散不均匀,且容易造成热点。可能需要动态的调整的分区边界,以维护分片的相对均匀。
70 |
71 | 仍以传感器数据存储为例,以时间戳为 Key,按天的粒度进行分区,所有最新写入都被路由到最后一个分区节点,造成严重的写入倾斜,不能充分利用所有机器的写入带宽。一个解决办法是**分级**或者**混合**,使用拼接主键,如使用传感器名称 + 时间戳作为主键,则可以将同时写入的多个传感器的数据分散到多机上去。
72 |
73 | ## 按键散列(Hash)分区
74 |
75 | 为了避免数据倾斜和读写热点,许多数据系统使用散列函数对键进行分区。
76 |
77 | 因此,选择散列函数的**依据**是,使得数据散列尽量均匀:即给定一个 Key,经过散列函数后,以等概率在哈希区间(如 `[0, 2^32-1)`)内产生一个值。即使原 Key 相似,他的散列值也能均匀分布。
78 |
79 | 而加密并不在考虑之列,因此并不需要多么复杂的加密算法,如,Cassandra 和 MongoDB 使用 MD5,Voldemort 使用 Fowler-Noll-Vo 函数。
80 |
81 | 选定哈希函数后,将原 Key 定义域映射到新的散列值阈,而散列值是均匀的,因此可以对散列值阈按给定分区数进行等分。
82 |
83 | 
84 |
85 | 还有一种常提的哈希方法叫做[一致性哈希](https://zh.m.wikipedia.org/zh-hans/%E4%B8%80%E8%87%B4%E5%93%88%E5%B8%8C) 。其特点是,会考虑逻辑分片和物理拓扑,将数据和物理节点按同样的哈希函数进行哈希,来决定如何将哈希分片路由到不同机器上。它可以避免在内存中维护**逻辑分片到物理节点的映射**,而是每次计算出来。即用一套算法同时解决了我们在最初提出的逻辑分片和物理路由的两个问题。比较经典的数据系统,[Amazon Dynamo](https://www.qtmuniao.com/2020/06/13/dynamo/) 就用了这种方式。
86 |
87 | 
88 |
89 | 如果不使用一致性哈希,我们需要在元数据节点中,维护逻辑分片到物理节点的映射。则在某些物理节点宕机后,需要调整该映射并手动进行数据迁移,而不能像一致性哈希一样,半自动的增量式迁移。
90 |
91 | 哈希分片在获取均匀散列能力的同时,也丧失了基于键高效的范围查询能力。如书中说,MongoDB 中选择基于哈希的分区方式,范围查询就要发送到所有分区节点;Riak、Couchbase 或 Voldmort 干脆不支持主键的上的范围查询。
92 |
93 | 一种折中方式,和上小节一样,使用组合的方式,先散列,再顺序。如使用主键进行散列得到分区,在每个分区内使用其他列顺序存储。如在社交网络上,首先按 user_id 进行散列分区,再使用 update_time 对用户事件进行顺序排序,则可以通过 (user_id, update_timestamp) 高效查询某个用户一段事件的事件。
94 |
95 | 小结一下,两种分区方式区别在于,一个使用应用相关值( `Key` )分区,一个使用应用无关值(`Hash(key)`)分区,前者支持高效范围查询,后者可以均摊负载。但可使用多个字段,组合使用两种方式,使用一个字段进行分区,使用另一个字段在分区内进行排序,兼取两者优点。
96 |
97 | ## 负载偏斜和热点消除
98 |
99 | 在数据层,可以通过哈希将数据均匀散列,以期将对数据的请求均摊;但如果在应用层,不同数据条目的负载本就有倾斜,存在对某些键的热点。那么仅在数据层哈希,就不能起到消除热点的作用。
100 |
101 | 如在社交网络中的大 V,其发布的信息,天然会引起同一个键(假设键是用户 id)大量数据的写入,因为可能会有针对该用户信息的大量评论和互动。
102 |
103 | 此时,就只能在应用层进行热点消除,如可以用拼接主键,对这些大 V 用户主键进行“分身”,即在用户主键开始或者结尾添加一个随机数,两个十进制后缀就可以增加 100 中拆分可能。但这无疑需要应用层做额外的工作,请求时需要进行拆分,返回时需要进行合并。
104 |
105 | 可能之后能开发出检测热点,自动拆分合并分区,以消除倾斜和热点。
106 |
107 | # 分片和次级索引
108 |
109 | **次级索引(secondary index)**,即主键以外的列的索引;由于分区都是基于主键的,在针对有分区的数据建立次级索引时,就会遇到一些困难。
110 |
111 | 关于次级索引,举个例子,对于某个用户表(id, name, age, company),我们按用户 id(如身份证)对所有用户数据进行分区。但我们常常会根据名字对用户进行查询,为了加快查询,于是需要基于 name 字段,建立次级索引。
112 |
113 | 在关系型和文档型数据库中,次级索引很常见。在 KV 存储中,为了降低实现复杂度,一般不支持。但大部分场景,因为我们不可能只按单一维度对数据进行检索,因此次级索引很有用。尤其对于搜索场景,比如 Solr 和 Elasticsearch,次级索引(在搜索领域称为**倒排索引**)更是其实现基石。
114 |
115 | 在有分区的数据中,常见的建立次级索引的方法有:
116 |
117 | 1. 本地索引(local index),书中又称 document-based index
118 | 2. 全局索引(global index),书中又称 term-based index
119 |
120 | > 注:书中给的 document-based、term-based 两个名词(包括 document 和 term)是从搜索中来的。由于搜索中都是 term→ document id list 的映射,document-based 是指按 document id 进行分区,每个分区存的索引都是本地的 document ids,而不管其他分区,因此是本地索引,查询时需要发到所有分区逐个查询。term-based 是指按 term 进行分区,则每个倒排索引都是存的全局的 document id list,因此查询的时候只需要去 term 所在分区查询即可。
121 |
122 | ## 本地索引
123 |
124 | 书中举了一个维护汽车信息数据例子:每种汽车信息由 (id, color, make, location) 四元组构成。首先会根据其主键 id 进行分区,其次为了方便查询,需要对汽车颜色(color)和制造商(make)字段(文档数据库中称为**field,字段**;关系型数据库中称为 **column,列**,图数据库中称为 **property,属性**)建立次级索引。
125 |
126 | 次级索引会对每个数据条目建立一个索引条目,这给数据库的实现带来了一些问题:
127 |
128 | 1. 当数据库已有数据时,建立索引,何时针对存量数据构建索引。
129 | 2. 当数据库中数据条目发生更改时,如何维护数据和索引的一致性,尤其是多客户端并发修改时。
130 |
131 | 
132 |
133 | 本地**索引(local index)**,就是对每个数据分区独立地建立次级索引,即,次级索引只针对本分区数据,而不关心其他分区数据。本地索引的**优点**是维护方便,在更新数据时,只需要在该分区所在机器同时更新索引即可。但**缺点**是,查询效率相对较低,所有基于索引的查询请求,都要发送到所有分区,并将结果合并,该过程也称为 **scatter/gather** 。但即使用多分区并发(而非顺序)进行索引查询优化,也仍然容易在某些机器上发生**长尾请求**(由于机器负载过高或者网络问题,导致改请求返回特别慢,称为长尾请求),导致整个请求过程变慢。
134 |
135 | 但由于实现简单,本地索引被广泛使用,如 MongoDB,Riak,Cassandra,Elasticsearch,SolrCloud 和 VoltDB 都使用本地索引。
136 |
137 | ## 全局索引
138 |
139 | 为了避免查询索引时将请求发到所有分区,可以建立**全局索引**,即每个次级索引条目都是针对全局数据。但为了避免索引查询热点,我们会将索引数据本身也分片,分散到多个机器上。
140 |
141 | 
142 |
143 | 当然,与数据本身一样,对于索引进行分区,也可基于 Range 或基于 Hash,同样也是各有优劣(面向扫描还是均匀散列)。
144 |
145 | 全局索引能避免索引查询时的 scatter/gather 操作,但维护起来较为复杂,因为每个数据的插入,可能会影响多个索引分区(基于该数据不同字段可能会有多个二级索引)。因此,为了避免增加写入延迟,在实践中,全局索引多为异步更新。但由此会带来短暂(有时可能会比较长)的数据和索引不一致。如果想要保证强一致性,需要引入跨分区的分布式事务(实现复杂度高,且会带来较大的性能损耗),但并不是所有数据库都支持。
146 |
147 | # 分片均衡
148 |
149 | 数据库在运行过程中,数据和机器都会发生一些变化:
150 |
151 | 1. 查询吞吐增加,需要增加机器以应对增加的负载。
152 | 2. 数据集变大,需要增加磁盘和 RAM 来存储增加数据。
153 | 3. 有机器故障,需要其他机器来接管故障机器数据。
154 |
155 | 所有这些问题都会引起数据分片在节点间的迁移,我们将之称为:**均衡(rebalancing)**。对于 rebalancing 我们期望:
156 |
157 | 1. 均衡后负载(存储、读写)在节点间均匀分布
158 | 2. 均衡时不能禁止读写,并且尽量减小影响
159 | 3. 尽量减少不必要的数据移动,尽量降低网络和磁盘 IO
160 |
161 | ## 均衡策略
162 |
163 | 分区策略会影响均衡策略。比如动态分区、静态分区,对应的均衡策略就不太一样;此外,分区的粒度和数量也会影响均衡策略。
164 |
165 | ### 不要使用:hash mod N
166 |
167 | 在说如何进行均衡之前,先说下不应该怎样做。
168 |
169 | 之前提到过,分区包括**逻辑分区**和**物理调度**两个阶段,此处说的是将两者合二为一:假设集群有 N 个节点,编号 `0 ~ N-1`,一条键为 key 的数据到来后,通过 `hash(key) mod N` 得到一个编号 n,然后将该数据发送到编号为 n 的机器上去。
170 |
171 | 为什么说这种策略不好呢?因为他不能应对机器数量的变化,如果要增删节点,就会有大量的数据需要发生迁移,否则,就不能保证数据在 `hash(key) mod N` 标号的机器上。在大规模集群中,机器节点增删比较频繁,这种策略更是不可接受。
172 |
173 | ### 静态分区
174 |
175 | 静态分区,即,逻辑分区阶段的**分区数量是固定的**,并且最好让分区数量大于(比如高一个数量级)机器节点。相比**动态分区**策略(比如,允许分区分裂和合并),固定数量分区更容易实现和维护。
176 |
177 | 书中没有提,但是估计需要在类似元信息节点,维护逻辑分区到物理节点的映射,并根据此映射信息来发现不均衡,进而进行调度。
178 |
179 | 在静态分区中,让分区数量远大于机器节点的好处在于:
180 |
181 | 1. **应对将来可能的扩容**。加入分区数量等于机器数量,则将来增加机器,仅就单个数据集来说,并不能增加其存储容量和吞吐。
182 | 2. **调度粒度更细,数据更容易均衡**。举个例子,假设只有 20 个分区,然后有 9 个机器,假设每个分区数据量大致相同,则最均衡的情况,也会有两个机器数的数据量比其他机器多 50%;
183 | 3. **应对集群中的异构性**。比如集群中某些节点磁盘容量比其他机器大,则可以多分配几个分区到该机器上。
184 |
185 | 
186 |
187 | 但当然,也不能太大,因为每个分区信息也是有管理成本的:比如元信息开销、均衡调度开销等。一般来说,可以取一个你将来集群可能扩展到的最多节点数量作为初始分区数量。
188 |
189 | 对于数据量会超预期增长的数据集,静态分区策略就会让用户进退两难,已经有很多数据,重新分区代价很大,不重新分区又难以应对数据量的进一步增长。
190 |
191 | ### 动态分区
192 |
193 | 对于按键范围(key range)进行分区的策略来说,由于数据在定义域内并**不均匀分布**,如果固定分区数量,则天然地难以均衡。因此,按范围分区策略下,都会支持动态分区。按生命周期来说:
194 |
195 | 1. 开始,数据量很少,只有一个分区。
196 | 2. 随着数据量不断增长,单个分区超过一定**上界**,则按尺寸一分为二,变成两个新的分区。
197 | 3. 如果某个分区,数据删除过多,少于某个**下界**,则会和相邻分区合并(合并后超过上界怎么办?)。
198 |
199 | 动态分区好处在于,小数据量使用少量分区,减少开销;大数据量增加分区,以均摊负载。
200 |
201 | 但同时,小数据量时,如果只有一个分区,会限制写入并发。因此,工程中有些数据库支持**预分区**(pre-splitting),如 HBase 和 MongoDB,即允许在空数据库中,配置最少量的初始分区,并确定每个分区的起止键。
202 |
203 | 另外,散列分区策略也可以支持动态分区,即,在**哈希空间中**对相邻数据集进行合并和分裂。
204 |
205 | ### 与节点成比例分区
206 |
207 | 前文所述,
208 |
209 | 1. 静态均衡的分区数量一开始就固定的,但是单分区尺寸会随着总数量增大而增大。
210 | 2. 动态均衡会按着数据量多少进行动态切合,单分区尺寸相对保持不变,一直于某个设定的上下界。
211 |
212 | 但他们的分区数量都和集群节点数量没有直接关系。而另一种均衡策略,则是保持**总分区数量**和节点数量成正比,也即,保持每个节点分区数量不变。
213 |
214 | 假设集群有 m 个节点,每个节点有 n 个分区,在此种均衡策略下,当有新节点加入时,会从 m\*n 个分区中随机选择 n 个分区,将其一分为二,一半由新节点分走,另一半留在原机器上。
215 |
216 | 随机选择,很容易产生有倾斜的分割。但如果 n 比较大,如 Cassandra 默认是 256,则新节点会比较容易均摊负载。
217 |
218 | - 为什么?
219 | 是因为可以从每个节点选同样数量的分区吗?比如说 n = 256,m = 16,则可以从每个节点选 16 分区吗?
220 |
221 | 随机选择分区,要求使用基于哈希的分区策略,这也是最接近原始一致性哈希的定义的方法。(同样存疑。
222 |
223 | ## 运维:自动均衡还是手动均衡
224 |
225 | 在实践中,均衡是自动进行还是手动进行需要慎重考虑。
226 |
227 | 1. **自动进行**。系统自动检测是否均衡,然后自动决策搬迁策略以及搬迁时间。
228 | 2. **手动进行**。管理员指定迁移策略和迁移时间。
229 |
230 | 数据均衡是一项非常昂贵且易出错的操作,会给网络带来很大压力,甚至影正常负载。自动均衡诚然可以减少运维,但在实践中,如何有效甄别是否真的需要均衡(比如网络抖动了一段时间、节点宕机又重启、故障但能修复)是一个很复杂的事情,如果做出错误决策,就会带来大量无用的数据搬迁。
231 |
232 | 因此,数据均衡通常会半自动的进行,如系统通过负载情况给出搬迁策略,由管理员审核没问题后,决定某个时间段运行(避开正常流量高峰),Couchbase、Riak 和 Voldemort 便采用了类似做法。
233 |
234 | # 请求路由
235 |
236 | 在我们将分区放到节点上去后,当客户端请求到来时,我们如何决定将请求路由到哪台机器?这势必要求我们**以某种方式**记下:
237 |
238 | 1. 数据条目到逻辑分区的映射。
239 | 2. 逻辑分区到物理机器的映射。
240 |
241 | 这在我们之前已经讨论过。
242 |
243 | 其次,是在哪里记下这些路由(映射)信息,泛化一下,是一个**服务发现**(service discovery)问题。概括来说,由内而外,有几种方案:
244 |
245 | 1. **每个节点都有全局路由表**。客户端可以连接集群中任意一个节点,如该节点恰有该分区,则处理后返回;否则,根据路由信息,将其路由合适节点。
246 | 2. **由一个专门的路由层来记录**。客户端所有请求都打到路由层,路由层依据分区路由信息,将请求转发给相关节点。路由层只负责请求路由,并不处理具体逻辑。
247 | 3. **让客户端感知分区到节点映射**。客户端可以直接根据该映射,向某个节点发送请求。
248 |
249 | 
250 |
251 | 无论记在何处,都有一个重要问题:如何让相关组件(节点本身、路由层、客户端)及时感知(分区到节点)的映射变化,将请求正确的路由到相关节点?也即,如何让所有节点就路由信息快速达成一致,业界有很多做法。
252 |
253 | **依赖外部协调组件**。如 Zookeeper、Etcd,他们各自使用某种共识协议保持高可用,可以维护轻量的路由表,并提供发布订阅接口,在有路由信息更新时,让外部所有节点快速达成一致。
254 |
255 | 
256 |
257 | **使用内部元数据服务器**。如三节点的 Meta 服务器,每个节点都存储一份路由数据,使用某种共识协议达成一致。如 TiDB 的 Placement Driver。
258 |
259 | **使用某种协议点对点同步**。如 Dynamo、Cassandra 和 Riak 使用流言协议(Gossip Protocol),在集群内所有机器节点间就路由信息进行传播,并最终达成一致。
260 |
261 | 更简单一些,如 Couchbase 不支持自动的负载均衡,因此只需要使用一个路由层通过心跳从集群节点收集到所有路由信息即可。
262 |
263 | 当使用路由层(或者 Proxy 层,通常由多个实例构成),或者客户端将请求随机发动到某个集群节点时,客户端需要确定一个具体 IP 地址,但这些信息变化相对较少,因此直接使用 DNS 或者反向代理进行轮询即可。
264 |
265 | ## 并行查询执行
266 |
267 | 大部分 NoSQL 存储,所支持的查询都不太负载,如基于主键的查询、基于次级索引的 scatter/gather 查询。如前所述,都是针对单个键值非常简单的查询路由。
268 |
269 | 但对于关系型数据库产品,尤其是支持 **大规模并行处理(MPP, Massively parallel processing)** 数仓,一个查询语句在执行层要复杂的多,可能会:
270 |
271 | 1. Stage:由多个阶段组成。
272 | 2. Partition:每个阶段包含多个针对每个分区的并行的子查询计划。
273 |
274 | 数仓的大规模的快速并行执行是另一个需要专门讨论的话题,由于多用于支持 BI,因此其优化具有重要意义,本书后面第十章会专门讨论。
275 |
--------------------------------------------------------------------------------
/ch07.md:
--------------------------------------------------------------------------------
1 | # DDIA 逐章精读(七): 事务(transaction)
2 |
3 | 在分布式数据系统中,任何问题都有可能发生:
4 |
5 | 1. 系统侧:数据库系统和硬件系统任何时间都有可能发生故障
6 | 2. 应用侧:使用数据库的应用程序任何时刻都有可能故障。
7 | 3. 网络侧:应用程序和数据库间,数据库的多个节点间,随时都有可能断开连接。
8 | 4. 多个客户端:并发写入时,可能会有竞态条件和相互覆盖。
9 | 5. 半读:一个客户端可能会读到部分更新的数据库。
10 |
11 | 复杂度不会凭空消失,只会发生转移。如果数据库对这些故障不做任何处理,应用层就需要处理上述所有相关问题,会极大增加应用侧编程复杂度。**事务**,就是数据库为了解决类似的问题,所提供的一种保证,以**简化应用层的编程模型**。
12 |
13 | 简单来说,**事务**就是将一组语句(或者说操作)打包成一个逻辑单元进行执行,并提供一种保证,这一组操作要么全部成功( **commit**,应用到数据库里),要么全部失败(被动 **abort**,或者主动 **rollback**),而不会存在只执行了一半的中间状态。此外,如果多个客户端的事务并发执行,会涉及到隔离性的问题,一般来说,数据库允许用户在隔离级别和性能之间做选择。
14 |
15 | 也可以从**时间**和**空间**两个角度来理解事务,从生命周期(时间)来讲,事务要保证一组操作的整体性;从并发控制(空间),事务要做好多个事务间的并发控制。
16 |
17 | 有了这种语义上的保证,用户在发现事务失败后,可以放心的进行重试,直到成功,就可以确定事务中的所有操作都生效了。但任何便利性都是有代价的,事务便是在一定程度上牺牲了性能和可用性。
18 |
19 | 本章首先讨论事务的基本概念,然后针对隔离级别(并发控制)做了详细探讨,包括读已提交、快照隔离和可串行化。事务保证和是否分布式在概念上相对正交,但在实现上,分布式系统中事务的实现难度要大的多。
20 |
21 | # 棘手的概念
22 |
23 | 一般来说,现代关系型数据库和一些非关系型数据库,所支持的事务,大多都遵循第一个 SQL 数据库—— IBM System R 所引入的规范。
24 |
25 | 但近些年来,NoSQL 的发展对事务的概念造成了一些冲击。2000 年后,为了支持大规模分布式数据的存储,NoSQL 引入了分区、冗余,同时部分放弃了对原事务的完整支持,但通过重新定义”事务“,仍然号称支持事务。或者为了商业的宣传,引入相近的名词。
26 |
27 | 于是出现了两种极端的观点,一是认为事务与可伸缩性不可兼得,大型分布式系统都必须放弃事务支持以保持高可用性和高性能;一是提出事务是保证高要求的应用不丢数据的必要条件。
28 |
29 | 两种观点都有失偏颇,和任何技术一样,事务有其优点和局限性,为了理解事务背后相关权衡,有必要探究下事务的详细内涵。
30 |
31 | ## ACID 的含义
32 |
33 | 说到事务,大家第一反应是 Theo Härder 和 Andreas Reuter 于 1983 提出的 ACID。虽然 ACID 最初提出是为了为数据库中的容错保证给出一种相对精确的描述,但不同数据库对 ACID 的支持并不相同,尤其是 Isolation —— 隔离性。如今,ACID 更多的沦为一个 PR 术语。
34 |
35 | 下面,将逐一探究 Atomicity、Consistency、Isolation 和 Durability 的精确含义,以此来对事务所要做出的保证建立一个基本的认识。
36 |
37 | ### 原子性(Atomicity)
38 |
39 | “原子”一般指最小单位,不可再分。在并发编程中,原子性通常和可见性关联,即一个线程无法看到另一个线程执行的原子操作的中间结果。
40 |
41 | 但,ACID 的隔离性是描述**多个客户端**并发的所需要解决的问题,而原子性更多的是描述**单个客户端**\线程内,一组操作可以被原子的执行,如果执行到一半失败,已经执行的操作可以被全部回滚。
42 |
43 | 因此,ACID 中原子性所提供的保证是:**在发生错误时,会回滚该事务所有已经写入的变更**。
44 |
45 | 这个保证很重要,否则用户在执行到一半出错时,很难知道哪些操作已经生效、哪些操作尚未生效。有了此保证,用户如果发现出错,可以安全的进行重试。
46 |
47 | ### 一致性(Consistency)
48 |
49 | **一致性**,是一个被广泛使用的词,在不同上下文中,有不同含义:
50 |
51 | 1. **多副本**。第五章讨论了多副本一致性,以及最终一致性的问题。
52 | 2. **一致性哈希**。是一种分区和调度的方式,在增删机器后,可以较小代价的进行副本迁移和均衡。
53 | 3. **CAP 定理**。一致性指的是线性一致性,是多副本间一致性的一种特例。
54 | 4. **ACID**。数据库在应用程序的视角处于某种”一致性的状态“。
55 |
56 | 因此,我们使用该术语时,一定要明确其所属上下文,进而明确其含义。具体到 ACID 中,一致性是对某些**不变性(invariants)** 的维持,所谓不变性,即某些约束条件。如,在银行账户中,在任何时刻,账户余额须等于收入减去支出。
57 |
58 | 不同于 ACID 中其他性质,一致性是需要**应用侧**和**数据库侧**共同维护的:
59 |
60 | 1. 应用侧需要写入满足应用侧视角约束要求的数据。
61 | 2. 数据库侧需要保证多次写入前后,尤其是遇到问题时,维持该约束。
62 |
63 | 因此,一致性可以表述为,应用侧依赖数据库提供的原子性、隔离性来实现一致性。可见,一致性并非数据库事务本身的一种特性,更多的是应用侧的一种属性。据此,乔・海勒斯坦(Joe Hellerstein)认为,在 Härder 与 Reuter 的论文中,“ACID 中的 C”是“用来凑数的”。
64 |
65 | ### 隔离性(Isolation)
66 |
67 | 多个客户端并发访问数据库时,如果访问的数据没有交集,是可以随意并发的。但如果有交集,则会产生**并发问题**,或称**竞态条件**(race condition)。
68 |
69 | 
70 |
71 | 设有一个计数器,且数据库没有内置原子的自增操作,有两个用户,各自读取当前值,加 1 后写回。如图,期望计数器由 42 变为 44,但由于并发问题,最终变成了 43。
72 |
73 | ACID 中隔离性是指,每个事务的执行是互相隔离的,每个事务都可以认为自己是系统中唯一正在运行的事务,因此传统上,教科书将事务隔离形式称为:**可串行化(Serializability)**。即,如果所有事务都串行执行,则任意时刻必然只有一个事务在执行,从而在根本上消除任何并发问题。
74 |
75 | 但在实践中,很少用这么强的隔离性,实际上隔离性强弱类似于一个光谱,数据库系统提供商一般会实现其中几个,用户可以根据业务情况在隔离性和性能间进行选择。
76 |
77 | 下一节,会详细讨论除可串行化外的几种**弱隔离级别**。
78 |
79 | ### 持久性(Durability)
80 |
81 | 持久性是一种保证,即事务一旦提交,即使服务器宕机重启、甚至发生硬件故障,已经提交的事务所写入的数据就不会丢失。
82 |
83 | 在单机数据库中,持久性意味着以数据页(Page)或日志形式(WAL)写入了非易失性存储。在多副本(Replication)数据库中,持久性意味着写入了多数节点。
84 |
85 | 但,**持久性**都只能做到某种程度的保证,而非绝对保证,比如:
86 |
87 | 1. 对于单机,可以容忍宕机。但磁盘坏了就完犊子。
88 | 2. 对于多机,可以容忍少数副本损坏,但是多数副本完后也没辙。
89 |
90 | 因为在现实世界中,存储所涉及到的所有环节,都不是完美的:
91 |
92 | - 写入磁盘后宕机,虽然数据没丢,但机器修复或磁盘转移前,数据服务都是不可用的。但冗余(Replication)系统可以解决该问题。
93 | - 一个关联性故障,如软件 bug 或者机房断电,可以同时摧毁一个机房中的所有副本。因此多副本的内存数据库仍要定期持久化到外存。
94 | - 异步复制系统中,当主副本不可用时,由于没来得及同步到多数节点,最近的写入主副本成功的数据可能会丢失。
95 | - 当突然断电时,固态硬盘可能不能保证数据已完整刷盘,甚至用户显式调用 fsync 也无济于事。此外,硬盘驱动可能也会有 bug。
96 | - 磁盘上的数据可能会随着时间逐渐损坏,甚至副本数据也可能同时损坏,此时就只能依赖于备份了。
97 |
98 | 在实践中,要通过多种手段,比如强制刷盘、校验码、异地复制、定时备份等多种手段来保证数据的持久性,但也只能做到大概率的保证(比如五个九),而非绝对保证。
99 |
100 | ## 单对象和多对象操作
101 |
102 | 总结来说,在 ACID 中,原子性和隔离性是数据库对用户进行多个写入时需要提供的保证,并且它们通常假设一个事务中会同时修改多个对象(rows、documents 和 records)。相比**单对象事务**(single-object transaction),这种**多对象事务**(multi-objects transaction)是一种更强的保证,且更常用,因为通常多个写入不会只针对单个对象。
103 |
104 | 设有电子邮件情景,邮箱首页需要如下语句来展示未读邮件数:
105 |
106 | ```sql
107 | SELECT COUNT(*) FROM emails WHERE recipient_id = 2 AND unread_flag = true
108 | ```
109 |
110 | 如果邮件过多,为了加快查询,可以使用额外字段将未读邮件数存储存储起来(术语:[denormalization](https://en.wikipedia.org/wiki/Denormalization)),但每次新增、读过邮件之后都要更新该计数值。
111 |
112 | 如下图,用户 1 插入一封邮件,然后更新未读邮件数;用户 2 先读取读取邮件列表,后读取未读计数。但邮箱列表中显示有新邮件,但未读计数却显示 0。
113 |
114 | 
115 |
116 | 隔离性可以避免此问题,使用户 2 要么看到用户 1 的所有更新,要么看不到任何更新。
117 |
118 | 下图,说明了原子性保证。如果是事务执行过程中发生错误,原子性会保证如果计数器更新失败,新增的邮件也会被撤销。
119 |
120 | 
121 |
122 | 在多对象事务中,一个关键点是如何确定多个操作是否属于同一事务:
123 |
124 | 1. 从**物理上来考虑**。可以通过 TCP 连接来确定,在同一个连接中,`BEGIN TRANSACTION` 和 `COMMIT`语句之间的所有内容,可以认为属于同一个事务。但会有一些 corner case,如在客户端提交请求后,服务器确认提交之前,网络中断,连接断开,此时客户端则无从得知事务是否被成功提交。
125 | 2. **从逻辑上来考虑**。使用事务管理器,为每个事务分配一个唯一标识符,从而对操作进行分组。
126 |
127 | 实际中基本上使用第二种方法。
128 |
129 | 有一些非关系型数据库,虽然提供 Batch 操作接口,但它们并不一定有事务语义,即可能有些对象成功,另外一些对象操作却失败。
130 |
131 | ### 单对象写入
132 |
133 | 当只更改单个对象时,仍会面临原子性和隔离性的问题。假设,在文档数据库中,你正在写入一个 20 KB 的 JSON 文档:
134 |
135 | 1. 如果发送了前 10 kb 数据后,网络断开,数据库是否已经存储了这不完整的 10k 数据?
136 | 2. 如果该操作是正在覆盖一个老版本同 id 数据,覆盖一半时电源发生故障,数据库是否会存在一半旧值一半新值?
137 | 3. 如果有另一个客户端同时在读取该文档,是否会看到半更新状态?
138 |
139 | 这些问题都非常棘手,如果数据库不提供任何保证,用户侧得写很多的错误处理逻辑。因此,一般的数据库哪怕不支持完整的事务,也会提供针对单个对象的原子性和隔离性。比如,可以使用写前日志来保证原子性,使用锁来保证隔离性。
140 |
141 | 其他一些数据库,也会提供更复杂的原子支持,如原子的自增操作,从而避免图 7-1 中的交错更新。另一种更泛化的原子性保证是提供单个对象上的 CAS 操作,允许用户原子的执行针对单个对象的 read-modify-write 操作。当然,如果咬文嚼字一下,**原子自增**(atomic increment),在 ACID 中应该是属于隔离性(Isolation)的范畴,此处的原子自增其实是多线程的概念。
142 |
143 | 有的 NoSQL 数据库将上述支持宣传为”轻量级事务“,甚而 PR 成”ACID”。但这是极其不负责任的,通常来说,事务是一种将针对**多个对象**的**多个操作**封装为一个执行单元的机制。
144 |
145 | ### 界定对多对象事务的需求
146 |
147 | 由于跨机器的分布式事务很难实现、且非常损失性能(可能在一个数量级),很多分布式数据库选择不支持多对象事务。但有些场景确实需要多对象事务,因此一些数据库多将是否打开事务设为一个开关。
148 |
149 | 因此,在用户侧,在数据库选型时,有必要审视一下是否真的需要多对象事务,键值对模型和单对象事务是否能够满足需求。一些情况下,是足够的,但更多的情景,需要协同更新多个对象:
150 |
151 | 1. 在关系型数据库中,一些表通常会有一些外键。在更新时,需要进行同步更新。
152 | 2. 在文档型数据库中,相关的数据通常会放到一个文档中,但由于大部分文档数据库不支持 Join,因此不得不使用前文提到的 denormalization 对信息进行冗余存储,便产生了同步更新需求。
153 | 3. 在支持次级索引的数据库中,数据和对应的多个索引需要进行同步更新。
154 |
155 | 如果没针对多对象事务,上述保证只能在应用侧实现,徒增复杂度,而且容易出错。
156 |
157 | ### 故障和终止
158 |
159 | 如前所述,事务的一个重要特点是在执行到一半时,可以安全的终止并重试。事务的设计哲学是:当出现违反原子性、隔离性和持久性的危险时,就**丢弃而非保留**已经发生的修改。
160 |
161 | 但,另一些场景,如多副本中的无主模型,就采用了“**尽力而为**”的模型,即尽可能的保证完成任务,如不能完成,也不会回滚已经发生的修改。因此,应用侧需要处理这种半完成的状态。
162 |
163 | 尽管无脑重试被终止的事务简单且有效,但并不万能:
164 |
165 | 1. 事务已经被成功提交,但返回给用户时出错。用户如果简单重试,就会使该事务中的操作执行两次,从而造成数据冗余,除非用户在应用代码侧进行去重(如多次执行这些语句,效果一样)。
166 | 2. 由于系统负载过高,而造成事务执行失败。如果无脑重试,会进一步加重系统负担。此时可以使用指数后退方式重试,并且限制最大重试次数。
167 | 3. 一些临时错误,如死锁、异常、网络抖动和故障切换时,重试才有效;对于一些永久故障,重试是没有意义的。
168 | 4. 某事务在数据库之外如有副作用,重试事务时,会造成副作用的多次发生。如果某个事务的副作用是发送邮件,则肯定不希望事务每次重试时都发送一次电子邮件。如果想进行多个系统间的协同,可以考虑两阶段提交。
169 | 5. 如果客户端在写入数据时故障,则所有该客户端正在执行的事务所涉及的数据修改都会丢失。
170 |
171 | # 几种弱隔离级别
172 |
173 | 如果两个事务修改的数据没有交集,则可以安全的并发;否则,就会出现竞态条件。一旦出现并发 BUG,通常很难复现和修复。单客户端的并发已经足够麻烦,多客户端并发访问更加剧了并发问题。
174 |
175 | 数据库试图通过**事务隔离**(transaction isolation)来给用户提供一种隔离保证,从而降低应用侧的编程复杂度。最强的隔离性,**可串行化**(Serializability),可以对用户提供一种保证:**任意时刻,可以认为只有一个事务在运行**。
176 |
177 | 初学者对几种隔离级别的递进关系通常难以理解,往往是找不到一个合适的角度。我的经验是,从实现的角度对几种隔离级别进行理解,会简单一些。如 ANSI SQL 定义的四种隔离级别:**读未提交**(Read Uncommited)、**读已提交(**Read Commited)、**可重复读**(Repeatable Read)和**可串行化**(Serializability),可以从使用锁实现事务的角度来理解。
178 |
179 | 最强的隔离性——可串行化,可以理解为全局一把大排它锁,每个事务在启动时获取,在提交、回滚或终止时释放,但无疑这种隔离级别性能最差。而其他几种弱隔离级别,可以理解为是为了提高性能,缩小了加锁的粒度、减小了加锁的时间,从而牺牲部分一致性换取性能。从上锁的强弱考虑,我们有**互斥锁**(Mutex Lock,又称写锁)和**共享锁**(Shared Lock,又称读锁);从上锁的长短来考虑,我们有**长时锁**(Long Period Lock,事务开始获取锁,到事务结束时释放)和**短时锁**(Short Period Lock,访问时获取,用完旋即释放);从上锁的粗细来考虑,我们有对象锁(Row Lock,锁一行)和谓词锁(Predicate Lock,锁一个范围)。
180 |
181 | 但这没有覆盖到到另一个常见的隔离级别——**快照隔离**(Snapshot Isolation),因为它引出了另一种实现族——MVCC。由于属于不同的实现,快照隔离和可重复读在隔离级别的光谱上属于一个偏序关系,不能说谁强于谁。
182 |
183 | 接下来几个小节,将依次考察读已提交、快照隔离、可重复读三个隔离级别。以及隔离级别不够导致的几种现象——**更新丢失**(Lost Update)、**写偏序**(Write Skew)和**幻读**(Phantom Read)。
184 |
185 | ## 读已提交
186 |
187 | 性能最好的隔离级别就是不上任何锁,但会存在**脏读**和**脏写**的问题。为了避免脏写,可以给要更改的对象加长时写锁,但读数据时并不加锁,此时的隔离级别称为**读未提交**(RU,Read Uncommitted)。但此时仍然会有脏读,为了避免脏读,可以对要读取的对象加短时读锁,此时的隔离级别是**读已提交**(RC,Read Committed),他提供了两个保证:
188 |
189 | 1. 从数据库读取时,只能读到已经提交的数据(即没有脏读,no dirty reads)
190 | 2. 往数据库写入时,只能覆盖已经提交的数据(即没有脏写,no dirty writes)
191 |
192 | ### 无脏读
193 |
194 | 如果一个事务 A 能够读到另一个未提交事务 B 的中间状态,则称有**脏读**(dirty reads)。在读已提交的隔离级别的运行的事务,不会有脏读。举个例子:
195 |
196 | 
197 |
198 | 在用户 1 的事务提交前,用户 2 看到的 x 值一直是 2。
199 |
200 | 如果允许脏读会有什么问题?举两个例子:
201 |
202 | 1. 一个事务如果更新多个对象,脏读则可能让另外的事务看到中间不一致的状态。如前文举的未读邮件数的例子。
203 | 2. 如果事务终止,回滚所有操作,允许脏读会让另外的事务读取到被回滚的数据。
204 |
205 | ### 无脏写
206 |
207 | 如果两个事务并发更新相同对象,且事务 A 修改了一个对象,但尚未提交,此时如果另一事务 B 同样修改该对象,并且覆盖了 A 未提交的值,则称**有脏写**(dirty writes)。在**读已提交**隔离级别运行的事务,为了防止脏写,通常会推迟(重试或者加锁)后面修改同一对象的事务到前一个事务提交或终止。
208 |
209 | 通过禁止脏写,可以避免一些并发产生的不一致问题:
210 |
211 | 1. 如果多个事务同时更新相交的多个对象,脏写可能会产生错误的结果。如下图二手车销售,购买汽车需要两个步骤:更新购买列表、将发票发给买家。如果 Alice 和 Bob 的购买事务允许脏写,则可能出现 Bob 买到了商品,但发票给了 Alice。
212 | 2. 但读已提交并不能防止如图 7-1 中的计数器的竞态条件(是一种更新丢失)。两个事务都是读的已提交的数据(因此不是脏读),且写入时,另一个事务写入发生在前一个事务之后(因此不是脏写),但仍然不能避免写入丢失的问题(只增加了一次)。
213 |
214 | 
215 |
216 | ### 实现
217 |
218 | 读已提交是一个常见的隔离级别,是 Oracle 11g、PostgreSQL、SQL Server 2012、MemSQL 和其他许多数据库的默认设置。
219 |
220 | 那如何实现读已提交的隔离级别呢?
221 |
222 | 首先说**脏写**,最简单、常见的方法是使用**行锁**(起源于关系型数据库),即针对单条数据的长时写锁(Long Period Write Lock)。当事务想要修改某对象时,需要先获取该对象的锁,如果已被获取,则等待,如果成功获取,则可以写入数据,待事务提交时释放锁。
223 |
224 | 其次说**脏读**,也可以使用针对单条数据的短时读锁来解决脏读问题。读锁可以并发,但和上述写锁是互斥的。这可以确保有脏数据(未提交的更改)时,其他事务针对该对象的读取会被阻塞。但使用行锁的性能也并不是很好,因为一个长写事务,可能会把其他要读取该对象的读事务都“饿死”,损失性能和延迟。
225 |
226 | 因此,当今大多数据库会走另一条路子,即非锁的形式实现读已提交。如某种方式将旧值记住,在有针对某对象写的事务进行时,其他针对同一对象的事务中的读取都会拿到旧值。当更改事务提交时,其后事务才能看到该对象的新值。将其泛化一下,就是我们常说的 MVCC。
227 |
228 | ## 快照隔离和重复读
229 |
230 | 粗看**读已提交**已经能够满足事务的定义,比如能够终止事务、能够实现某种程度上的隔离,但仍然会产生一些并发问题。
231 |
232 | 如图,考察这样一种场景,Alice 分两个账户,各存了 500 块钱,但如果其两次分别查看两个账户期间,发生了一笔转账交易,则两次查看的余额加起来并不等于 1000。
233 |
234 | 
235 |
236 | 这种异常被称为**不可重复读**(non-repeatable read)或者**读倾斜**(read skew,skew 有点被过度使用)。读已提交的隔离级别允许出现不可重复读问题,如上述例子,每次读取到的都是已提交的内容。
237 |
238 | 例子中的不一致情况,只是暂时的。但在某些情况下,这种暂时的不一致也是不可接受的:
239 |
240 | 1. **备份**。备份可能需要花费很长时间,而备份过程中可能会有读写存在,从而造成备份时的不一致。如果之后再利用此备份进行恢复,则会造成永久的不一致。
241 | 2. **分析型查询和完整性检查**。这个操作和备份一样,耗时都会比较长,如果中间有其他事务并发导致出现不一致的现象,就会导致返回的结果有问题。
242 |
243 | **快照隔离**(snapshot isolation)级别能够解决上述问题,使用此级别,每个事务都可以取得一个某个时间点的**一致性快照**(consistent snapshot),在整个事务期间,读到的状态都是该时间点的快照。其他事务的修改并不会影响到该快照上。
244 |
245 | 快照隔离级别在数据库中很常用,PostgreSQL、使用 InnoDB 引擎的 MySQL、Oracle、SQL Server 等都支持。
246 |
247 | ### 快照隔离的实现
248 |
249 | 和读已提交一样,快照隔离也使用加锁的方式来防止脏写,但在进行读取不使用锁。快照隔离的一个关键原则是“读不阻塞写,写不阻塞读”,从而允许用户在进行长时间查询时,不影响新的写入。
250 |
251 | 为了实现快照隔离,保证读不阻塞写,且避免脏读,数据库需要对同一个对象保留多个已提交的版本,我们称之为**多版本并发控制**(**MVCC,multi-version concurrency control**)。
252 |
253 | 如果一个数据只需要实现到读已提交级别,那么保留两个版本就够了;但要实现快照隔离级别,一般使用 MVCC。相对于锁,MVCC 是另一个进行事务实现的流派,而且近些年来更受欢迎。当然,MVCC 是一种思想或者协议,具体到实现,有 MVTO(Timestamp Ordering)、MVOCC(Optimistic Currenccy Control)、MV2PL(2 Phrase Lock),即基于多版本,加上一种避免写写冲突的方式。
254 |
255 | 具体来说,使用 MVCC 流派,也可以实现读未提交、读已提交、快照隔离、可串行化等隔离级别。
256 |
257 | 1. **读已提交**在查询语句粒度使用单独的快照,快照粒度更小,因此性能更好。
258 | 2. **快照隔离**在事务粒度使用相同的快照(主要解决**不可重复读**问题)。
259 |
260 | MVCC 的基本要点为:
261 |
262 | 1. 每个事务开始时会获取一个自增的、唯一的事务 ID(txid),该 txid = max(existing tx id) + 1。
263 | 2. 该事务在修改数据时,不会修改以前版本,而会新增一个具有 txid 版本的数据。
264 | 3. 该事务只能访问到所有版本 ≤ txid 的数据。
265 | 4. 在写入时,如果发现某个数据存在 > txid 的版本,则存在写写冲突。
266 |
267 | 下图是 PostgreSQL 中基于 MVCC 实现快照隔离的示意图,其场景仍是两个账户,每个账户各有 500 块钱。本例中是通过使用两个版本信息:created by 和 deleted by,来标记一个数据版本的生命周期。
268 |
269 | 
270 |
271 | 使用 delete by 进行标记删除的原因在于,可能还有正在进行的事务(txid < deleted by)可能会访问该对象。之后,会有专门进行 GC 进程对这些数据进行真正的回收,当然删除时需要确认所有正在进行的事务 txid > deleted by。
272 |
273 | - 个人认为不使用 delete by 也能达到标记删除的效果?
274 | 新的版本数据存在后,自动就使得老版本不可见。之后,只要确定没有事务正在访问老版本数据,即可进行 gc。通过 min(current tx) > latest version 即可判定没有事务访问了。
275 |
276 | ### 可见性规则
277 |
278 | 在事务中进行读取时,对于每个对象来说,需要**控制**其版本对事务的可见性,以保证该事务能够看到一致性的视图。
279 |
280 | 使用 MVCC,每个对象都有多个版本。上一节粗略说到该事务只能访问到所有版本 ≤ txid 的数据。展开来讲:
281 |
282 | 1. 事务开始时,所有正在进行(已经开始但未提交或中止)的事务,所做的任何写入都会被忽略。
283 | 2. 被中止的事务,所做的任何写入都会被忽略。
284 | 3. 具有较晚事务 ID 的事务所做的任何写入都会被忽略。
285 | 4. 剩余其他的数据,对此事务都可见。
286 |
287 | 如果事务 txid 是严格自增的,则可以理解为,对于 txid = x 的事务来说:
288 |
289 | 1. 对于所有 txid < x 的事务,如果已经中止或**正在进行**,则其所写数据不可见。
290 | 2. 对于所有 txid > x 的事务,所写数据皆不可见。
291 |
292 | 从另外一个角度来讲,如果一个对象的版本:
293 |
294 | 1. 在事务开始时,创建该版本的事务已经提交。
295 | 2. 未被标记删除,或被标记删除的事务尚未提交。
296 |
297 | 则该对象版本对改事务可见。
298 |
299 | 长时间运行的事务,可能会导致某些标记删除的对象版本不能够真正的被回收。但如果此类事务不太多,则代价并不大,只是需要维护一些对象的多个版本。
300 |
301 | ## 索引和快照隔离
302 |
303 | 当数据有多个版本时,如何给数据建立索引?一个简单的方法是将索引指向对象的所有版本,然后在查询时使用再进行版本过滤。当某个对象的所有版本对任何事务都不再可见时,相应的索引条目也可以被同时删除。
304 |
305 | 在实践中,有很多优化。如 PostgreSQL 的一个优化是,如果某个对象更新前后的数据都在一个物理页中,则对应的索引指向可以不用更新。
306 |
307 | CouchDB、Datomic 和 LMDB 中使用一种 **仅追加 / 写时拷贝(append-only/copy-on-write)**的 B 树变体,是一种**多版本技术**的变体。[boltdb](https://www.qtmuniao.com/2020/11/29/bolt-data-organised/) 参考了 LMDB,也可以归为此类,此类 B 族树每次修改,都会引起叶子节点(所有数据都会落到叶子节点)到根节点的一条路径的全部修改(叶子节点变了,其父节点内容——指针也要修改,从而引起级联修改),如果发生节点的分裂或合并,会引起更大范围的更新。
308 |
309 | 这种方式在更新时不会覆盖老的页,每个数据修改都会新生成一个树根,每个树根所代表的树可以视作一个版本的快照。使用某个树根就相当于使用某个版本快照,其所能访问到的数据都属于同一个版本,而无须再进行版本过滤。当然,这类系统也需要后台常驻的 compaction 和 GC。
310 |
311 | ### 可重复读和命名困惑
312 |
313 | 在 1975 年 System R 定义 ANSI SQL 标准的隔离级别时,只定义了 RU、RC、RR 和 Serializability。当时,快照隔离还没有被发明,但是上述四种级别汇总有一个和快照隔离类似的级别:RR、Repeatable Read、可重复读。
314 |
315 | 因此,虽然快照隔离级别很有用,尤其是只读事务,但很多数据库虽然实现了快照隔离,但却另有称谓。比如 Oracle 将 SI 称为 **可串行化(Serializable)**,PostgreSQL 和 MySQL 将 SI 称为 **可重复读(repeatable read)**。因为这样可以符合 SQL 标准要求,以号称兼容 SQL 标准。
316 |
317 | 但严格来说,SQL 对隔离级别的定义是有问题的,比如标准依赖于实现、几个隔离级别不连续、模糊不精确。很多数据库都号称实现了**可重复读**级别,但它们提供的保证却存在着很大差异。虽然一些文献中有对可重复读进行了精确定义,但大部分实现并不严格满足此定义。到最后,没有人知道可重复读的真正含义。
318 |
319 | ## 防止更新丢失
320 |
321 | **读已提交**和**快照隔离**,只是定义了从**只读事务**的视角,在有并发写入时,哪些数据是可见的,即解决了**读写冲突**。但我们忽略了包含并发写的多个事务的一些冲突情况。当然,产生脏写的**写写冲突**已经讨论过,但还有其他几类冲突,比较有名的是**更新丢失**(lost update),典型的例子如并发更新计数器。
322 |
323 | 更新丢失发生的关键在于,两个事务中都有读后写序列(读取 - 修改 - 写入序列,写偏序也是这个序列,但是针对多个对象),即**写依赖于之前的读**。如果读到的内容被其他事务修改,则本事务稍后的依赖于此读的写就会发生问题。如:
324 |
325 | 1. 并发更新计数器和账户余额。
326 | 2. 复合值的并发修改(如 json 文档中的列表字段,需要先读出,加一个字段后写回)。
327 | 3. 两个用户同时修改 wiki 页面,并且都是修改后将页面完整覆写回。
328 |
329 | 可以看出,这是一个普遍存在的问题,因此也诞生了很多方案来解决此问题。
330 |
331 | ### 原子写
332 |
333 | 有些数据库提供原子的(**针对单个对象的**)read-modify-write 操作,因此,如果应用层逻辑能用这个原子操作表达,就可以避免更新丢失。如大多数关系型数据库都可以使用此种原子操作对计数器进行安全并发更新:
334 |
335 | ```sql
336 | UPDATE counters SET value = value + 1 WHERE key = 'foo';
337 | ```
338 |
339 | 与关系数据库类似,
340 |
341 | 1. 文档数据库如 MongoDB,提供对文档局部的原子更新操作。
342 | 2. KV 存储如 Redis,支持对复合数据结构优先队列的原子更新。
343 |
344 | 原子操作的通常实现方式为,在读取某对象时,获取其互斥锁,从而阻止其他事务读取该对象。这种实现有时也被称为**游标稳定性**(cursor stability)。如[下图](https://www.ibm.com/docs/zh/informix-servers/12.10?topic=level-cursor-stability-isolation),在 `fetch a row` 处,数据库会释放上一行的互斥锁,同时获取该行的互斥锁,以阻止其他事务对改行进行读取或者修改。如果此处只获取短时读锁,则会退化成读已提交级别。
345 |
346 | ```cpp
347 | set isolation to cursor stability
348 | declare cursor for SELECT * FROM customer
349 | open the cursor
350 | while there are more rows
351 | fetch a row
352 | do work
353 | end while
354 | close the cursor
355 | ```
356 |
357 | 其他粗暴的实现包括,让所有针对同一个对象的操作都在一个线程上执行,从而将对任何单个对象的执行序列化。
358 |
359 | 另外,ORM 框架很容易不使用原子操作来执行 read-modify-write 序列,常常会产生隐含的 bug。
360 |
361 | ### 显式上锁
362 |
363 | 即应用在有针对单个对象的 read-modify-write 序列时,将是否上锁的决策交给应用层,通常的 SQL 语法是:
364 |
365 | ```sql
366 | select xx where xx for update;
367 | ```
368 |
369 | 书中举了一个多人下棋游戏,有几个玩家可以同时移动相同的棋子,由于规则限制,单个原子操作是不够的。但此时,可以使用数据库提供的语法来显式上锁,从而防止两个玩家移动有交集的棋子集。
370 |
371 | ```sql
372 | BEGIN TRANSACTION;
373 | SELECT * FROM figures
374 | WHERE name = 'robot' AND game_id = 222 FOR UPDATE;
375 | -- Check whether move is valid, then update the position -- of the piece that was returned by the previous SELECT. UPDATE figures SET position = 'c4' WHERE id = 1234;
376 | COMMIT;
377 | ```
378 |
379 | 但需要根据应用需求进行合理的加锁——不要过度、也不要忘记。
380 |
381 | ### 自动检测更新丢失
382 |
383 | 除了使用锁的(**悲观**)方式(在数据库层或应用层)强制 read-modify-write 原子的执行;还可以使用**乐观**方式,允许其并发执行,检测到更新丢失后进行重试。
384 |
385 | 在使用 SI 隔离级的基础上,可以高效的对更新丢失进行检测。事实上,PostgreSQL 的可重复读,Oracle 的可串行化和 SQL Server 的快照隔离级别,都能够自动检测更新丢失的冲突,并中止后面的事务。但 MySQL 的 InnoDB 的快照隔离级别并不检测是否有更新丢失,有些人认为,**快照隔离**级别需要检测更新丢失冲突,从这个角度来讲,MySQL 没有提供严格的快照隔离。
386 |
387 | 相对于应用层主动上锁来说,自动检测更新丢失可以减少很多心智负担,避免写出一些察觉不到的 bug。
388 |
389 | ### Compare-and-set
390 |
391 | 在不提供事务的数据库中,有时候会支持 CAS 操作,前面单对象写入中提到了该概念。使用 CAS 操作也能避免更新丢失,保证 read-modify-write 的原子性。
392 |
393 | 例如,在文档数据库的维基百科场景中,可以使用数据库提供的 CAS 操作,来对 wiki 页面进行原子的更新,仅当发现内容没有被修改时,才写会修改后的内容。
394 |
395 | ```sql
396 | -- This may or may not be safe, depending on the database implementation
397 | UPDATE wiki_pages SET content = 'new content' WHERE id = 1234 AND content = 'old content';
398 | ```
399 |
400 | 对于上述语句,如果数据库支持从快照中读取数据,则仍然没有办法防止丢失更新。
401 |
402 | ### 多副本和冲突解决
403 |
404 | 在多副本数据库中,解决更新丢失问题会更难一些,尤其如果多个副本允许并发写入。
405 |
406 | 在多主和无主模型中,允许数据进行并发的写入和异步的同步,因此难以保证所有的数据即时收敛成一致。之前提到的锁和 CAS 操作都是针对单份数据,因此在此情况下都不适用。
407 |
408 | 但如之前章节提到,可以允许并发写入和异步更新,如果有冲突就用多版本来解决,最后使用用户代码或者特殊数据结构来合并冲突。
409 |
410 | 特殊的,当多个操作满足“交换律”时,原子操作可以在多副本数据中进行正常的工作,如计数器场景就满足交换律。Riak 2.0 之后就支持并发的更新,并且会自动合并结果,而不会有更新丢失。
411 |
412 | 另一方面,**后者胜**(LWW,last write win)的冲突解决策略是会造成更新丢失的。虽然,很多多副本数据库默认使用 LWW 进行冲突解决。
413 |
414 | ## 写偏序和幻读
415 |
416 | 当不同事务试图并发地更新**单个对象**时,就会出现前面小节已经讨论过的的**脏写**和**更新丢失**问题。为了保持数据一致性,既可以在数据库层面自动的解决,也可以通过在应用层面显式的使用原子操作或加锁来解决。
417 |
418 | 但除了上述并发写入问题,还有一些更奇妙的冲突例子,你没猜错,这里会涉及到**多个对象**的访问。
419 |
420 | 考察一个医生值班的场景,医院通常会要求几名医生同时值班,即使有特殊情况,也要保证有不少于一名医生值班。假设在某天,轮到 Alice 和 Bob 两人值班,不巧的是,他们都感觉身体不适,并且恰好同时发起请假。
421 |
422 | 
423 |
424 | 假定数据库运行在快照隔离级别下,Alice 和 Bob 同时查询了今天值班情况,发现有多于一人值班,然后先后提交了休假申请,并且都通过了。这并没有违反快照隔离级别,但确实造成了问题——今天没有人值班了。
425 |
426 | ### 写偏序的特点
427 |
428 | 上述异常称为**写偏序**(write skew),它显然不属**脏写**和**更新丢失**,因为这两个事务在更新不同的对象,这里的竞态条件稍微有点不明显,但
429 | 的确存在竞态条件,因为如果顺序执行,不可能出现没人值班的后果。另一个常见的例子是黑白棋翻转。
430 |
431 | 从单对象到多对象的角度来看,写偏序可以算作是更新丢失的一种泛化。写偏序本质也是 read-modify-write,虽然是涉及多个对象,但本质仍然是**一个事务的写入会导致另外一个事务读取到的信息失效**。补充一句,写偏序是由 MVCC 实现的快照隔离级别的特有的缺陷,它是由于读依赖同一个不变的快照引起的。
432 |
433 | 解决更新丢失的很多手段,都难以直接用到解决写偏序上:
434 |
435 | 1. 由于涉及多个对象,针对单对象的原子操作不能使用。
436 | 2. 在快照隔离中,想要真正避免写偏序需要真正的可串行化。
437 | 3. 虽然有些数据库允许指定约束(constraints),但往往是单对象的简单约束,如唯一性、外键约束。当然,可以使用触发器来在应用层维护多对象约束,以解决上述问题。
438 | 4. 如果没有办法使用可串行的化的隔离级别,还可以利用数据库提供的(for update)机制进行显式的加锁。
439 |
440 | ```sql
441 | BEGIN TRANSACTION;
442 |
443 | SELECT * FROM doctors
444 | WHERE on_call = true
445 | AND shift_id = 1234 FOR UPDATE;
446 |
447 | UPDATE doctors
448 | SET on_call = false
449 | WHERE name = 'Alice'
450 | AND shift_id = 1234;
451 |
452 | COMMIT;
453 | ```
454 |
455 | ### 其他写偏序例子
456 |
457 | 写偏序初看起来不好理解,但只要把握住写偏序的特点:
458 |
459 | 1. 涉及多个对象。
460 | 2. 一个事务的写入会使得另外事务的读取失效,进而影响其写入决策。
461 |
462 | 就能发现很多写偏序的例子。
463 |
464 | **会议室预定系统**
465 |
466 | 基本流程是先检查是否有冲突的预定(同一个会议室、同一个时间段),如果没有,则创建会议。语句如下:
467 |
468 | ```sql
469 | BEGIN TRANSACTION;
470 | -- Check for any existing bookings that overlap with the period of noon-1pm
471 | SELECT COUNT(*) FROM bookings
472 | WHERE room_id = 123 AND
473 | end_time > '2015-01-01 12:00' AND start_time < '2015-01-01 13:00';
474 |
475 | -- If the previous query returned zero:
476 | INSERT INTO bookings
477 | (room_id, start_time, end_time, user_id)
478 | VALUES (123, '2015-01-01 12:00', '2015-01-01 13:00', 666);
479 |
480 | COMMIT;
481 | ```
482 |
483 | 但在快照隔离级别下,使用上述语句,不能避免多个用户并发预定时,预定到同一个会议室的时段。为了避免此冲突,就需要上可串行化级别了。
484 |
485 | **多人棋类游戏**
486 |
487 | 之前提到的多人棋类游戏,对棋子对象加锁,虽然可以防止两个玩家同时移动同一个棋子,却不能避免两个玩家将不同棋子移到一个位置。
488 |
489 | **抢注用户名**
490 |
491 | 在每个用户具有唯一用户名的网站上,两个用户可能会并发的尝试创建具有相同名字的账户。如果使用检查是否存在该名字 → 没有则注册该名字流程,在快照隔离级别下,是没法避免两个用户注册到相同用户名的。当然,可以通过对用户名列加唯一性约束来保证该特性,这样,第二个事务在提交时会因为违反唯一性约束而终止。
492 |
493 | **防止一钱多花**
494 |
495 | 允许用户花钱和点券的服务,通常会在用户消费时检查其没有透支,导致余额变为负数。可以通过在账户余额中插入一个临时项目来实现:列出用户中所有项目,并检查总和是否为正。但有写偏序时,可能会导致两个支出项目各自检查都合法,但加在一块就超支了。
496 |
497 | ### 幻读会导致写偏序
498 |
499 | 上述例子都可以归纳为以下模式:
500 |
501 | 1. 通过 select 语句 + 条件过滤出符合条件的所有行。
502 | 2. 依赖上述结果,应用侧代码决定是否继续。
503 | 3. 如果应用侧决定继续,就执行更改(插入、更新或者删除),并提交事务。
504 |
505 | 步骤 3 会导致另一个事务的步骤 1 失效,即如果另一个事务此时重新执行 1 的 select 查询,会得到不同的结果,进而影响步骤 2 是否继续的决策。
506 |
507 | 当然,这些步骤可能以不同的顺序发生,如可以首先写入,然后进行 select 查询,根据查询结果决定事务是否提交。
508 |
509 | 对于医生值班的例子,我们可以通过 for update 语句来锁住步骤 1 中查询到的结果;但对于其他例子,步骤 1 查询结果集为空,则无法锁住任何东西。
510 |
511 | 这种一个事务的写入会改变另一个事务的查询结果的现象,称为**幻读**。快照隔离能够避免只读事务中的幻读,但对于读写事务,就很可能出现由幻读引起的写偏序问题。
512 |
513 | ### 物化冲突
514 |
515 | 幻读在步骤 1 读不到任何对象来进行加锁。那很自然的一个想法就是,能不能手动引入一些对象槽来代表不存在的对象,从而是的加锁成为可能。
516 |
517 | 在预定会议室的例子中,可以创建一个会议室号 + 时间段表,比如每 15 分钟一个时间段。可以在该表中插入未来几个月中所有可预订的会议室号 + 时间段。如果现在一个事务想要预定某个会议室的某个时间段,便可在该表中将对应对象都锁住,然后执行预定的操作。
518 |
519 | 需要强调的是,该表只用于防止同时预定同一个会议室的同一个时间段,并不用来存储预定相关信息,可以理解为是个**锁表**,每一行都是一把锁。
520 |
521 | 这种方法称为**物化冲突**(materializing conflicts),因为它将幻读转化为数据库物理中实实在在的表和行。但如何对冲突进行合理的物化,很难且易出错。并且,此方法会将解决冲突的细节暴露给了应用层(因为应用层需要感知物化出来的表)。因此,这是最不得以的一种方法,如果数据库本就支持可串行化,则大多数情况下,可以直接使用可串行隔离级别。
522 |
523 | # 可串行化
524 |
525 | 前两节列举了很多由于隔离级别不够造成的异常,比如脏写、脏读、不可重复读、更新丢失、写偏序和幻读等等。读已提交能够避免脏写和脏读,快照隔离在此基础上,可以避免不可重复读。但更新丢失、写偏序和幻读等问题,在上述隔离级别,仍然难以解决。
526 |
527 | 而且,弱隔离级别有一些固有的问题:
528 |
529 | 1. 从数据库侧,弱隔离级别的真正含义难以理解,且不同数据库产品实现的也千差万别。
530 | 2. 从应用侧,很难判断当前代码在特定隔离级别上,是否会有竞态条件和并发问题。如果应用代码很复杂,更难看出问题。
531 | 3. 从工具侧,没有比较好用的工具来帮助检测我们的代码在特定隔离级别的竞态条件。对有竞态条件的代码测试是非常难以编写,尤其对于很小概率出现的错误来说,更是难以复现。
532 |
533 | 解决这些问题,最容易想起的就是——使用最强隔离级别,可串行化。在此种隔离级别下,所有事务的行为像顺序执行,则从根本上避免了各种竞态条件。
534 |
535 | 但可串行化有很多弱点,要了解这一点,需要逐一考察可串行化当前主要实现方法:
536 |
537 | 1. 物理上真正的对所有事务进行串行的执行。
538 | 2. **两阶段锁**(2PL,two-phase locking),曾经几十年中唯一的可用选项。
539 | 3. **乐观并发控制**(OCC,Optimistic concurrency control),如可串行化的快照隔离。
540 |
541 | 本章主要针对单机数据库探讨上述实现,到第九章时,将会将这些理论扩展到多机。
542 |
543 | ## 物理上串行
544 |
545 | 虽然实现可串行化最直观的做法就是将所有事务串行的执行。但在过去几十年,单线程事务的性能基本是不可用的。直到 2007 年左右,一些软硬件的的发展,才促成了单线程事务的真正落地:
546 |
547 | 1. **RAM 足够大且便宜**。从而促使某些场景的数据可以都放内存中,即,使用内存数据库。由于不需要每次事务都执行 IO(定期 backup 可能还是需要),单线程事务只需访问内存,因此性能还可以接受。
548 | 2. **AP、TP 场景的界定和区分**。数据库设计人员发现,在 TP 场景下,读写事务通常持续时间较短、用到的数据规模较小;对比来说,AP 场景通常只包含读取操作。因此可以让长时间、大范围的 AP 场景运行在独立于主事务循环外的**只读事务**上,然后只读事务使用一个一致性的快照即可不影响主循环。
549 |
550 | VoltDB/H-Store, Redis, and Datomic 实现了物理上的串行执行事务。由于避免了多线程间用锁同步的开销,单线程的事务某些场景下可能性能更好,但在吞吐上可能受制于单核 CPU 的上限。此外,为了充分利用单核,相比传统形式,会对事务结构重新组织(如存储过程)。
551 |
552 | ### 将事务封装成存储过程
553 |
554 | 在数据库发展早期阶段,人们试图将数据库事务设计成为包含整个用户交互流程。如果整个交互流程都从属于一个事务,那么它们就可以被原子的提交,这么抽象看起来很干净。
555 |
556 | 但人的交互所引入延迟远大于计算机 CPU 时钟周期甚至 IO 延迟,因此 OLTP 型数据库多会避免在单个事务中包含人的交互,以求单个事务能够较快的执行结束。在 Web 上,这意味着,不能让单个事务跨多个请求。但如果只允许单次请求执行一个语句,一个完整流程通常会包含多个语句,从而包含多次 RPC\HTTP 请求,会在通信上耗费太多时间。
557 |
558 | 因此,单线程串行事务系统不允许交互式的多语句事务。用户需要将多语句封装为存储过程一次性提交给数据库。如果数据都在内存中,则存储过程可以被快速的执行。
559 |
560 | 
561 |
562 | TODO:存储过程需要 if、while 都判断分支,来依赖之前结果进行决策,否则就只能实时交互。
563 |
564 | ### 存储过程的优缺点
565 |
566 | 存储过程从 1999 年就进了 SQL 标准(SQL/PSM),但由于种种原因,一直为人所诟病:
567 |
568 | 1. 每个数据库厂商都有自己的存储过程语言支持(Oracle 有 PL/SQL,SQL Server 有 T-SQL,PostgreSQL 有 PL/pgSQL 等),且语法陈旧,迭代缓慢。
569 | 2. 相比本地应用代码,存储过程运行在数据库服务器中,难以进行调试、测试和监控。
570 | 3. 数据库通常对性能表现更敏感,一个写的不好的存储过程可能会拖累整个数据库的执行。
571 |
572 | 现代的存储过程放弃了 PL/SQL,转而使用通用编程语言:VoltDB 使用 Java 或 Groovy,Datomic 使用 Java 或 Clojure,而 Redis 使用 Lua。从而在某种程度上部分克服上述缺点。
573 |
574 | 对于内存数据库的单线程事务,使用存储过程可以获得不错的吞吐:
575 |
576 | 1. 内存数据库和存储过程避免了 IO
577 | 2. 单线程避免了锁开销
578 |
579 | 值得一提的是,VoltDB 还使用存储过程进行跨节点的数据同步:不是将改动复制到多个节点上,而是在每个节点执行同样的存储过程。当然,这要求存储过程具有确定性:在不同节点的不同时间执行,需要产生相同的结果。如,在存储过程中,获取当前时间戳,就要用特殊 API。
580 |
581 | ### 对数据进行分区
582 |
583 | 单线程事务受限于单个 CPU 吞吐,为了提高写入吞吐,处理较大数据量,可以将数据进行分区。VoltDB 支持对数据以某种方式(猜测是用户指定一个分区函数)对数据进行分区。
584 |
585 | 需要注意的是,分区方式要谨慎选择,以使绝大部分事务都局限于单个分区上。对于跨分区事务,由于需要进行额外协调(如上分布式锁),以串行执行。这会带来严重的性能损失,要尽量避免。
586 |
587 | 如,VoltDB 据称支持每秒 1000 的跨分区写入,比单机事务低几个数量级,且不能通过增加机器来平滑扩展。
588 |
589 | 能否对数据进行分区,取决于数据建模方式。如键值数据可以方便的进行分区,但具有**多个**次级索引的数据就很难,因为数据只能按照一种顺序来存储,而多个索引总会带来跨分区访问。
590 |
591 | ### 串行执行小结
592 |
593 | 在某些特定约束场景下,对事务进行真正物理上的串行执行,已经成为一种可串行化隔离级别的实现方案。这些约束包括:
594 |
595 | 1. **所有**事务都必须小(摸数据少)而快(延迟低)。因为只要有一个慢,就会拖累所有其他事务。
596 | 2. **活跃数据**能够全部装入内存,沉寂数据可以放在磁盘。总之,需要最少化 IO,以保证所有事务能够快速的执行。
597 | 3. 单核 CPU 能够处理所有写入吞吐,或者,能够将事务局限在单个分区,不需要跨分区协调。
598 | 4. 只允许**有限的**跨分区事务。
599 |
600 | ## 两阶段锁
601 |
602 | 在历史上的大约三十年时间里,只有一种广泛使用的可串行化实现:**两阶段锁**(2PL,two phase locking)。需要明确的是,本文所称 2PL 其实是**严格两阶段锁**(**SS2PL, strong strict two-phase locking**)。
603 |
604 | 这里,就我的理解稍微澄清下概念。两阶段锁,其实就是将使用锁的过程分为两个阶段,通常称为**扩张阶段**和**收缩阶段**。在扩张阶段(事务的整个执行过程),只会申请锁,在收缩阶段(事务提交时),只会释放锁。从另一个角度理解,每个事务都是访问数据库的一个数据对象子集,扩张阶段就是逐渐拿到该子集所有相关对象的所有权,收缩阶段就是将持有对象所有权释放。而 **S2PL**(Strict 2PL),是在 2PL 的基础上,将写锁保持到事务结束; **SS2PL**(Strong 2PL 或 Strong Strict 2PL)是将读写锁都保持到事务结束。这里仅简单做下**概念上**区分,具体背后理论,还是挺庞杂,感兴趣可以自行找相关书和论文读读。
605 |
606 | TDOO:2PL 并非拿到所有的锁,才开始进行读写操作?而是按需拿锁,提交时集中放锁。
607 |
608 | 为了和书中保持一致,下面仍然称 2PL。
609 |
610 | > 2PL 和 2PC 听起来很像,但它们不是一个东西,只是恰好都有两个阶段而已。
611 |
612 | 在防止脏写一节,提到了锁。但 2PL 中的锁会严格一些:
613 |
614 | 1. 如果所有事务都没有写入,允许多事务并发读取一个对象。
615 | 2. 只要任何一个事务有写入,就会将其**独占**到事务**结束**,不允许其他任何事务**读或写**。
616 |
617 | 2PL 不允许读写并发、写写并发,而快照隔离却正好相反,即读写互相不阻塞。另一方面,2PL 通过阻止读写并发,可以避免**更新丢失**和**写偏序**等并发问题。
618 |
619 | ### 两阶段锁的实现
620 |
621 | 2PL 用于 MySQL(InnoDB)和 SQL Server 中实现可串行化隔离级别,以及 DB2 中实现可重复读隔离级别。
622 |
623 | 通过对每个对象进行加锁,可以实现单个对象的读写互斥。锁可以处于**共享模式**(shared mode)或者**互斥模式**(exclusive mode),具体来说:
624 |
625 | 1. 如果某个事务想**读取**一个对象,需要首先获取该对象的**共享锁**。多个事务可以同时获取同一个对象的共享锁。但若某个事务持有该对象的互斥锁,则所有需要读写该对象的事务都得等待。
626 | 2. 如果某个事务想**写入**一个对象,需要首先获取该对象的**互斥锁**。任何其他事务都不能同时持有该对象的任何种类的锁。因此,如果该对象上已经有锁,该事务必须先等待其释放。
627 | 3. 如果某个事务要先**读取**,再**写入**某个对象,可以先获取其共享锁,然后将其升级为互斥锁。升级互斥锁和获取互斥锁的条件相同。
628 | 4. 当某个事务获取锁之后,必须**持有**到事务结束(中止或者提交)。这也是上面两阶段定义的由来。
629 |
630 | 由于每个对象都要上锁,而一个事务通常会访问多个对象,因此很可能造成**死锁**:多个事务持有锁,并且互相等待对方的锁。
631 |
632 | ### 两阶段锁的性能
633 |
634 | 两阶段锁的最大问题在于其性能,这也是其没有被所有人都接受原因。两阶段锁的实现下,事务的吞吐要比其他弱隔离级别低的多。维护大量锁的开销是一个原因,更重要的原因是**并发性**的降低。
635 |
636 | **延迟不稳定**。按照 2PL 的实现定义,任何有竞态条件的事务都要通过锁进行物理上串行化的执行,类似于 DAG 的拓扑排序。因此,基于 2PL 的实现,其响应延迟会相当不稳定。由于没有对等待时长进行限制,虽然你的事务很短,但系统中任何长事务都可能对你的执行造成影响。
637 |
638 | **死锁更加频繁**。尽管基于锁实现的**读已提交**隔离级别会发生死锁,但其发生频次远不如基于 2PL 实现的**可串行化**隔离级别。这也会造成额外的性能问题:死锁被检测到,会引发重试;如果死锁频繁,则会浪费巨大的性能。
639 |
640 | ### 谓词锁
641 |
642 | 前面所提到的锁,其实遗漏了一个很关键的细节——锁的粒度。
643 |
644 | 在之前小节,我们讲到,**幻读**是一个事务改变另一个更事务的查询结果,而一个具有可串行化隔离级别的数据库,需要避免幻读。在会议室预定例子中,这意味着,在一个事务查询某个时间段可用会议室时,另外的事务不能更新该时间段的同会议室的使用情况。
645 |
646 | 如何实现这一点?从概念上讲,我们需要一个**谓词锁(predicate lock)**。它通常是共享模式,但粒度更大——不再限于单个对象,而需要囊括所有符合条件的查询结果。
647 |
648 | ```sql
649 | SELECT * FROM bookings
650 | WHERE room_id = 123 AND
651 | end_time > '2018-01-01 12:00' AND
652 | start_time < '2018-01-01 13:00';
653 | ```
654 |
655 | 如何使用谓词锁?和共享锁类似,只不过粒度更大一些。
656 |
657 | 1. 当某个事务需要读取匹配条件的所有对象时,需要获得该查询条件的**共享谓词锁**。如果有任何其他事务持有该范围内对象的互斥锁,则该事务需要等待其结束。
658 | 2. 当某个事务想要写入(插入、更新或者删除)某个对象时,上互斥锁前,需要检查是否有其他事务持有包含该对象的谓词锁。如果有,则该事务需要等待其结束。
659 |
660 | 谓词锁的一个关键点是,可以锁住一个对象集合,该对象集中的对象甚至不必已存在,但将来可能会被添加。通过谓词锁,2PL 可以解决幻读问题。
661 |
662 | ### 索引范围锁
663 |
664 | 如果活跃事务比较多,谓词锁的性能会非常差,因为锁冲突的检查会(TODO:谓词锁代表的集合可能是离散的、非连续的几个集合的并集,只能线性检查?)非常耗时。因此,大多数 2PL 的数据库使用了谓词锁的一个近似——**索引范围锁**(index-range locking,也称为 next-key locking)。
665 |
666 | 通过适当**放大锁住的对象集**来简化谓词锁。如当有多个条件进行**与**的时候,只锁一个条件。仍以会议室预定为例,假设你想预定一个中午十二点到下午一点的 123 会议室。相对于该条件上的谓词锁,锁定 123 会议室或者锁定十二点到一点的所有会议室,也是安全的,因为后者的对象集包括前者。
667 |
668 | 只锁定单个条件的好处在于,你可能在该条件上有索引。则可以将谓词锁,转化为一个在该索引上的范围锁、甚至单个索引对象锁。相比谓词锁,可以更快的判断冲突:
669 |
670 | 1. 假设索引在会议室编号 `room_id` 上,并且使用此索引查询 123 会议室的所有预定,则可将共享锁加在该索引项上。
671 | 2. 假设索引在时间段上,则可以将十二点到一点的所有索引条目上加共享锁。
672 |
673 | 无论那种方式,都使用单个条件将共享锁加在了相应的索引上;如果另一个事务想要修改相关房间或者相应时间段的会议室预定,则其必定需要**同步更新索引**。此时,索引上锁的存在会保证这些事务串行的执行。
674 |
675 | 这种方式也可以避免幻读和写偏序。相比谓词锁,索引范围锁虽然锁住的范围大,但实现开销较低。但谓词相关的索引并不总是能找到,此时可以简单的退化成整张表上的共享锁。这样做虽然有损性能,但是实现简单且安全。
676 |
677 | ## 可串行的快照隔离
678 |
679 | 前面小节详细聊了下数据库中隔离级别的图景:
680 |
681 | 1. 在光谱一侧,我们有很好的隔离级别——可串行化,但其实现要么性能差(两阶段锁),要么不可扩展(物理上串行执行)。
682 | 2. 在光谱另一个侧,我们有一些相对较弱的隔离级别,它们性能较好,但会有各种竞态条件(更新丢失、写偏序、幻读等等)。
683 |
684 | 难道说强隔离级别和高性能两者不可得兼吗?
685 |
686 | 2008 年,Michael Cahill 在其博士论文中提出了一种新型的可串行化实现方案:**可串行的快照隔离**(SSI,serializable snapshot isolation)。今天,无论单机数据库(PostgreSQL 9.1+ 的可串行化隔离级别)和分布式数据库(FoundationDB 使用了类似算法)都有 SSI 的身影。相比其他实现方式,SSI 还相对不太成熟,但其表现出的性能优势,使其隐隐然有成为可串行化默认实现的趋势。
687 |
688 | ### 乐悲观并发控制
689 |
690 | 2PL 是一种**悲观**(*pessimistic*)的并发控制机制,就像多线程编程中的**互斥锁**(mutual exclusion)。其背后哲学是,当可能有不好的事情(如并发)发生时,先悲观的等待到条件好转(其他事务释放锁),再进行执行。而物理上的串行执行,是将这种悲观哲学提升到了极致,等价于每个事务在执行时都持有了整个数据库级别的互斥锁。为了弥补这种悲观带来的性能损失,需要保证每个事务执行足够快。
691 |
692 | SSI 是一种**乐观**(*optimistic*)的并发控制机制,类比多线程编程中的乐观锁。其相应哲学是,当存在潜在危险时,仍然不做任何检查去大胆的执行。当事务提交时,再进行冲突检测,如果存在冲突,则回退重试。将乐观发展到极致,则是不上任何锁,但为了给这种乐观进行兜底,需要在执行完后进行检查。
693 |
694 | 乐观并发控制并不是一种新思想,其优缺点被充分的讨论过:
695 |
696 | 1. 如果系统负载接近上限,且争用很多,乐观并发控制会导致事务大量中止和重试,从而进一步加重系统负载。
697 | 2. 如果系统很空闲,且争用较少,乐观并发控制性能较好,因为其避免了锁的开销。此外,可以调换满足交换律的原子操作顺序,来减少争用。如并发增加的计数器场景。
698 |
699 | SSI,顾名思义,基于快照隔离。即在 SSI 隔离级别中,所有的读取都针对一份一致性的快照,这是其区别于早期乐观并发控制之处。在快照隔离之上,增加写写冲突检测算法,以决定哪些事务需要中止重试,是为 SSI。
700 |
701 | ### 基于失效前提的决策
702 |
703 | 在之前讨论写偏差时,我们观察到一种一再发生的模式:**读取 - 决策 - 写入**。
704 |
705 | 1. 读取:事务首先从数据库中读取到一些数据。
706 | 2. 决策:考察读到的数据,做出某种决策。
707 | 3. 写入:将对应决策造成结果写回数据库。
708 |
709 | 即,这里面存在一个因果关系,读为因,写为果。如果在提交时,发现决策的**前提**(*premise*,如:“今天有两名医生排到了值班”)不再满足,则后面写入失去意义。因此为了提供可串行化的隔离级别,需要识别这种因果关系,并且能够在提交时检测前提是否失效,以决定是否中止事务。
710 |
711 | 那如何检测前提是否失效呢?
712 |
713 | 1. 在读取时,要检测读到的数据版本是否为最新版本。(读之前,可能有未提交的写入)
714 | 2. 在写入时,要检测写入的数据是否覆盖了其他事务的读取。(读之后,可能发生了写入)
715 |
716 | 代入之前的例子,其实是从上述模式的不同阶段来考虑这个冲突的。
717 |
718 | 
719 |
720 | ### MVCC 读取的过时检测
721 |
722 | 快照隔离通常通过**多版本并发控制**(MVCC)来实现。当事务基于 MVCC 数据库中的某个一致性的快照进行读取时,会忽略其他事务潜在的任何修写入。
723 |
724 | 在下图中,事务 43 在查询时,认为 Alice `on_call = true`,但在事务 43 提交时,事务 42 已经先一步提交,并且导致 Alice `on_call = false`。
725 |
726 | 
727 |
728 | 为了避免这种异常,数据库需要跟踪由于 MVCC 读所**忽略的写入集合**(读时发现有更新的未提交版本),如果在提交时检测到这些写入集存在已经提交的对象,则本事务必须终止。
729 |
730 | 延迟到提交时检测,而不是发现过时读取立即终止,是因为事务并不知道之后是否会发生基于这些读取的写入操作。
731 |
732 | 总结:**读取时,检测写读冲突,延到提交时,看有冲突的写入是否已提交**。
733 |
734 | ### 影响之前读取的写入检测
735 |
736 | 即,在一个事务写入某对象时,需要检测是否该数据被另一个事务读取过(TODO:提交时检测?检测读取是否提交?)。
737 |
738 | 
739 |
740 | 在 2PL 中,我们讨论了索引范围锁,可以基于索引对某个条件范围整体上锁。此处为了检测冲突,使用了类似的技术,但不会真的锁定,只是进行了记录。
741 |
742 | 如上图,假设在班次编号 `shift_id` 上存在索引,事务 42、43 在读取了对应数据后,会在 `shift_id = 1234` 的索引条目上记下事务编号,并在事务和所有并发事务完成时,删除标记。当事务发生写入时,需要通知读过该索引的所有事务(通过标记可以知道):你读到的数据过期了。该过程类似于上锁,但并不真正的等待,而是简单通知。
743 |
744 | 如上图,事务 43 会在写入数据时,会通知事务 42 其所读取的数据过期;事务 42 在写入时,也会通知事务 43。但事务 42 首先发起提交,尽管事务 43 的写入影响了 42,但 43 未提交,此时 42 会提交成功。但 43 在提交时,发现收到通知的事务已经提交,则 43 只能中止,然后重试。
745 |
746 | 总结:**在写入时,利用之前在对应索引范围记下的读取事务编号记录冲突,在提交时,看有冲突的读取是否已经提交**。
747 |
748 | ### 可串行快照隔离的性能
749 |
750 | 一如既往,实现的细节会影响其性能表现,如事务读写跟踪粒度:
751 |
752 | 1. 如果细粒度跟踪,虽然能精确的检测到真正的冲突,减少重试,但会有显著的记录开销。
753 | 2. 如果粗粒度的跟踪,虽然性能会好,但会导致更多的冲突和重试。
754 |
755 | 在某些情况下,即使一个事务读到的信息被另外一个事务的写入覆盖,仍然能保证可串行化的隔离级别。这取决于事务读到这些信息后,用来做了什么,*PostgreSQL* 便根据这个原则来减少不必要的重试。
756 |
757 | 和 2PL 相比,SSI 的最大优点是,不会通过锁来阻塞有依赖关系的事务并发执行。SSI 就想运行在快照隔离级别一样,读不阻塞写,写不阻塞读。只是追踪记录,在提交时决定是否提交或重试。这种设计是的查询延迟更可预测。尤其是,只读事务可以工作在一致性快照上,而不受影响,这对读负载很重的场景很有吸引力。
758 |
759 | 相比物理上的串行化,SSI 能够进行平滑扩展。如 FoundationDB 就可以利用多机并行进行冲突检测,从而通过加机器获取很高的吞吐。
760 |
761 | 事务的中止率会显著影响 SSI 性能。长时间的读写事务大概率会引起冲突,并重试。因此 SSI 要求**读写事务**尽可能的短。尽管如此,SSI 仍然比物理串行化以及两阶段锁对慢事务更友好。
762 |
--------------------------------------------------------------------------------
/css/waline.css:
--------------------------------------------------------------------------------
1 | :root{--waline-font-size: 1rem;--waline-white: #fff;--waline-light-grey: #999;--waline-dark-grey: #666;--waline-theme-color: #77AAC2;--waline-active-color: #70c7ef;--waline-color: #444;--waline-bgcolor: #fff;--waline-bgcolor-light: #f8f8f8;--waline-bgcolor-hover: #f0f0f0;--waline-border-color: #ddd;--waline-disable-bgcolor: #f8f8f8;--waline-disable-color: #000;--waline-code-bgcolor: #282c34;--waline-bq-color: #f0f0f0;--waline-avatar-size: 3.25rem;--waline-m-avatar-size: calc(var(--waline-avatar-size) * 9 / 13);--waline-badge-color: #3498db;--waline-badge-font-size: 0.75em;--waline-info-bgcolor: #f8f8f8;--waline-info-color: #999;--waline-info-font-size: 0.625em;--waline-border: 1px solid var(--waline-border-color);--waline-avatar-radius: 50%;--waline-box-shadow: none}[data-waline]{font-size:var(--waline-font-size);text-align:start}[dir=rtl] [data-waline]{direction:rtl}[data-waline] *{box-sizing:content-box;line-height:1.75}[data-waline] p{color:var(--waline-color)}[data-waline] a{position:relative;display:inline-block;color:var(--waline-theme-color);text-decoration:none;word-break:break-word;cursor:pointer}[data-waline] a:hover{color:var(--waline-active-color)}[data-waline] img{max-width:100%;max-height:400px;border:none}[data-waline] hr{margin:.825em 0;border-style:dashed;border-color:var(--waline-bgcolor-light)}[data-waline] code,[data-waline] pre{margin:0;padding:.2em .4em;border-radius:3px;background:var(--waline-bgcolor-light);font-size:85%}[data-waline] pre{overflow:auto;padding:10px;line-height:1.45}[data-waline] pre::-webkit-scrollbar{width:6px;height:6px}[data-waline] pre::-webkit-scrollbar-track-piece:horizontal{-webkit-border-radius:6px;border-radius:6px;background:rgba(0,0,0,.1)}[data-waline] pre::-webkit-scrollbar-thumb:horizontal{width:6px;-webkit-border-radius:6px;border-radius:6px;background:var(--waline-theme-color)}[data-waline] pre code{padding:0;background:rgba(0,0,0,0);color:var(--waline-color);white-space:pre-wrap;word-break:keep-all}[data-waline] blockquote{margin:.5em 0;padding:.5em 0 .5em 1em;border-inline-start:8px solid var(--waline-bq-color);color:var(--waline-dark-grey)}[data-waline] blockquote>p{margin:0}[data-waline] ol,[data-waline] ul{margin-inline-start:1.25em;padding:0}[data-waline] input[type=checkbox],[data-waline] input[type=radio]{display:inline-block;vertical-align:middle;margin-top:-2px}.wl-btn{display:inline-block;vertical-align:middle;min-width:2.5em;margin-bottom:0;padding:.5em 1em;border:1px solid var(--waline-border-color);border-radius:.5em;background:rgba(0,0,0,0);color:var(--waline-color);font-weight:400;font-size:.75em;line-height:1.5;text-align:center;white-space:nowrap;cursor:pointer;user-select:none;transition-duration:.4s;touch-action:manipulation}.wl-btn:hover,.wl-btn:active{border-color:var(--waline-theme-color);color:var(--waline-theme-color)}.wl-btn:disabled{border-color:var(--waline-border-color);background:var(--waline-disable-bgcolor);color:var(--waline-disable-color);cursor:not-allowed}.wl-btn.primary{border-color:var(--waline-theme-color);background:var(--waline-theme-color);color:var(--waline-white)}.wl-btn.primary:hover,.wl-btn.primary:active{border-color:var(--waline-active-color);background:var(--waline-active-color);color:var(--waline-white)}.wl-btn.primary:disabled{border-color:var(--waline-border-color);background:var(--waline-disable-bgcolor);color:var(--waline-disable-color);cursor:not-allowed}.wl-loading{text-align:center}.wl-loading svg{margin:0 auto}.wl-comment{position:relative;display:flex;margin-bottom:.75em}.wl-close{position:absolute;top:-4px;inset-inline-end:-4px;padding:0;border:none;background:rgba(0,0,0,0);line-height:1;cursor:pointer}.wl-login-info{max-width:80px;margin-top:.75em;text-align:center}.wl-logout-btn{position:absolute;top:-10px;inset-inline-end:-10px;padding:3px;border:none;background:rgba(0,0,0,0);line-height:0;cursor:pointer}.wl-avatar{position:relative;width:var(--waline-avatar-size);height:var(--waline-avatar-size);margin:0 auto;border:var(--waline-border);border-radius:var(--waline-avatar-radius)}@media(max-width: 720px){.wl-avatar{width:var(--waline-m-avatar-size);height:var(--waline-m-avatar-size)}}.wl-avatar img{width:100%;height:100%;border-radius:var(--waline-avatar-radius)}.wl-login-nick{display:block;color:var(--waline-theme-color);font-size:.75em;word-break:break-all}.wl-panel{position:relative;flex-shrink:1;width:100%;margin:.5em;border:var(--waline-border);border-radius:.75em;background:var(--waline-bgcolor);box-shadow:var(--waline-box-shadow)}.wl-header{display:flex;overflow:hidden;padding:0 4px;border-bottom:2px dashed var(--waline-border-color);border-top-left-radius:.75em;border-top-right-radius:.75em}@media(max-width: 580px){.wl-header{display:block}}.wl-header label{min-width:40px;padding:.75em .5em;color:var(--waline-color);font-size:.75em;text-align:center}.wl-header input{flex:1;width:0;padding:.5em;background:rgba(0,0,0,0);font-size:.625em;resize:none}.wl-header-item{display:flex;flex:1}@media(max-width: 580px){.wl-header-item:not(:last-child){border-bottom:2px dashed var(--waline-border-color)}}.wl-header-1 .wl-header-item{width:100%}.wl-header-2 .wl-header-item{width:50%}@media(max-width: 580px){.wl-header-2 .wl-header-item{flex:0;width:100%}}.wl-header-3 .wl-header-item{width:33.33%}@media(max-width: 580px){.wl-header-3 .wl-header-item{width:100%}}.wl-editor{position:relative;width:calc(100% - 1em);min-height:8.75em;margin:.75em .5em;border-radius:.5em;background:rgba(0,0,0,0);font-size:.875em;resize:vertical}.wl-editor,.wl-input{max-width:100%;border:none;color:var(--waline-color);outline:none;transition:all .25s ease}.wl-editor:focus,.wl-input:focus{background:var(--waline-bgcolor-light)}.wl-preview{padding:0 .5em .5em}.wl-preview h4{margin:.25em;font-weight:bold;font-size:.9375em}.wl-preview .wl-content{min-height:1.25em;padding:.25em;word-break:break-word;hyphens:auto}.wl-preview .wl-content>*:first-child{margin-top:0}.wl-preview .wl-content>*:last-child{margin-bottom:0}.wl-footer{position:relative;display:flex;flex-wrap:wrap;margin:.5em .75em}.wl-actions{display:flex;flex:2;align-items:center}.wl-action{display:inline-flex;align-items:center;justify-content:center;width:1.5em;height:1.5em;margin:2px;padding:0;border:none;background:rgba(0,0,0,0);color:var(--waline-color);font-size:16px;cursor:pointer}.wl-action:hover{color:var(--waline-theme-color)}.wl-action.active{color:var(--waline-active-color)}#wl-image-upload{display:none}#wl-image-upload:focus+label{color:var(--waline-color)}#wl-image-upload:focus-visible+label{outline:-webkit-focus-ring-color auto 1px}.wl-info{display:flex;flex:3;align-items:center;justify-content:flex-end}.wl-info .wl-text-number{color:var(--waline-info-color);font-size:.75em}.wl-info .wl-text-number .illegal{color:red}.wl-info button{margin-inline-start:.75em}.wl-info button svg{display:block;margin:0 auto;line-height:18px}.wl-emoji-popup{position:absolute;top:100%;inset-inline-start:1.25em;z-index:10;max-width:526px;border:var(--waline-border);border-radius:6px;background:var(--waline-bgcolor);box-shadow:var(--waline-box-shadow);opacity:0;visibility:hidden;transition:transform .2s ease-out,opacity .2s ease-out;transform:scale(0.9, 0.9);transform-origin:0 0}.wl-emoji-popup.display{opacity:1;visibility:visible;transform:none}.wl-emoji-popup button{display:inline-block;vertical-align:middle;width:2em;margin:.125em;padding:0;border-width:0;background:rgba(0,0,0,0);font-size:inherit;line-height:2;text-align:center;cursor:pointer}.wl-emoji-popup button:hover{background:var(--waline-bgcolor-hover)}.wl-emoji-popup .wl-emoji{display:inline-block;vertical-align:middle;max-width:1.5em;max-height:1.5em}.wl-emoji-popup .wl-tab-wrapper{overflow-y:auto;max-height:145px;padding:.5em}.wl-emoji-popup .wl-tab-wrapper::-webkit-scrollbar{width:6px;height:6px}.wl-emoji-popup .wl-tab-wrapper::-webkit-scrollbar-track-piece:vertical{-webkit-border-radius:6px;border-radius:6px;background:rgba(0,0,0,.1)}.wl-emoji-popup .wl-tab-wrapper::-webkit-scrollbar-thumb:vertical{width:6px;-webkit-border-radius:6px;border-radius:6px;background:var(--waline-theme-color)}.wl-emoji-popup .wl-tabs{position:relative;overflow-x:auto;padding:0 6px;white-space:nowrap}.wl-emoji-popup .wl-tabs::before{content:" ";position:absolute;top:0;right:0;left:0;z-index:2;height:1px;background:var(--waline-border-color)}.wl-emoji-popup .wl-tabs::-webkit-scrollbar{width:6px;height:6px}.wl-emoji-popup .wl-tabs::-webkit-scrollbar-track-piece:horizontal{-webkit-border-radius:6px;border-radius:6px;background:rgba(0,0,0,.1)}.wl-emoji-popup .wl-tabs::-webkit-scrollbar-thumb:horizontal{height:6px;-webkit-border-radius:6px;border-radius:6px;background:var(--waline-theme-color)}.wl-emoji-popup .wl-tab{position:relative;margin:0;padding:0 .5em}.wl-emoji-popup .wl-tab.active{z-index:3;border:1px solid var(--waline-border-color);border-top-width:0;border-bottom-right-radius:6px;border-bottom-left-radius:6px;background:var(--waline-bgcolor)}.wl-gif-popup{position:absolute;top:100%;inset-inline-start:1.25em;z-index:10;width:calc(100% - 3em);padding:.75em .75em .25em;border:var(--waline-border);border-radius:6px;background:var(--waline-bgcolor);box-shadow:var(--waline-box-shadow);opacity:0;visibility:hidden;transition:transform .2s ease-out,opacity .2s ease-out;transform:scale(0.9, 0.9);transform-origin:0 0}.wl-gif-popup.display{opacity:1;visibility:visible;transform:none}.wl-gif-popup input{box-sizing:border-box;width:100%;margin-bottom:10px;padding:3px 5px;border:var(--waline-border)}.wl-gif-popup img{display:block;box-sizing:border-box;width:100%;border-width:2px;border-style:solid;border-color:#fff;cursor:pointer}.wl-gif-popup img:hover{border-color:var(--waline-theme-color);border-radius:2px}.wl-gallery{display:flex;overflow-y:auto;max-height:80vh}.wl-gallery-column{display:flex;flex:1;flex-direction:column;height:-webkit-max-content;height:-moz-max-content;height:max-content}.wl-cards .wl-user{--avatar-size: var(--waline-avatar-size);position:relative;margin-inline-end:.75em}@media(max-width: 720px){.wl-cards .wl-user{--avatar-size: var(--waline-m-avatar-size)}}.wl-cards .wl-user img{width:var(--avatar-size);height:var(--avatar-size);border-radius:var(--waline-avatar-radius);box-shadow:var(--waline-box-shadow)}.wl-cards .wl-user .verified-icon{position:absolute;top:calc(var(--avatar-size)*3/4);inset-inline-start:calc(var(--avatar-size)*3/4);border-radius:50%;background:var(--waline-bgcolor);box-shadow:var(--waline-box-shadow)}.wl-card-item{position:relative;display:flex;padding:.5em}.wl-card-item .wl-card-item{padding-inline-end:0}.wl-card{flex:1;width:0;padding-bottom:.5em;border-bottom:1px dashed var(--waline-border-color)}.wl-card:first-child{margin-inline-start:1em}.wl-card-item:last-child>.wl-card{border-bottom:none}.wl-card .wl-nick svg{position:relative;bottom:-0.125em;line-height:1}.wl-card .wl-head{overflow:hidden;line-height:1.5}.wl-card .wl-head .wl-nick{position:relative;display:inline-block;margin-inline-end:.5em;font-weight:bold;font-size:.875em;line-height:1;text-decoration:none}.wl-card span.wl-nick{color:var(--waline-dark-grey)}.wl-card .wl-badge{display:inline-block;margin-inline-end:1em;padding:0 .3em;border:1px solid var(--waline-badge-color);border-radius:4px;color:var(--waline-badge-color);font-size:var(--waline-badge-font-size)}.wl-card .wl-time{margin-inline-end:.875em;color:var(--waline-info-color);font-size:.75em}.wl-card .wl-meta{position:relative;line-height:1}.wl-card .wl-meta>span{display:inline-block;margin-inline-end:.25em;padding:2px 4px;border-radius:.2em;background:var(--waline-info-bgcolor);color:var(--waline-info-color);font-size:var(--waline-info-font-size);line-height:1.5}.wl-card .wl-meta>span:empty{display:none}.wl-card .wl-comment-actions{float:right;line-height:1}[dir=rtl] .wl-card .wl-comment-actions{float:left}.wl-card .wl-delete,.wl-card .wl-like,.wl-card .wl-reply,.wl-card .wl-edit{display:inline-flex;align-items:center;border:none;background:rgba(0,0,0,0);color:var(--waline-color);line-height:1;cursor:pointer;transition:color .2s ease}.wl-card .wl-delete:hover,.wl-card .wl-like:hover,.wl-card .wl-reply:hover,.wl-card .wl-edit:hover{color:var(--waline-theme-color)}.wl-card .wl-delete.active,.wl-card .wl-like.active,.wl-card .wl-reply.active,.wl-card .wl-edit.active{color:var(--waline-active-color)}.wl-card .wl-content{position:relative;margin-bottom:.75em;padding-top:.625em;font-size:.875em;line-height:2;word-wrap:break-word}.wl-card .wl-content.expand{overflow:hidden;max-height:8em;cursor:pointer}.wl-card .wl-content.expand::before{content:"";position:absolute;top:0;bottom:3.15em;inset-inline-start:0;z-index:999;display:block;width:100%;background:linear-gradient(180deg, #000, rgba(255, 255, 255, 0.9))}.wl-card .wl-content.expand::after{content:attr(data-expand);position:absolute;bottom:0;inset-inline-start:0;z-index:999;display:block;width:100%;height:3.15em;background:rgba(255,255,255,.9);color:#828586;line-height:3.15em;text-align:center}.wl-card .wl-content>*:first-child{margin-top:0}.wl-card .wl-content>*:last-child{margin-bottom:0}.wl-card .wl-admin-actions{margin:8px 0;font-size:12px;text-align:right}.wl-card .wl-comment-status{margin:0 8px}.wl-card .wl-comment-status .wl-btn{border-radius:0}.wl-card .wl-comment-status .wl-btn:first-child{border-inline-end:0;border-radius:.5em 0 0 .5em}.wl-card .wl-comment-status .wl-btn:last-child{border-inline-start:0;border-radius:0 .5em .5em 0}.wl-card .wl-quote{border-inline-start:1px dashed rgba(237,237,237,.5)}.wl-card .wl-quote .wl-user{--avatar-size: var(--waline-m-avatar-size)}.wl-close-icon{color:var(--waline-border-color)}.wl-content .vemoji,.wl-content .wl-emoji{display:inline-block;vertical-align:baseline;height:1.25em;margin:-0.125em .25em}.wl-content .wl-tex{background:var(--waline-info-bgcolor);color:var(--waline-info-color)}.wl-content span.wl-tex{display:inline-block;margin-inline-end:.25em;padding:2px 4px;border-radius:.2em;font-size:var(--waline-info-font-size);line-height:1.5}.wl-content p.wl-tex{text-align:center}.wl-content .katex-display{overflow:auto hidden;-webkit-overflow-scrolling:touch;padding-top:.2em;padding-bottom:.2em}.wl-content .katex-display::-webkit-scrollbar{height:3px}.wl-content .katex-error{color:red}.wl-count{flex:1;font-weight:bold;font-size:1.25em}.wl-empty{overflow:auto;padding:1.25em;color:var(--waline-color);text-align:center}.wl-operation{text-align:center}.wl-operation button{margin:1em 0}.wl-power{padding:.5em 0;color:var(--waline-light-grey);font-size:var(--waline-info-font-size);text-align:end}.wl-meta-head{display:flex;flex-direction:row;align-items:center;padding:.375em}.wl-sort{margin:0;list-style-type:none}.wl-sort li{display:inline-block;color:var(--waline-info-color);font-size:.75em;cursor:pointer}.wl-sort li.active{color:var(--waline-theme-color)}.wl-sort li+li{margin-inline-start:1em}.wl-reaction{overflow:auto hidden;margin-bottom:1.75em;text-align:center}.wl-reaction img{width:100%;height:100%;transition:all 250ms ease-in-out}.wl-reaction-title{margin:16px auto;font-weight:bold;font-size:18px}.wl-reaction-list{display:flex;flex-direction:row;gap:16px;justify-content:center;margin:0;padding:8px;list-style-type:none}@media(max-width: 580px){.wl-reaction-list{gap:12px}}[data-waline] .wl-reaction-list{margin-inline-start:0}.wl-reaction-item{display:flex;flex-direction:column;align-items:center;cursor:pointer}.wl-reaction-item:hover img,.wl-reaction-item.active img{transform:scale(1.15)}.wl-reaction-img{position:relative;width:42px;height:42px}@media(max-width: 580px){.wl-reaction-img{width:32px;height:32px}}.wl-reaction-loading{position:absolute;top:-4px;inset-inline-end:-5px;width:18px;height:18px;color:var(--waline-theme-color)}.wl-reaction-votes{position:absolute;top:-9px;inset-inline-end:-9px;min-width:1em;padding:2px;border:1px solid var(--waline-theme-color);border-radius:1em;background:var(--waline-bgcolor);color:var(--waline-theme-color);font-weight:700;font-size:.75em;line-height:1}.wl-reaction-item.active .wl-reaction-votes{background:var(--waline-theme-color);color:var(--waline-bgcolor)}.wl-reaction-text{font-size:.875em}.wl-reaction-item.active .wl-reaction-text{color:var(--waline-theme-color)}.wl-content pre,.wl-content pre[class*=language-]{overflow:auto;margin:.75rem 0;padding:1rem 1.25rem;border-radius:6px;background:var(--waline-code-bgcolor);line-height:1.4}.wl-content pre code,.wl-content pre[class*=language-] code{padding:0;border-radius:0;background:rgba(0,0,0,0) !important;color:#bbb;direction:ltr}.wl-content code[class*=language-],.wl-content pre[class*=language-]{background:none;color:#ccc;font-size:1em;font-family:Consolas,Monaco,"Andale Mono","Ubuntu Mono",monospace;text-align:left;white-space:pre;word-spacing:normal;word-wrap:normal;word-break:normal;tab-size:4;hyphens:none}.wl-content pre[class*=language-]{overflow:auto}.wl-content :not(pre)>code[class*=language-],.wl-content pre[class*=language-]{background:#2d2d2d}.wl-content :not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.wl-content .token.comment,.wl-content .token.block-comment,.wl-content .token.prolog,.wl-content .token.doctype,.wl-content .token.cdata{color:#999}.wl-content .token.punctuation{color:#ccc}.wl-content .token.tag,.wl-content .token.attr-name,.wl-content .token.namespace,.wl-content .token.deleted{color:#e2777a}.wl-content .token.function-name{color:#6196cc}.wl-content .token.boolean,.wl-content .token.number,.wl-content .token.function{color:#f08d49}.wl-content .token.property,.wl-content .token.class-name,.wl-content .token.constant,.wl-content .token.symbol{color:#f8c555}.wl-content .token.selector,.wl-content .token.important,.wl-content .token.atrule,.wl-content .token.keyword,.wl-content .token.builtin{color:#cc99cd}.wl-content .token.string,.wl-content .token.char,.wl-content .token.attr-value,.wl-content .token.regex,.wl-content .token.variable{color:#7ec699}.wl-content .token.operator,.wl-content .token.entity,.wl-content .token.url{color:#67cdcc}.wl-content .token.important,.wl-content .token.bold{font-weight:bold}.wl-content .token.italic{font-style:italic}.wl-content .token.entity{cursor:help}.wl-content .token.inserted{color:green}.wl-recent-item p{display:inline}.wl-user-list{padding:0;list-style:none}.wl-user-list a,.wl-user-list a:hover,.wl-user-list a:visited{color:var(--waline-color);text-decoration:none}.wl-user-list .wl-user-avatar{position:relative;display:inline-block;overflow:hidden;margin-inline-end:10px;border-radius:4px;line-height:0}.wl-user-list .wl-user-avatar>img{width:var(--waline-user-avatar-size, 48px);height:var(--waline-user-avatar-size, 48px)}.wl-user-list .wl-user-badge{position:absolute;bottom:0;inset-inline-end:0;min-width:.7em;height:1.5em;padding:0 .4em;border-radius:4px;background:var(--waline-info-bgcolor);color:var(--waline-info-color);font-weight:bold;font-size:10px;line-height:1.5em;text-align:center}.wl-user-list .wl-user-item{margin:10px 0}.wl-user-list .wl-user-item:nth-child(1) .wl-user-badge{background:var(--waline-rank-gold-bgcolor, #fa3939);color:var(--waline-white);font-weight:bold}.wl-user-list .wl-user-item:nth-child(2) .wl-user-badge{background:var(--waline-rank-silver-bgcolor, #fb811c);color:var(--waline-white);font-weight:bold}.wl-user-list .wl-user-item:nth-child(3) .wl-user-badge{background:var(--waline-rank-copper-bgcolor, #feb207);color:var(--waline-white)}.wl-user-list .wl-user-meta{display:inline-block;vertical-align:top}.wl-user-list .wl-badge{display:inline-block;vertical-align:text-top;margin-inline-start:.5em;padding:0 .3em;border:1px solid var(--waline-badge-color);border-radius:4px;color:var(--waline-badge-color);font-size:var(--waline-badge-font-size)}.wl-user-wall{padding:0;list-style:none}.wl-user-wall .wl-user-badge,.wl-user-wall .wl-user-meta{display:none}.wl-user-wall .wl-user-item{position:relative;display:inline-block;transition:transform ease-in-out .2s}.wl-user-wall .wl-user-item::before,.wl-user-wall .wl-user-item::after{position:absolute;bottom:100%;left:50%;z-index:10;opacity:0;pointer-events:none;transition:all .18s ease-out .18s;transform:translate(-50%, 4px);transform-origin:top}.wl-user-wall .wl-user-item::before{content:"";width:0;height:0;border:5px solid rgba(0,0,0,0);border-top-color:rgba(16,16,16,.95)}.wl-user-wall .wl-user-item::after{content:attr(aria-label);margin-bottom:10px;padding:.5em 1em;border-radius:2px;background:rgba(16,16,16,.95);color:#fff;font-size:12px;white-space:nowrap}.wl-user-wall .wl-user-item:hover{transform:scale(1.1)}.wl-user-wall .wl-user-item:hover::before,.wl-user-wall .wl-user-item:hover::after{opacity:1;pointer-events:none;transform:translate(-50%, 0)}.wl-user-wall .wl-user-item img{width:var(--waline-user-avatar-size, 48px);height:var(--waline-user-avatar-size, 48px)}/*# sourceMappingURL=waline.css.map */
2 |
--------------------------------------------------------------------------------
/img/ch01-book-software-design.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch01-book-software-design.jpeg
--------------------------------------------------------------------------------
/img/ch01-data-society.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch01-data-society.png
--------------------------------------------------------------------------------
/img/ch01-fig01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch01-fig01.png
--------------------------------------------------------------------------------
/img/ch01-fig02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch01-fig02.png
--------------------------------------------------------------------------------
/img/ch01-fig03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch01-fig03.png
--------------------------------------------------------------------------------
/img/ch02-06.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch02-06.png
--------------------------------------------------------------------------------
/img/ch02-fig01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch02-fig01.png
--------------------------------------------------------------------------------
/img/ch02-fig02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch02-fig02.png
--------------------------------------------------------------------------------
/img/ch02-fig05.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch02-fig05.png
--------------------------------------------------------------------------------
/img/ch02-how-mr-works.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch02-how-mr-works.png
--------------------------------------------------------------------------------
/img/ch02-layered-data-models.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch02-layered-data-models.png
--------------------------------------------------------------------------------
/img/ch02-semantic-web-stack.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch02-semantic-web-stack.png
--------------------------------------------------------------------------------
/img/ch02-spo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch02-spo.png
--------------------------------------------------------------------------------
/img/ch03-fig01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch03-fig01.png
--------------------------------------------------------------------------------
/img/ch03-fig03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch03-fig03.png
--------------------------------------------------------------------------------
/img/ch03-fig04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch03-fig04.png
--------------------------------------------------------------------------------
/img/ch03-fig05.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch03-fig05.png
--------------------------------------------------------------------------------
/img/ch03-fig06.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch03-fig06.png
--------------------------------------------------------------------------------
/img/ch03-fig07.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch03-fig07.png
--------------------------------------------------------------------------------
/img/ch03-fig08.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch03-fig08.png
--------------------------------------------------------------------------------
/img/ch03-fig09.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch03-fig09.png
--------------------------------------------------------------------------------
/img/ch03-fig10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch03-fig10.png
--------------------------------------------------------------------------------
/img/ch03-fig11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch03-fig11.png
--------------------------------------------------------------------------------
/img/ch03-fig12.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch03-fig12.png
--------------------------------------------------------------------------------
/img/ch03-sized-tiered.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch03-sized-tiered.png
--------------------------------------------------------------------------------
/img/ch04-encodec.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch04-encodec.png
--------------------------------------------------------------------------------
/img/ch04-fig01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch04-fig01.png
--------------------------------------------------------------------------------
/img/ch04-fig02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch04-fig02.png
--------------------------------------------------------------------------------
/img/ch04-fig03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch04-fig03.png
--------------------------------------------------------------------------------
/img/ch04-fig04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch04-fig04.png
--------------------------------------------------------------------------------
/img/ch04-fig05.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch04-fig05.png
--------------------------------------------------------------------------------
/img/ch04-fig06.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch04-fig06.png
--------------------------------------------------------------------------------
/img/ch04-fig07.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch04-fig07.png
--------------------------------------------------------------------------------
/img/ch05-fig01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch05-fig01.png
--------------------------------------------------------------------------------
/img/ch05-fig02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch05-fig02.png
--------------------------------------------------------------------------------
/img/ch05-fig03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch05-fig03.png
--------------------------------------------------------------------------------
/img/ch05-fig04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch05-fig04.png
--------------------------------------------------------------------------------
/img/ch05-fig05.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch05-fig05.png
--------------------------------------------------------------------------------
/img/ch05-fig06.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch05-fig06.png
--------------------------------------------------------------------------------
/img/ch05-fig07.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch05-fig07.png
--------------------------------------------------------------------------------
/img/ch05-fig08.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch05-fig08.png
--------------------------------------------------------------------------------
/img/ch05-fig09.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch05-fig09.png
--------------------------------------------------------------------------------
/img/ch05-fig10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch05-fig10.png
--------------------------------------------------------------------------------
/img/ch05-fig11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch05-fig11.png
--------------------------------------------------------------------------------
/img/ch05-fig12.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch05-fig12.png
--------------------------------------------------------------------------------
/img/ch05-fig13.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch05-fig13.png
--------------------------------------------------------------------------------
/img/ch05-fig14.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch05-fig14.png
--------------------------------------------------------------------------------
/img/ch05-sloppy-quorum.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch05-sloppy-quorum.png
--------------------------------------------------------------------------------
/img/ch06-dynamo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch06-dynamo.png
--------------------------------------------------------------------------------
/img/ch06-fig01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch06-fig01.png
--------------------------------------------------------------------------------
/img/ch06-fig02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch06-fig02.png
--------------------------------------------------------------------------------
/img/ch06-fig03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch06-fig03.png
--------------------------------------------------------------------------------
/img/ch06-fig04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch06-fig04.png
--------------------------------------------------------------------------------
/img/ch06-fig05.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch06-fig05.png
--------------------------------------------------------------------------------
/img/ch06-fig06.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch06-fig06.png
--------------------------------------------------------------------------------
/img/ch06-fig07.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch06-fig07.png
--------------------------------------------------------------------------------
/img/ch06-fig08.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch06-fig08.png
--------------------------------------------------------------------------------
/img/ch07-fig01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch07-fig01.png
--------------------------------------------------------------------------------
/img/ch07-fig02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch07-fig02.png
--------------------------------------------------------------------------------
/img/ch07-fig03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch07-fig03.png
--------------------------------------------------------------------------------
/img/ch07-fig04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch07-fig04.png
--------------------------------------------------------------------------------
/img/ch07-fig05.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch07-fig05.png
--------------------------------------------------------------------------------
/img/ch07-fig06.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch07-fig06.png
--------------------------------------------------------------------------------
/img/ch07-fig07.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch07-fig07.png
--------------------------------------------------------------------------------
/img/ch07-fig08.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch07-fig08.png
--------------------------------------------------------------------------------
/img/ch07-fig09.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch07-fig09.png
--------------------------------------------------------------------------------
/img/ch07-fig10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch07-fig10.png
--------------------------------------------------------------------------------
/img/ch07-fig11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch07-fig11.png
--------------------------------------------------------------------------------
/img/ch08-fig01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch08-fig01.png
--------------------------------------------------------------------------------
/img/ch08-fig02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch08-fig02.png
--------------------------------------------------------------------------------
/img/ch08-fig03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch08-fig03.png
--------------------------------------------------------------------------------
/img/ch08-fig04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch08-fig04.png
--------------------------------------------------------------------------------
/img/ch08-fig05.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch08-fig05.png
--------------------------------------------------------------------------------
/img/ch09-fig01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch09-fig01.png
--------------------------------------------------------------------------------
/img/ch09-fig02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch09-fig02.png
--------------------------------------------------------------------------------
/img/ch09-fig03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch09-fig03.png
--------------------------------------------------------------------------------
/img/ch09-fig04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch09-fig04.png
--------------------------------------------------------------------------------
/img/ch09-fig05.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch09-fig05.png
--------------------------------------------------------------------------------
/img/ch09-fig06.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch09-fig06.png
--------------------------------------------------------------------------------
/img/ch09-fig07.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch09-fig07.png
--------------------------------------------------------------------------------
/img/ch09-fig08.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch09-fig08.png
--------------------------------------------------------------------------------
/img/ch09-fig09.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch09-fig09.png
--------------------------------------------------------------------------------
/img/ch09-fig10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch09-fig10.png
--------------------------------------------------------------------------------
/img/ch10-fig01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch10-fig01.png
--------------------------------------------------------------------------------
/img/ch10-fig02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch10-fig02.png
--------------------------------------------------------------------------------
/img/ch10-fig03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch10-fig03.png
--------------------------------------------------------------------------------
/img/ch11-fig01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch11-fig01.png
--------------------------------------------------------------------------------
/img/ch11-fig02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch11-fig02.png
--------------------------------------------------------------------------------
/img/ch11-fig03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch11-fig03.png
--------------------------------------------------------------------------------
/img/ch11-fig04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch11-fig04.png
--------------------------------------------------------------------------------
/img/ch11-fig05.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch11-fig05.png
--------------------------------------------------------------------------------
/img/ch11-fig06.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch11-fig06.png
--------------------------------------------------------------------------------
/img/ch11-fig07.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/ch11-fig07.png
--------------------------------------------------------------------------------
/img/wechat-column.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/wechat-column.jpg
--------------------------------------------------------------------------------
/img/xiaobot-column.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DistSysCorp/ddia/92390fc816545fbd2855f17efc50a6d46b8da535/img/xiaobot-column.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | DDIA 逐章精读
6 |
7 |
8 |
9 |
10 |
11 |
20 |
21 |
22 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
103 |
104 |
105 |
--------------------------------------------------------------------------------
/js/docsify-pagination.min.js:
--------------------------------------------------------------------------------
1 | !function(t){("object"!=typeof exports||"undefined"==typeof module)&&"function"==typeof define&&define.amd?define(t):t()}(function(){"use strict";var i="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};function t(t,e){return t(e={exports:{}},e.exports),e.exports}var c=t(function(t,e){function n(t,e){return e.querySelector(t)}(e=t.exports=function(t,e){return n(t,e=e||document)}).all=function(t,e){return(e=e||document).querySelectorAll(t)},e.engine=function(t){if(!t.one)throw new Error(".one callback required");if(t.all)return n=t.one,e.all=t.all,e;throw new Error(".all callback required")}}),e=(c.all,c.engine,t(function(e){var n=eval;try{n("export default global")}catch(t){try{n("export default self")}catch(t){try{e.exports=i}catch(t){try{self.global=self}catch(t){window.global=window}}}}}));try{var a=c}catch(t){a=c}var e=e.Element,e=e&&e.prototype||{},r=e.matches||e.webkitMatchesSelector||e.mozMatchesSelector||e.msMatchesSelector||e.oMatchesSelector,s=function(t,e){if(!t||1!==t.nodeType)return!1;if(r)return r.call(t,e);for(var n=a.all(e,t.parentNode),i=0;i*{line-height:1;vertical-align:middle}.pagination-item-label svg{height:.8em;width:auto;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round;stroke-width:1px}.pagination-item--next{margin-left:auto;text-align:right}.pagination-item--next svg{margin-left:.5em}.pagination-item--previous svg{margin-right:.5em}.pagination-item-title{font-size:1.6em}.pagination-item-subtitle{text-transform:uppercase;opacity:.3}",u=(u=void 0===u?{}:u).insertAt,e&&"undefined"!=typeof document&&(n=document.head||document.getElementsByTagName("head")[0],(l=document.createElement("style")).type="text/css","top"===u&&n.firstChild?n.insertBefore(l,n.firstChild):n.appendChild(l),l.styleSheet?l.styleSheet.cssText=e:l.appendChild(document.createTextNode(e)));var n,l,u=function(t,e,n){return e&&f(t.prototype,e),n&&f(t,n),t};function f(t,e){for(var n=0;n ul > li"),c("p",e)),this.hyperlink=m(t))}var b=function(){return''},k=function(t,e){a=e,r=t.route.path,o={},["previousText","nextText"].forEach(function(n){var i=a[n];"string"==typeof i?o[n]=i:Object.keys(i).some(function(t){var e=r&&-1