This commit is contained in:
by931
2022-09-06 22:30:37 +08:00
parent 66970f3e38
commit 3d6528675a
796 changed files with 3382 additions and 3382 deletions

View File

@@ -201,7 +201,7 @@ function hide_canvas() {
<p>你好,我是高洪涛,前华为云技术专家、前当当网系统架构师和 Oracle DBA也是 Apache ShardingSphere PMC 成员。作为创始团队核心成员,我深度参与的 Apache ShardingShpere 目前已经服务于国内外上百家企业,并得到了业界广泛的认可。</p>
<p>我在分布式数据库设计与研发领域工作近 5 年也经常参与和组织一些行业会议比如中国数据库大会、Oracle 嘉年华等,与业界人士交流分布式数据库领域的最新动向和发展趋势。</p>
<p>近十年来,整个行业都在争先恐后地进入这个领域,从而大大加速了技术进步。特别是近五年,云厂商相继发布重量级分布式数据库产品,普通用户接触这门技术的门槛降低了,越来越多人正在参与其中,整个领域生态呈现出“百花齐放”的态势。</p>
<p><img src="assets/Cip5yGABRteAYTZyAADXPevWOF0943.png" alt="Drawing 0.png" /></p>
<p><img src="assets/Cip5yGABRteAYTZyAADXPevWOF0943.png" alt="png" /></p>
<p>2021 年数据大会上,阿里云发布了分布式数据库使用率统计图</p>
<h3>学好分布式数据库将给你带来哪些机会?</h3>
<p>但在生产实践过程中我们会发现,许多技术人员对分布式数据库还停留在一知半解的状态,比如下面这些疑问:</p>
@@ -244,7 +244,7 @@ function hide_canvas() {
<li><strong>模块三,分布式数据库的高扩展性保证——分布式系统</strong>。详细介绍分布式数据库中所蕴含的系统设计原理、算法等,包含但不限于错误侦测、领导选举、数据可靠传播、分布式事务、共识算法等内容。虽然分布式内容很多,但我不会面面俱到,而是帮你提炼精华,基于实例为你建立知识体系。</li>
<li><strong>模块四,知识拓展</strong>。我会和你探讨当代最成功的分布式数据库(传统&amp;新型),探讨它们成功的关键,同时将它们与之前模块中所介绍的技术原理进行相应的映射,让你的知识体系更加丰富。</li>
</ul>
<p><img src="assets/Cip5yGABRxaAUamaAANhzb0pQa4104.png" alt="Drawing 1.png" /></p>
<p><img src="assets/Cip5yGABRxaAUamaAANhzb0pQa4104.png" alt="png" /></p>
<h3>讲师寄语</h3>
<p>本课程的设计目标是,尽最大程度解决你的实际问题,让你在不同的工程实践中,对分布式场景下的数据库存储有更加专业的认知,并对技术趋势建立深入的洞察。</p>
</div>

View File

@@ -211,7 +211,7 @@ function hide_canvas() {
<h3>基本概念</h3>
<p>分布式数据库,从名字上可以拆解为:分布式+数据库。用一句话总结为:由多个独立实体组成,并且彼此通过网络进行互联的数据库。</p>
<p>理解新概念最好的方式就是通过已经掌握的知识来学习,下表对比了大家熟悉的分布式数据库与集中式数据库之间主要的 5 个差异点。</p>
<p><img src="assets/CgpVE2ABTo6AR5YmAAEPyUn_Xrc581.png" alt="Drawing 0.png" /></p>
<p><img src="assets/CgpVE2ABTo6AR5YmAAEPyUn_Xrc581.png" alt="png" /></p>
<p>从表中,我们可以总结出<strong>分布式数据库的核心——数据分片、数据同步</strong></p>
<h4>1. 数据分片</h4>
<p>该特性是分布式数据库的技术创新。它可以突破中心化数据库单机的容量限制,从而将数据分散到多节点,以更灵活、高效的方式来处理数据。这是分布式理论带给数据库的一份礼物。</p>
@@ -226,7 +226,7 @@ function hide_canvas() {
<p>当然分布式数据库还有其他特点,但把握住以上两点,已经足够我们理解它了。下面我将从这两个特性出发,探求技术史上分布式数据库的发展脉络。我会以互联网、云计算等较新的时间节点来进行断代划分,毕竟我们的核心还是着眼现在、面向未来。</p>
<h3>商业数据库</h3>
<p>互联网浪潮之前的数据库,特别是前大数据时代。谈到分布式数据库绕不开的就是 Oracle RAC。</p>
<p><img src="assets/CgqCHmABT3OAWKNmAADhjXR2H_U089.png" alt="Drawing 2.png" /></p>
<p><img src="assets/CgqCHmABT3OAWKNmAADhjXR2H_U089.png" alt="png" /></p>
<p>Oracle RAC 是典型的大型商业解决方案,且为软硬件一体化解决方案。我在早年入职国内顶级电信行业解决方案公司的时候,就被其强大的性能所震撼,又为它高昂的价格所深深折服。它是那个时代数据库性能的标杆和极限,是完美方案与商业成就的体现。</p>
<p>我们试着用上面谈到的两个特性来简单分析一下 RAC它确实是做到了数据分片与同步。每一层都是离散化的特别在底层存储使用了 ASM 镜像存储技术,使其看起来像一块完整的大磁盘。</p>
<p>这样做的好处是实现了极致的使用体验,即使用单例数据库与 RAC 集群数据库,在使用上没有明显的区别。它的分布式存储层提供了完整的磁盘功能,使其对应用透明,从而达到扩展性与其他性能之间的平衡。甚至在应对特定规模的数据下,其经济性又有不错的表现。</p>
@@ -234,17 +234,17 @@ function hide_canvas() {
<p>该规模在当时的环境下是完全够用的,但是随着互联网的崛起,一场轰轰烈烈的“运动”将会打破 Oracle RAC 的不败金身。</p>
<h3>大数据</h3>
<p>我们知道 Oracle、DB2 等商业数据库均为 OLTP 与 OLAP 融合数据库。而首先在分布式道路上寻求突破的是 OLAP 领域。在 2000 年伊始,以 Hadoop 为代表的大数据库技术凭借其“无共享”share nothing的技术体系开始向以 Oracle 为代表的关系型数据库发起冲击。</p>
<p><img src="assets/CgpVE2ABT4iAci6AAAE2nfoHLwM617.png" alt="Drawing 4.png" /></p>
<p><img src="assets/CgpVE2ABT4iAci6AAAE2nfoHLwM617.png" alt="png" /></p>
<p>这是一次水平扩展与垂直扩展,通用经济设备与专用昂贵服务,开源与商业这几组概念的首次大规模碰撞。<strong>拉开了真正意义上分布式数据库的帷幕</strong></p>
<p>当然从一般的观点出发Hadoop 一类的大数据处理平台不应称为数据库。但是从前面我们归纳的两点特性看,它们又确实非常满足。因此我们可以将它们归纳为早期面向商业分析场景的分布式数据库。<strong>从此 OLAP 型数据库开始了自己独立演化的道路</strong></p>
<p>除了 Hadoop另一种被称为 MPP大规模并行处理类型的数据库在此段时间也经历了高速的发展。MPP 数据库的架构图如下:</p>
<p><img src="assets/CgpVE2ABT4-AdI5VAAE42YTeOoQ273.png" alt="Drawing 6.png" /></p>
<p><img src="assets/CgpVE2ABT4-AdI5VAAE42YTeOoQ273.png" alt="png" /></p>
<p>我们可以看到这种数据库与大数据常用的 Hadoop 在架构层面上非常类似,但理念不同。简而言之,它是对 SMP对称多处理器结构、NUMA非一致性存储访问结构这类硬件体系的创新采用 shared-nothing 架构,通过网络将多个 SMP 节点互联,使它们协同工作。</p>
<p>MPP 数据库的特点是首先支持 PB 级的数据处理,同时支持比较丰富的 SQL 分析查询语句。同时,该领域是商业产品的战场,其中不仅仅包含独立厂商,如 Teradata还包含一些巨头玩家如 HP 的 Vertica、EMC 的 Greenplum 等。</p>
<p>大数据技术的发展使 OLAP 分析型数据库从原来的关系型数据库之中独立出来形成了完整的发展分支路径。而随着互联网浪潮的发展OLTP 领域迎来了发展的机遇。</p>
<h3>互联网化</h3>
<p>国内数据库领域进入互联网时代第一个重大事件就是“去 IOE”。</p>
<p><img src="assets/Cip5yGABT5qAM34oAAE2hs8yVAU932.png" alt="Drawing 8.png" /></p>
<p><img src="assets/Cip5yGABT5qAM34oAAE2hs8yVAU932.png" alt="png" /></p>
<p>其中尤以“去 Oracle 数据库”产生的影响深远。十年前,阿里巴巴喊出的这个口号深深影响了国内数据库领域,这里我们不去探讨其中细节,也不去评价它正面或负面的影响。但从对于分布式数据库的影响来说,它至少带来两种观念的转变。</p>
<ol>
<li>应用成为核心:去 O 后开源数据库需要配合数据库中间件proxy去使用但这种组合无法实现传统商业库提供的一些关键功能如丰富的 SQL 支持和 ACID 级别的事务。因此应用软件需要进行精心设计,从而保障与新数据库平台的配合。应用架构设计变得非常关键,整个技术架构开始脱离那种具有调侃意味的“面向数据库” 编程,转而变为以应用系统为核心。</li>
@@ -267,11 +267,11 @@ function hide_canvas() {
<p>首先,由于云服务天生的“超卖”特性,造成其采购成本较低,从而使终端用户尝试分布式数据库的门槛大大降低。</p>
<p>其次,来自云服务厂商的支撑人员可以与用户可以进行深度的合作,形成了高效的反馈机制。这种反馈机制促使云原生的分布式数据库有机会进行快速的迭代,从而可以积极响应客户的需求。</p>
<p>这就是云原生带给分布式数据库的变化,它是<strong>通过生态系统的优化完成了对传统商业数据库的超越</strong>。以下来自 DB-Engines 的分析数据说明了未来的数据库市场属于分布式数据库,属于云原生数据库。</p>
<p><img src="assets/CgqCHmABT_aAByOoAAH2ctjuqy4281.png" alt="Drawing 9.png" /></p>
<p><img src="assets/CgqCHmABT_aAByOoAAH2ctjuqy4281.png" alt="png" /></p>
<p>随着分布式数据库的发展,我们又迎来了新的一次融合:那就是 OLTP 与 OLAP 将再一次合并为 HTAP融合交易分析处理数据库。</p>
<p>该趋势的产生主要来源于云原生 OLTP 型分布式数据库的日趋成熟。同时由于整个行业的发展,客户与厂商对于实时分析型数据库的需求越来越旺盛,但传统上大数据技术包括开源与 MPP 类数据库,强调的是离线分析。</p>
<p>如果要进行秒级的数据处理,那么必须将交易数据与分析数据尽可能地贴近,并减少非实时 ELT 的引入,这就促使了 OLTP 与 OLAP 融合为 HTAP。下图就是阿里云 PolarDB 的 HTAP 架构。</p>
<p><img src="assets/Ciqc1GABT_6AVFtwAAHdreedW2k751.png" alt="Drawing 11.png" /></p>
<p><img src="assets/Ciqc1GABT_6AVFtwAAHdreedW2k751.png" alt="png" /></p>
<h3>总结</h3>
<p>用《三国演义》的第一句话来说:“天下大势,分久必合,合久必分。”而我们观察到的分布式数据库,乃至数据库本身的发展正暗合了这句话。</p>
<p><strong>分布式数据库发展就是一个由合到分,再到合的过程</strong></p>

View File

@@ -221,7 +221,7 @@ function hide_canvas() {
<p>NoSQL 数据库因具有庞大的数据存储需求,常被用于大数据和 C 端互联网应用。例如Twitter、Facebook、阿里和腾讯这样的公司每天都利用其收集几十甚至上百 TB 的用户数据。</p>
<p>那么 NoSQL 数据库与 SQL 数据库的区别表现在哪呢?如下表所示。</p>
<p>表 NoSQL 数据库与 SQL 数据库的区别</p>
<p><img src="assets/CgqCHmABUO2ARErIAAC-JxCHpDg212.png" alt="image" /></p>
<p><img src="assets/CgqCHmABUO2ARErIAAC-JxCHpDg212.png" alt="png" /></p>
<p>NoSQL 除了不是 SQL 外,另外一个广泛的解释是 Not Only SQL。其背后暗含我们没有 SQL但是有一项比 SQL 要吸引人的东西,那就是——分布式。</p>
<p>在 NoSQL 出现之前的商业数据库,多节点部署的难度很大且费用高昂,甚至需要使用专用的硬件。虽然理论上规模应该足够大,但其实不然。而后出现的 NoSQL大部分在设计层面天然考虑了使用廉价硬件进行系统扩容同时由于其放弃了 ACID性能才没有随着系统规模的扩大而衰减。</p>
<p>当然 NoSQL 的缺点也比较明显:由于缺乏 ACID应用时需要非常小心地处理数据一致性问题同时由于其数据模型往往只针对特定场景一般不能使用一种 NoSQL 数据库来完成整个应用的构建,导致设计层面的复杂和维护的困难。</p>

View File

@@ -210,7 +210,7 @@ function hide_canvas() {
<li>垂直分片:在不同的数据库节点中存储表不同的表列。</li>
</ol>
<p>如下图所示,水平和垂直这两个概念来自原关系型数据库表模式的可视化直观视图。</p>
<p><img src="assets/CgpVE2ABUUmAL3H4AAGTDb7sQOQ568.png" alt="Drawing 0.png" /></p>
<p><img src="assets/CgpVE2ABUUmAL3H4AAGTDb7sQOQ568.png" alt="png" /></p>
<p>图 1 可视化直观视图</p>
<p>分片理念其实来源于经济学的边际收益理论:如果投资持续增加,但收益的增幅开始下降时,被称为边际收益递减状态。而刚好要开始下降的那个点被称为边际平衡点。</p>
<p>该理论应用在数据库计算能力上往往被表述为:如果数据库处理能力遇到瓶颈,最简单的方式是持续提高系统性能,如更换更强劲的 CPU、更大内存等这种模式被称为垂直扩展。当持续增加资源以提升数据库能力时垂直扩展有其自身的限制最终达到边际平衡收益开始递减。</p>
@@ -224,11 +224,11 @@ function hide_canvas() {
<h4>哈希分片</h4>
<p>哈希分片,首先需要获取分片键,然后根据特定的哈希算法计算它的哈希值,最后使用哈希值确定数据应被放置在哪个分片中。数据库一般对所有数据使用统一的哈希算法(例如 ketama以促成哈希函数在服务器之间均匀地分配数据从而降低了数据不均衡所带来的热点风险。通过这种方法数据不太可能放在同一分片上从而使数据被随机分散开。</p>
<p>这种算法非常适合随机读写的场景,能够很好地分散系统负载,但弊端是不利于范围扫描查询操作。下图是这一算法的工作原理。</p>
<p><img src="assets/Cip5yGABUVOANTI_AACPCvFkQMQ491.png" alt="Drawing 1.png" /></p>
<p><img src="assets/Cip5yGABUVOANTI_AACPCvFkQMQ491.png" alt="png" /></p>
<p>图 2 哈希分片</p>
<h4>范围分片</h4>
<p>范围分片根据数据值或键空间的范围对数据进行划分,相邻的分片键更有可能落入相同的分片上。每行数据不像哈希分片那样需要进行转换,实际上它们只是简单地被分类到不同的分片上。下图是范围分片的工作原理。</p>
<p><img src="assets/Cip5yGABUXSATworAABCLehE-pM870.png" alt="Drawing 2.png" /></p>
<p><img src="assets/Cip5yGABUXSATworAABCLehE-pM870.png" alt="png" /></p>
<p>图 3 范围分片</p>
<p>范围分片需要选择合适的分片键,这些分片键需要尽量不包含重复数值,也就是其候选数值尽可能地离散。同时数据不要单调递增或递减,否则,数据不能很好地在集群中离散,从而造成热点。</p>
<p>范围分片非常适合进行范围查找,但是其随机读写性能偏弱。</p>
@@ -261,7 +261,7 @@ function hide_canvas() {
<p>ShardingShpere 首先提供了分布式的主键生成,这是生成分片键的关键。由于分布式数据库内一般由多个数据库节点参与,因此基于数据库实例的主键生成并不适合分布式场景。</p>
<p>常用的算法有 UUID 和 Snowfalke 两种无状态生成算法。</p>
<p>UUID 是最简单的方式,但是生成效率不高,且数据离散度一般。因此目前生产环境中会采用后一种算法。下图就是用该算法生成的分片键的结构。</p>
<p><img src="assets/Cip5yGABUWmAW6olAAECEkYbH8U406.png" alt="Drawing 3.png" /></p>
<p><img src="assets/Cip5yGABUWmAW6olAAECEkYbH8U406.png" alt="png" /></p>
<p>图 4 分片键结构</p>
<p>其中有效部分有三个。</p>
<ol>
@@ -280,21 +280,21 @@ function hide_canvas() {
<p>用户通过以上多种分片工具,可以灵活和统一地制定数据库分片策略。</p>
<h4>自动分片</h4>
<p>ShardingShpere 提供了 Sharding-Scale 来支持数据库节点弹性伸缩,该功能就是其对自动分片的支持。下图是自动分片功能展示图,可以看到经过 Sharding-Scale 的特性伸缩,原有的两个数据库扩充为三个。</p>
<p><img src="assets/CgqCHmABUYSAb4GHAAQVlwbl-X4314.png" alt="Drawing 4.png" /></p>
<p><img src="assets/CgqCHmABUYSAb4GHAAQVlwbl-X4314.png" alt="png" /></p>
<p>图 5 自动分片功能展示</p>
<p>自动分片包含下图所示的四个过程。</p>
<p><img src="assets/Ciqc1GABUY2ACpn4AAM1n2uEO-A067.png" alt="Drawing 5.png" /></p>
<p><img src="assets/Ciqc1GABUY2ACpn4AAM1n2uEO-A067.png" alt="png" /></p>
<p>图 6 自动分片过程</p>
<p>从图 6 中可以看到通过该工作量ShardingShpere 可以支持复杂的基于哈希的自动分片。同时我们也应该看到,没有专业和自动化的弹性扩缩容工具,想要实现自动化分片是非常困难的。</p>
<p>以上就是分片算法的实际案例,使用的是经典的水平分片模式。而目前水平和垂直分片有进一步合并的趋势,下面要介绍的 TiDB 正代表着这种融合趋势。</p>
<h3>垂直与水平分片融合案例</h3>
<p>TiDB 就是一个垂直与水平分片融合的典型案例,同时该方案也是 HATP 融合方案。</p>
<p>其中水平扩展依赖于底层的 TiKV如下图所示。</p>
<p><img src="assets/Ciqc1GABUZWAF6UYAACmuUoCK3Y948.png" alt="Drawing 6.png" /></p>
<p><img src="assets/Ciqc1GABUZWAF6UYAACmuUoCK3Y948.png" alt="png" /></p>
<p>图 7 TiKV</p>
<p>TiKV 使用范围分片的模式,数据被分配到 Region 组里面。一个分组保持三个副本这保证了高可用性相关内容会在“05 | 一致性与 CAP 模型:为什么需要分布式一致性?”中详细介绍)。当 Region 变大后,会被拆分,新分裂的 Region 也会产生多个副本。</p>
<p>TiDB 的水平扩展依赖于 TiFlash如下图所示。</p>
<p><img src="assets/Ciqc1GABUZ-AAH-KAAJGbLaxtiI142.png" alt="Drawing 7.png" /></p>
<p><img src="assets/Ciqc1GABUZ-AAH-KAAJGbLaxtiI142.png" alt="png" /></p>
<p>图 8 TiFlash</p>
<p>从图 8 中可以看到 TiFlash 是 TiKV 的列扩展插件,数据异步从 TiKV 里面复制到 TiFlash而后进行列转换其中要使用 MVCC 技术来保证数据的一致性。</p>
<p>上文所述的 Region 会增加一个新的异步副本,而后该副本进行了数据切分,并以列模式组合到 TiFlash 中,从而达到了水平和垂直扩展在同一个数据库的融合。这是两种数据库引擎的融合。</p>

View File

@@ -204,7 +204,7 @@ function hide_canvas() {
<p>现在让我们开始学习单主复制,其中不仅介绍了该技术本身,也涉及了一些复制领域的话题,如复制延迟、高可用和复制方式等。</p>
<h3>单主复制</h3>
<p>单主复制,也称主从复制。写入主节点的数据都需要复制到从节点,即存储数据库副本的节点。当客户要写入数据库时,他们必须将请求发送给主节点,而后主节点将这些数据转换为复制日志或修改数据流发送给其所有从节点。从使用者的角度来看,从节点都是只读的。下图就是经典的主从复制架构。</p>
<p><img src="assets/Ciqc1GAJV6SADprzAACli5qqAMo678.png" alt="Drawing 0.png" /></p>
<p><img src="assets/Ciqc1GAJV6SADprzAACli5qqAMo678.png" alt="png" /></p>
<p>这种模式是最早发展起来的复制模式,不仅被广泛应用在传统数据库中,如 PostgreSQL、MySQL、Oracle、SQL Server它也被广泛应用在一些分布式数据库中如 MongoDB、RethinkDB 和 Redis 等。</p>
<p>那么接下来,我们就从复制同步模式、复制延迟、复制与高可用性以及复制方式几个方面来具体说说这个概念。</p>
<h4>复制同步模式</h4>
@@ -292,7 +292,7 @@ function hide_canvas() {
<p>下面我就从第一代复制技术开始说起。</p>
<h4>MHA 复制控制</h4>
<p>下图是 MHA 架构图。</p>
<p><img src="assets/Cip5yGAJV9qAVnjXAAC85xLxhaU613.png" alt="Drawing 1.png" /></p>
<p><img src="assets/Cip5yGAJV9qAVnjXAAC85xLxhaU613.png" alt="png" /></p>
<p>MHA 作为第一代复制架构,有如下适用场景:</p>
<ol>
<li>MySQL 的版本≤5.5,这一点说明它很古老;</li>
@@ -317,7 +317,7 @@ function hide_canvas() {
<li>这一代开始需要支持跨 IDC 复制。需要引入监控 Monitor配合 consul 注册中心。多个 IDC 中 Monitor 组成分布式监控,把健康的 MySQL 注册到 consul 中,同时将从库复制延迟情况也同步到 consul 中。</li>
</ol>
<p>下图就是带有 consul 注册中心与监控模块的半同步复制架构图。</p>
<p><img src="assets/Cip5yGAJV-KAAg5HAAF8syZ9vQM483.png" alt="Drawing 2.png" /></p>
<p><img src="assets/Cip5yGAJV-KAAg5HAAF8syZ9vQM483.png" alt="png" /></p>
<p>第二代复制技术也有自身的一些缺陷。</p>
<ol>
<li>存在幻读的情况。当事务同步到从库但没有 ACK 时,主库发生宕机;此时主库没有该事务,而从库有。</li>
@@ -329,7 +329,7 @@ function hide_canvas() {
<p>这一代复制技术采用的是增强半同步。首先主从的复制都是用独立的线程来运行;其次主库采用 binlog group commit也就是组提交来提供数据库的写入性能而从库采用并行复制它是基于事务的通过数据参数调整线程数量来提高性能。这样主库可以并行从库也可以并行。</p>
<p>这一代技术体系强依赖于增强半同步,利用半同步保证 RPO对于 RTO则取决于复制延迟。</p>
<p>下面我们用 Xenon 来举例说明,请看下图(图片来自官网)。</p>
<p><img src="assets/CgpVE2AJV-mAE6vWAAB_JZptW8Y497.png" alt="Drawing 3.png" /></p>
<p><img src="assets/CgpVE2AJV-mAE6vWAAB_JZptW8Y497.png" alt="png" /></p>
<p>从图中可以看到。每个节点上都有一个独立的 agent这些 agent 利用 raft 构建一致性集群,利用 GTID 做索引选举主节点;而后主节点对外提供写服务,从节点提供读服务。</p>
<p>当主节点发生故障后agent 会通过 ping 发现该故障。由于 GTID 和增强半同步的加持,从节点与主节点数据是一致的,因此很容易将从节点提升为主节点。</p>
<p>第三代技术也有自身的缺点,如增强半同步中存在幽灵事务。这是由于数据写入 binlog 后,主库掉电。由于故障恢复流程需要从 binlog 中恢复,那么这份数据就在主库。但是如果它没有被同步到从库,就会造成从库不能切换为主库,只能去尝试恢复原崩溃的主库。</p>

View File

@@ -223,7 +223,7 @@ function hide_canvas() {
<p>CAP 意味着即使所有节点都在运行中我们也可能会遇到一致性问题这是因为它们之间存在连接性问题。CAP 理论常常用三角形表示,就好像我们可以任意匹配三个参数一样。然而,尽管我们可以调整可用性和一致性,但分区容忍性是我们无法实际放弃的。</p>
<p>如果我们选择了 CA 而放弃了 P那么当发生分区现象时为了保证 C系统需要禁止写入。也就是当有写入请求时系统不可用。这与 A 冲突了,因为 A 要求系统是可用的。因此,分布式系统理论上不可能选择 CA 架构,只能选择 CP 或者 AP 架构。</p>
<p>如下图所示,其实 CA 类系统是不存在的,这里你需要特别注意。</p>
<p><img src="assets/Cip5yGAJWLmAJW7kAABPImLZRig108.png" alt="Drawing 0.png" /></p>
<p><img src="assets/Cip5yGAJWLmAJW7kAABPImLZRig108.png" alt="png" /></p>
<p>图 1 CAP 理论</p>
<p>CAP 中的可用性也不同于上述的高可用性CAP 定义对请求的延迟没有任何限制。此外,与 CAP 相反,数据库的高可用性并不需要每个在线节点都可以提供服务。</p>
<p>CAP 里面的 C 代表线性一致,除了它以外,还有其他的一致模式,我们现在来具体介绍一下。</p>
@@ -232,7 +232,7 @@ function hide_canvas() {
<p>从用户的角度看,分布式数据库就像具有共享存储的单机数据库一样,节点间的通信和消息传递被隐藏到了数据库内部,这会使用户产生“分布式数据库是一种共享内存”的错觉。一个支持读取和写入操作的单个存储单元通常称为寄存器,我们可以把代表分布式数据库的共享存储看作是一组这样的寄存器。</p>
<p>每个读写寄存器的操作被抽象为“调用”和“完成”两个动作。如果“调用”发生后,但在“完成”之前该操作崩溃了,我们将操作定义为失败。如果一个操作的调用和完成事件都在另一个操作被调用之前发生,我们说这个操作在另一个操作之前,并且这两个操作是顺序的;否则,我们说它们是并发的。</p>
<p>如下图所示a是顺序操作b和 c是并发操作。</p>
<p><img src="assets/Ciqc1GAJWMaAahgyAAA9-0_mXvY966.png" alt="Drawing 1.png" /></p>
<p><img src="assets/Ciqc1GAJWMaAahgyAAA9-0_mXvY966.png" alt="png" /></p>
<p>图 2 顺序操作&amp;并发操作</p>
<p>多个读取或写入操作可以同时访问一个寄存器。对寄存器的读写操作不是瞬间完成的,需要一些时间,即调用和完成两个动作之间的时间。由不同进程执行的并发读/写操作不是串行的,根据寄存器在操作重叠时的行为,它们的顺序可能不同,并且可能产生不同的结果。</p>
<p>当我们讨论数据库一致性时,可以从两个维度来区别。</p>
@@ -265,7 +265,7 @@ function hide_canvas() {
<li>第三次读只能返回 2因为第二次写是在第一次写之后进行的。</li>
</ol>
<p>下图正是现象一致性的直观展示。</p>
<p><img src="assets/Ciqc1GAJWNaABNY5AACytLnfuEE642.png" alt="Drawing 2.png" /></p>
<p><img src="assets/Ciqc1GAJWNaABNY5AACytLnfuEE642.png" alt="png" /></p>
<p>图 3 线性一致性</p>
<p>线性一致性的代价是很高昂的,甚至 CPU 都不会使用线性一致性。有并发编程经验的朋友一定知道 CAS 操作,该操作可以实现操作的线性化,是高性能并发编程的关键,它就是通过编程手段来模拟线性一致。</p>
<p>一个比较常见的误区是,使用一致性算法可以实现线性一致,如 Paxos 和 Raft 等。但实际是不行的,以 Raft 为例,算法只是保证了复制 Log 的线性一致,而没有描述 Log 是如何写入最终的状态机的,这就暗含状态机本身不是线性一致的。</p>
@@ -273,10 +273,10 @@ function hide_canvas() {
<h4>顺序一致性</h4>
<p>由于线性一致的代价高昂,因此人们想到,既然全局时钟导致严格一致性很难实现,那么顺序一致性就是放弃了全局时钟的约束,改为分布式逻辑时钟实现。顺序一致性是指所有的进程以相同的顺序看到所有的修改。读操作未必能及时得到此前其他进程对同一数据的写更新,但是每个进程读到的该数据的不同值的顺序是一致的。</p>
<p>下图展示了 P1、P2 写入两个值后P3 和 P4 是如何读取的。以真实的时间衡量1 应该是在 2 之前被写入但是在顺序一致性下1 是可以被排在 2 之后的。同时,尽管 P3 已经读取值 1P4 仍然可以读取 2。但是需要注意的是这两种组合1-&gt;2 和 2 -&gt;1P3 和 P4 从它们中选择一个并保持一致。下图正是展示了它们读取顺序的一种可能2-&gt;1。</p>
<p><img src="assets/CgqCHmAJWOCABAs2AABs-o-Dn-I630.png" alt="Drawing 3.png" /></p>
<p><img src="assets/CgqCHmAJWOCABAs2AABs-o-Dn-I630.png" alt="png" /></p>
<p>图 4 顺序一致性</p>
<p>我们使用下图来进一步区分线性一致和顺序一致。</p>
<p><img src="assets/Ciqc1GAJWOaAT1zmAAB5GZRY2aI676.png" alt="Drawing 4.png" /></p>
<p><img src="assets/Ciqc1GAJWOaAT1zmAAB5GZRY2aI676.png" alt="png" /></p>
<p>图 5 区分线性一致和顺序一致</p>
<p>其中,图 a 满足了顺序一致性但是不满足线性一致性。原因在于从全局时钟的观点来看P2 进程对变量 x 的读操作在 P1 进程对变量 x 的写操作之后,然而读出来的却是旧的数据。但是这个图却是满足顺序一致性,因为两个进程 P1 和 P2 的一致性并没有冲突。</p>
<p>图 b 满足线性一致性,因为每个读操作都读到了该变量的最新写的结果,同时两个进程看到的操作顺序与全局时钟的顺序一样。</p>
@@ -292,10 +292,10 @@ function hide_canvas() {
<li>闭包传递:和时钟向量里面定义的一样,如果 a-&gt;b、b-&gt;c那么肯定也有 a-&gt;c。</li>
</ol>
<p>那么,为什么需要因果关系,以及没有因果关系的写法如何传播?下图中,进程 P1 和 P2 进行的写操作没有因果关系,也就是最终一致性。这些操作的结果可能会在不同时间,以乱序方式传播到读取端。进程 P3 在看到 2 之前将看到值 1而 P4 将先看到 2然后看到 1。</p>
<p><img src="assets/Ciqc1GAJWPCATmnsAACWjAazgFM942.png" alt="Drawing 5.png" /></p>
<p><img src="assets/Ciqc1GAJWPCATmnsAACWjAazgFM942.png" alt="png" /></p>
<p>图 6 因果一致性</p>
<p>而下图显示进程 P1 和 P2 进行因果相关的写操作并按其逻辑顺序传播到 P3 和 P4。因果写入除了写入数据外还需要附加一个逻辑时钟用这个时钟保证两个写入是有因果关系的。这可以防止我们遇到上面那张图所示的情况。你可以在两个图中比较一下 P3 和 P4 的历史记录。</p>
<p><img src="assets/CgqCHmAJWPiAexxkAACijWQR6zY931.png" alt="Drawing 6.png" /></p>
<p><img src="assets/CgqCHmAJWPiAexxkAACijWQR6zY931.png" alt="png" /></p>
<p>图 7 逻辑时钟</p>
<p>而实现这个逻辑时钟的一种主要方式就是向量时钟。向量时钟算法利用了向量这种数据结构,将全局各个进程的逻辑时间戳广播给所有进程,每个进程发送事件时都会将当前进程已知的所有进程时间写入到一个向量中,而后进行传播。</p>
<p>因果一致性典型案例就是 COPS 系统,它是基于 causal+一致性模型的 KV 数据库。它定义了 dependencies操作了实现因果一致性。这对业务实现分布式数据因果关系很有帮助。另外在亚马逊 Dynamo 基于向量时钟,也实现了因果一致性。</p>
@@ -308,7 +308,7 @@ function hide_canvas() {
<p>那么它们之间的联系如何呢?其实就是事务的隔离性与一致模型有关联。</p>
<p>如果把上面线性一致的例子看作多个并行事务,你会发现它们是没有隔离性的。因为在开始和完成之间任意一点都会读取到这份数据,原因是一致性模型关心的是单一操作,而事务是由一组操作组成的。</p>
<p>现在我们看另外一个例子,这是展示事务缺乏一致性后所导致的问题。</p>
<p><img src="assets/CgqCHmAJWQGAARkoAAB7ZMQP49s438.png" alt="Drawing 7.png" /></p>
<p><img src="assets/CgqCHmAJWQGAARkoAAB7ZMQP49s438.png" alt="png" /></p>
<p>图 8 事务与一致性</p>
<p>其中三个事务满足隔离性。可以看到 T2 读取到了 T1 入的值。但是这个系统缺乏一致性保障,造成 T3 可以读取到早于 T2 读取值之前的值,这就会造成应用的潜在 Bug。</p>
<p>那现在给出结论:事务隔离是描述并行事务之间的行为,而一致性是描述非并行事务之间的行为。其实广义的事务隔离应该是经典隔离理论与一致性模型的一种混合。</p>

View File

@@ -225,7 +225,7 @@ function hide_canvas() {
<p>以上两点互相作用,从而使现在很多组织和技术团队都开始去构建属于自己的分布式数据库。</p>
<h3>设计分布式数据库案例</h3>
<p>熟悉我的朋友可能知道,我另外一个身份是 Apache SkyWalking 的创始成员,它是一个开源的 APM 系统。其架构图可以在官网找到,如下所示。</p>
<p><img src="assets/Cip5yGASjYiAJEr8ABbiUnhbcXQ434.png" alt="image" /></p>
<p><img src="assets/Cip5yGASjYiAJEr8ABbiUnhbcXQ434.png" alt="png" /></p>
<p>可以看到其中的 Storage Option也就是数据库层面可以有多种选择。除了单机内存版本的 H2 以外,其余生产级别的数据库均为分布式数据库。</p>
<p>选择多一方面证明了 SkyWalking 有很强的适应能力,但更重要的是目前业界没有一款数据库可以很好地满足其使用场景。</p>
<p>那么现在我们来尝试给它设计一个数据库。这里我简化了设计流程,只给出了需求分析与概念设计,目的是展示设计方式,帮助你更好地体会分布式数据库的关键点。</p>

View File

@@ -255,7 +255,7 @@ function hide_canvas() {
<p>目前有很多种不同的数据结构可以在内存中存储有序的数据。在分布式数据库的存储引擎中有一种结构因其简单而被广泛地使用那就是跳表SkipList</p>
<p>跳表的优势在于其实现难度比简单的链表高不了多少,但是其时间复杂度可以接近负载平衡的搜索树结构。</p>
<p>跳表在插入和更新时避免对节点做旋转或替换,而是使用了随机平衡的概念来使整个表平衡。跳表由一系列节点组成,它们又由不同的高度组成。连续访问高度较高的节点可以跳过高度较低的节点,有点像蜘蛛侠利用高楼在城市内快速移动一样,这也就是跳表名称的来源。现在我们用一个例子来说明跳表的算法细节。请看下面的图片。</p>
<p><img src="assets/CioPOWAc-pCAW-h1AACAT7yvNXU780.png" alt="Drawing 0.png" /></p>
<p><img src="assets/CioPOWAc-pCAW-h1AACAT7yvNXU780.png" alt="png" /></p>
<p>如果我们以寻找 15 为例来说明跳表的查找顺序。</p>
<ol>
<li>首先查找跳表中高度最高的节点从图中可以看到是10。</li>

View File

@@ -215,10 +215,10 @@ function hide_canvas() {
<p>可以看到双树操作是比较简单明了的,而且可以作为一种 B 树类的索引结构而存在。但实际上几乎没有存储引擎去使用它,主要原因是它的合并操作是同步的,也就是刷盘的时候要同步进行合并。而刷盘本身是个相对频繁的操作,这样会造成写放大,也就是会影响写入效率且会占用非常大的磁盘空间。</p>
<p><strong>多树结构是在双树的基础上提出的,内存数据刷盘时不进行合并操作</strong>,而是完全把内存数据写入到单独的文件中。那这个时候另外的问题就出现了:随着刷盘的持续进行,磁盘上的文件会快速增加。这时,读取操作就需要在很多文件中去寻找记录,这样读取数据的效率会直线下降。</p>
<p>为了解决这个问题此种结构会引入合并操作Compaction。该操作是异步执行的它从这众多文件中选择一部分出来读取里面的内容而后进行合并最后写入一个新文件中而后老文件就被删除掉了。如下图所示这就是典型的多树结构合并操作。而这种结构也是本讲介绍的主要结构。</p>
<p><img src="assets/Cgp9HWAqWPaAI1cVAAF0GY8NUFc418.png" alt="1.png" /></p>
<p><img src="assets/Cgp9HWAqWPaAI1cVAAF0GY8NUFc418.png" alt="png" /></p>
<p>最后,我再为你详细介绍一下刷盘的流程。</p>
<p>首先定义几种角色,如下表所示。</p>
<p><img src="assets/Cgp9HWAqWQKAYYdQAAChiD3W3lQ653.png" alt="2.png" /></p>
<p><img src="assets/Cgp9HWAqWQKAYYdQAAChiD3W3lQ653.png" alt="png" /></p>
<p>数据首先写入当前内存表,当数据量到达阈值后,当前数据表把自身状态转换为刷盘中,并停止接受写入请求。此时会新建另一个内存表来接受写请求。刷盘完成后,由于数据在磁盘上,除了废弃内存表的数据外,还对提交日志进行截取操作。而后将新数据表设置为可以读取状态。</p>
<p>在合并操作开始时,将被合并的表设置为合并中状态,此时它们还可以接受读取操作。完成合并后,原表作废,新表开始启用提供读取服务。</p>
<p>以上就是经典的 LSM 树的结构和一些操作细节。下面我们开始介绍如何对其进行查询、更新和删除等操作。</p>
@@ -236,14 +236,14 @@ function hide_canvas() {
<p>常见的合并策略有 Size-Tiered Compaction 和 Leveled Compaction。</p>
<h4>Size-Tiered Compaction</h4>
<p>下图就是这种策略的合并过程。</p>
<p><img src="assets/CioPOWAqWQ6AH7acAACcL3NKUVQ048.png" alt="3.png" /></p>
<p><img src="assets/CioPOWAqWQ6AH7acAACcL3NKUVQ048.png" alt="png" /></p>
<p>其中数据表按照大小进行合并较小的数据表逐步合并为较大的数据表。第一层保存的是系统内最小的数据表它们是刚刚从内存表中刷新出来的。合并过程就是将低层较小的数据表合并为高层较大的数据表的过程。Apache Cassandra 使用过这种合并策略。</p>
<p>该策略的优点是比较简单,容易实现。但是它的空间放大性很差,合并时层级越高该问题越严重。比如有两个 5GB 的文件需要合并,那么磁盘至少要保留 10GB 的空间来完成这次操作,可想而知此种容量压力是巨大的,必然会造成系统不稳定。</p>
<p>那么有没有什么策略能缓解空间放大呢?答案就是 Leveled Compaction。</p>
<h4>Leveled Compaction</h4>
<p>如名称所示,该策略是将数据表进行分层,按照编号排成 L0 到 Ln 这样的多层结构。L0 层是从内存表刷盘产生的数据表,该层数据表中间的 key 是可以相交的L1 层及以上的数据,将 Size-Tiered Compaction 中原本的大数据表拆开,成为多个 key 互不相交的小数据表,每层都有一个最大数据量阈值,当到达该值时,就出发合并操作。每层的阈值是按照指数排布的,例如 RocksDB 文档中介绍了一种排布L1 是 300MB、L2 是 3GB、L3 是 30GB、L4 为 300GB。</p>
<p>该策略如下图所示。</p>
<p><img src="assets/Cgp9HWAqWRmAPoPlAACQe1Ek6yI202.png" alt="4.png" /></p>
<p><img src="assets/Cgp9HWAqWRmAPoPlAACQe1Ek6yI202.png" alt="png" /></p>
<p>上图概要性地展示了从 L1 层开始,每个小数据表的容量都是相同的,且数据量阈值是按 10 倍增长。即 L1 最多可以有 10 个数据表L2 最多可以有 100 个,以此类推。</p>
<p>随着数据表不断写入L1 的数据量会超过阈值。这时就会选择 L1 中的至少一个数据表,将其数据合并到 L2 层与其 key 有交集的那些文件中,并从 L1 中删除这些数据。</p>
<p>仍然以上图为例,一个 L1 层数据表的 key 区间大致能够对应到 10 个 L2 层的数据表,所以一次合并会影响 11 个文件。该次合并完成后L2 的数据量又有可能超过阈值,进而触发 L2 到 L3 的合并,如此往复。</p>

View File

@@ -262,7 +262,7 @@ function hide_canvas() {
<h3>总结</h3>
<p>这一讲是模块三的引导课,我首先为你介绍了失败模型的概念,它是描述分布式数据库内各种可能行为的一个准则;而后根据失败模型为你梳理了本模块的讲解思路。</p>
<p>分布式算法根据目标不同可能分为下面几种行为模式,这些模式与对应的课时如下表所示。</p>
<p><img src="assets/CioPOWA3btaADEsJAADf09O0yw4036.png" alt="image.png" /></p>
<p><img src="assets/CioPOWA3btaADEsJAADf09O0yw4036.png" alt="png" /></p>
</div>
</div>
<div>

View File

@@ -224,16 +224,16 @@ function hide_canvas() {
<li>一个节点向周围节点以一个固定的频率发送特定的数据包(称为心跳包),周围节点根据接收的频率判断该节点的健康状态。如果超出规定时间,未收到数据包,则认为该节点已经离线。</li>
</ol>
<p>可以看到这两种方法虽然实现细节不同,但都包含了一个所谓“规定时间”的概念,那就是超时机制。我们现在以第一种模式来详细介绍这种算法,请看下面这张图片。</p>
<p><img src="assets/Cgp9HWA4wn-ACDStAABEuFgWB6c085.png" alt="Drawing 0.png" /></p>
<p><img src="assets/Cgp9HWA4wn-ACDStAABEuFgWB6c085.png" alt="png" /></p>
<p>图 1 模拟两个连续心跳访问</p>
<p>上面的图模拟了两个连续心跳访问,节点 1 发送 ping 包,在规定的时间内节点 2 返回了 pong 包。从而节点 1 判断节点 2 是存活的。但在现实场景中经常会发生图 2 所示的情况。</p>
<p><img src="assets/CioPOWA4woqAFrAbAABF2-Jmi34588.png" alt="Drawing 2.png" /></p>
<p><img src="assets/CioPOWA4woqAFrAbAABF2-Jmi34588.png" alt="png" /></p>
<p>图 2 现实场景下的心跳访问</p>
<p>可以看到节点 1 发送 ping 后,节点没有在规定时间内返回 pong此时节点 1 又发送了另外的 ping。此种情况表明节点 2 存在延迟情况。偶尔的延迟在分布式场景中是极其常见的,故基于超时的心跳检测算法需要设置一个超时总数阈值。当超时次数超过该阈值后,才判断远程节点是离线状态,从而避免偶尔产生的延迟影响算法的准确性。</p>
<p>由上面的描述可知,基于超时的心跳检测法会为了调高算法的准确度,从而牺牲算法的效率。那有没有什么办法能改善算法的效率呢?下面我就要介绍一种不基于超时的心跳检测算法。</p>
<h4>不基于超时</h4>
<p>不基于超时的心跳检测算法是基于异步系统理论的。它保存一个全局节点的心跳列表,上面记录了每一个节点的心跳状态,从而可以直观地看到系统中节点的健康度。由此可知,该算法除了可以提高检测的效率外,还可以非常容易地获得所有节点的健康状态。那么这个全局列表是如何生成的呢?下图展示了该列表在节点之间的流转过程。</p>
<p><img src="assets/CioPOWA4wpWAZg3ZAABADm-xENc006.png" alt="Drawing 4.png" /></p>
<p><img src="assets/CioPOWA4wpWAZg3ZAABADm-xENc006.png" alt="png" /></p>
<p>图 3 全局列表在节点之间的流转过程</p>
<p>由图可知,该算法需要生成一个节点间的主要路径,该路径就是数据流在节点间最常经过的一条路径,该路径同时要包含集群内的所有节点。如上图所示,这条路径就是从节点 1 经过节点 2最后到达节点 3。</p>
<p>算法开始的时候,节点首先将自己记录到表格中,然后将表格发送给节点 2节点 2 首先将表格中的节点 1 的计数器加 1然后将自己记录在表格中而后发送给节点 3节点 3 如节点 2 一样,将其中的所有节点计数器加 1再把自己记录进去。一旦节点 3 发现所有节点全部被记录了,就停止这个表格的传播。</p>
@@ -242,7 +242,7 @@ function hide_canvas() {
<p>那么有没有方法能提高对于单一节点的判断呢?现在我就来介绍一种间接的检测方法。</p>
<h4>间接检测</h4>
<p>间接检测法可以有效提高算法的稳定性。它是将整个网络进行分组,我们不需要知道网络中所有节点的健康度,而只需要在子网中选取部分节点,它们会告知其相邻节点的健康状态。</p>
<p><img src="assets/Cgp9HWA4wp2AffksAABafzwFuLM251.png" alt="Drawing 6.png" /></p>
<p><img src="assets/Cgp9HWA4wp2AffksAABafzwFuLM251.png" alt="png" /></p>
<p>图 4 间接检测法</p>
<p>如图所示,节点 1 无法直接去判断节点 2 是否存活,这个时候它转而询问其相邻节点 3。由节点 3 去询问节点 2 的健康情况,最后将此信息由节点 3 返回给节点 1。</p>
<p>这种算法的好处是不需要将心跳检测进行广播,而是通过有限的网络连接,就可以检测到集群中各个分组内的健康情况,从而得知整个集群的健康情况。此种方法由于使用了组内的多个节点进行检测,其算法的准确度相比于一个节点去检测提高了很多。同时我们可以并行进行检测,算法的收敛速度也是很快的。因此可以说,<strong>间接检测法在准确度和效率上取得了比较好的平衡</strong></p>

View File

@@ -202,7 +202,7 @@ function hide_canvas() {
<p>现在我就和你一起,把一致性模型的知识体系补充完整。</p>
<h3>完整的一致性模型</h3>
<p>完整的一致性模型如下图所示。</p>
<p><img src="assets/Cgp9HWBCAs-AXQ4kAABf1EJoKHo006.png" alt="Drawing 0.png" /></p>
<p><img src="assets/Cgp9HWBCAs-AXQ4kAABf1EJoKHo006.png" alt="png" /></p>
<p>图中不同的颜色代表了可用性的程度,下面我来具体说说。</p>
<ol>
<li>粉色代表网络分区后完全不可用。也就是 CP 类的数据库。</li>
@@ -265,7 +265,7 @@ function hide_canvas() {
<p>由于目前 CRDT 算法仍然处于高速发展的阶段,为了方便你理解,我这里选取携程网内部 Redis 集群一致性方案,它的技术选型相对实用。如果你对 CRDT 有兴趣,可以进一步研究,这里就不对诸如 PN-Counter、G-Set 等做进一步说明了。</p>
<p>由于 Redis 最常用的处理手段是设置字符串数据,故需要使用 CRDT 中的 register 进行处理。携程团队选择了经典的 LWW Regsiter也就是最后写入胜利的冲突处理方案。</p>
<p>这种方案,最重要的是数据上需要携带时间戳。我们用下图来说明它的流程。</p>
<p><img src="assets/Cgp9HWBCAuiAPfgGAABs8POB6vo270.png" alt="Drawing 1.png" /></p>
<p><img src="assets/Cgp9HWBCAuiAPfgGAABs8POB6vo270.png" alt="png" /></p>
<p>从图中可以看到,每个节点的数据是一个二元组,分别是 value 和 timestamp。可以看到节点间合并数据是根据 timestamp也就是谁的 timestamp 大,合并的结果就以哪个值为准。使用 LWW Register 可以保证高并发下合并结果最终一致。</p>
<p>而删除时,就需要另外一种算法了。那就是 Observed-Remove SETOR Set其主要的目的是解决一般算法无法删除后重新增加该值的情况。</p>
<p>它相较于 LWW-Register 会复杂一些,除了时间戳以外,还需要给每个值标记一个唯一的 tag。比如上图中 P1 设置13实际需要设置1α3而后如果删除 1集合就为空再添加 1 时标签就需要与一开始不同5。这样就保证步骤 2 中的删除操作不会影响步骤 3 中的增加操作。因为它们虽然数值相同,但是标签不同,所以都是唯一的。</p>

View File

@@ -210,7 +210,7 @@ function hide_canvas() {
<p>随着熵逐步增加,系统进入越来越混乱的状态。但是如果没有读取操作,这种混乱其实是不会暴露出去的。那么人们就有了一个思路,我们可以在读取操作发生的时候再来修复不一致的数据。</p>
<p>具体操作是,请求由一个总的协调节点来处理,这个协调节点会从一组节点中查询数据,如果这组节点中某些节点有数据缺失,该协调节点就会把缺失的数据发送给这些节点,从而修复这些节点中的数据,达到反熵的目的。</p>
<p>有的同学可能会发现,这个思路与上一讲的可调节一致性有一些关联。因为在可调节一致性下,读取操作为了满足一致性要求,会从多个节点读取数据从而发现最新的数据结果。而读修复会更进一步,在此以后,会将落后节点数据进行同步修复,最后将最新结果发送回客户端。这一过程如下图所示。</p>
<p><img src="assets/CioPOWBIL8yAMlGZAAGZjMMkMrI651.png" alt="Drawing 0.png" /></p>
<p><img src="assets/CioPOWBIL8yAMlGZAAGZjMMkMrI651.png" alt="png" /></p>
<p>当修复数据时,读修复可以使用阻塞模式与异步模式两种。阻塞模式如上图所示,在修复完成数据后,再将最终结果返还给客户端;而异步模式会启动一个异步任务去修复数据,而不必等待修复完成的结果,即可返回到客户端。</p>
<p>你可以回忆一下,阻塞的读修复模式其实满足了上一讲中客户端一致性提到的<strong>读单增</strong>。因为一个值被读取后,下一次读取数据一定是基于上一次读取的。也就是说,同步修复的数据可以保证在下一次读取之前就被传播到目标节点;而异步修复就没有如此保证。但是阻塞修复同时丧失了一定的可用性,因为它需要等待远程节点修复数据,而异步修复就没有此问题。</p>
<p>在进行消息比较的时候,我们有一个优化的手段是使用散列来比较数据。比如协调节点收到客户端请求后,只向一个节点发送读取请求,而向其他节点发送散列请求。而后将完全请求的返回值进行散列计算,与其他节点返回的散列值进行比较。如果它们是相等的,就直接返回响应;如果不相等,将进行上文所描述的修复过程。</p>
@@ -218,7 +218,7 @@ function hide_canvas() {
<p>以上就是在读取操作中进行的反熵操作,那么在写入阶段我们如何进行修复呢?下面我来介绍暗示切换。</p>
<h4>暗示切换</h4>
<p>暗示切换名字听起来很玄幻。其实原理非常明了,让我们看看它的过程,如下图所示。</p>
<p><img src="assets/Cgp9HWBIL9WALPvqAAGcHTvEnf0629.png" alt="Drawing 1.png" /></p>
<p><img src="assets/Cgp9HWBIL9WALPvqAAGcHTvEnf0629.png" alt="png" /></p>
<p>客户端首先写入协调节点。而后协调节点将数据分发到两个节点中这个过程与可调节一致性中的写入是类似的。正常情况下可以保证写入的两个节点数据是一致的。如果其中的一个节点失败了系统会启动一个新节点来接收失败节点之后的数据这个结构一般会被实现为一个队列Queue即暗示切换队列HHQ</p>
<p>一旦失败的节点恢复了回来HHQ 会把该节点离线这一个时间段内的数据同步到该节点中,从而修复该节点由于离线而丢失的数据。这就是在写入节点进行反熵的操作。</p>
<p>以上介绍的前台同步操作其实都有一个限制就是需要假设此种熵增过程发生的概率不高且范围有限。如果熵增大范围产生那么修复读会造成读取延迟增高即使使用异步修复也会产生很高的冲突。而暗示切换队列的问题是其容量是有限的这意味着对于一个长期离线的节点HHQ 可能无法保存其全部的消息。</p>
@@ -228,7 +228,7 @@ function hide_canvas() {
<p>而后台方案与前台方案的关注点是不同的。前台方案重点放在修复数据而后台方案由于需要比较和处理大量的非活跃数据故需要重点解决如何使用更少的资源来进行数据比对。我将要为你介绍两种比对技术Merkle 树和位图版本向量。</p>
<h4>Merkle 树</h4>
<p>如果想要检查数据的差异,我们一般能想到最直观的方式是进行全量比较。但这种思路效率是很低的,在实际生产中不可能实行。而通过 Merkle 树我们可以快速找到两份数据之间的差异,下图就是一棵典型的 Merkle 树。</p>
<p><img src="assets/Cgp9HWBIL96AR7YaAAA7C1vVQBU503.png" alt="Drawing 2.png" /></p>
<p><img src="assets/Cgp9HWBIL96AR7YaAAA7C1vVQBU503.png" alt="png" /></p>
<p>树构造的过程是:</p>
<ol>
<li>将数据划分为多个连续的段。而后计算每个段的哈希值,得到 hash1 到 hash4 这四个值;</li>
@@ -240,7 +240,7 @@ function hide_canvas() {
<h4>位图版本向量</h4>
<p>最近的研究发现,大部分数据差异还是发生在距离当前时间不远的时间段。那么我们就可以针对此种场景进行优化,从而避免像 Merkle 树那样计算全量的数据。而位图版本向量就是根据这个想法发展起来的。</p>
<p>这种算法利用了位图这一种对内存非常友好的高密度数据格式,将节点近期的数据同步状态记录下来;而后通过比较各个节点间的位图数据,从而发现差异,修复数据。下面我用一个例子为你展示这种算法的执行过程,请看下图。</p>
<p><img src="assets/CioPOWBIL-eAF5kCAAAo07ziIqo508.png" alt="Drawing 3.png" /></p>
<p><img src="assets/CioPOWBIL-eAF5kCAAAo07ziIqo508.png" alt="png" /></p>
<p>如果有三个节点,每个节点包含了一组与其他节点数据同步的向量。上图表示节点 2 的数据同步情况。目前系统中存在 8 条数据,从节点 2 的角度看,每个节点都没有完整的数据。其中深灰色的部分表明同步的数据是连续的,我们用一个压缩的值表示。节点 1 到 3 这个压缩的值分别为 3、5 和 2。可以看到节点 2 自己的数据是连续的。</p>
<p>数据同步一旦出现不连续的情况,也就是出现了空隙,我们就转而使用位图来存储。也就是图中浅灰色和白色的部分。比如节点 2 观察节点 1可以看到有三个连续的数据同步而后状态用 00101 来表示(浅灰色代表 1白色代表 0。其中 1 是数据同步了,而 0 是数据没有同步。节点 2 可以从节点 1 和节点 3 获取完整的 8 条数据。</p>
<p>这种向量列表除了具有内存优势外,我们还可以很容易发现需要修复数据的目标。但是它的一个明显缺点与暗示切换队列 HHQ 类似,就是存储是有限的,如果数据偏差非常大,向量最终会溢出,从而不能比较数据间的差异。但不要紧,我们可以用上面提到的 Merkle 来进行全量比较。</p>

View File

@@ -239,15 +239,15 @@ function hide_canvas() {
<li>bal:lock 存 start_ts=&gt;(primary cell)Primary cell 是 Rowkey 和列名的组合,它在提交容错处理和事务冲突时使用,用来清理由于协调器失败导致的事务失败留下的锁信息。</li>
</ol>
<p>我们现在用一个例子来介绍一下整个过程,请看下图。</p>
<p><img src="assets/Cgp9HWBPCKiAbXCoAAB0tHTHvis535.png" alt="Drawing 0.png" /></p>
<p><img src="assets/Cgp9HWBPCKiAbXCoAAB0tHTHvis535.png" alt="png" /></p>
<p>一个账户表中Bob 有 10 美元Joe 有 2 美元。我们可以看到 Bob 的记录在 write 字段中最新的数据是 <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="f793968396b7c2">[email&#160;protected]</a>,它表示当前最新的数据是 ts=5 那个版本的数据ts=5 版本中的数据是 10 美元,这样读操作就会读到这个 10 美元。同理Joe 的账号是 2 美元。</p>
<p><img src="assets/Cgp9HWBPCK-ABB4wAAC8TLGF6II238.png" alt="Drawing 1.png" /></p>
<p><img src="assets/Cgp9HWBPCK-ABB4wAAC8TLGF6II238.png" alt="png" /></p>
<p>现在我们要做一个转账操作,从 Bob 账户转 7 美元到 Joe 账户。这需要操作多行数据这里是两行。首先需要加锁Percolator 从要操作的行中随机选择一行作为 Primary Row其余为 Secondary Row。对 Primary Row 加锁,成功后再对 Secondary Row 加锁。从上图我们看到,在 ts=7 的行 lock 列写入了一个锁I am primary该行的 write 列是空的,数据列值为 310-7=3。 此时 ts=7 为 start_ts。</p>
<p><img src="assets/Cgp9HWBPCLqAAl_ZAAE417OCMCw175.png" alt="Drawing 2.png" /></p>
<p><img src="assets/Cgp9HWBPCLqAAl_ZAAE417OCMCw175.png" alt="png" /></p>
<p>然后对 Joe 账户加锁,同样是 ts=7在 Joe 账户的加锁信息中包含了指向 Primary lock 的引用如此这般处于同一个事务的行就关联起来了。Joe 的数据列写入 9(2+7=9)write 列为空,至此完成 Prewrite 阶段。</p>
<p><img src="assets/Cgp9HWBPCMKAR9C1AAEYmGw4fnE874.png" alt="Drawing 3.png" /></p>
<p><img src="assets/Cgp9HWBPCMKAR9C1AAEYmGw4fnE874.png" alt="png" /></p>
<p>接下来事务就要 Commit 了。Primary Row 首先执行 Commit只要 Primary Row Commit 成功了事务就成功了。Secondary Row 失败了也不要紧后续会有补救措施。Commit 操作首先清除 Primary Row 的锁,然后写入 ts=8 的行(因为时间是单向递增的,这里是 commit_ts该行可以称为 Commit Row因为它不包含数据只是在 write 列中写入 <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="4f2b2e3b2e0f78">[email&#160;protected]</a>,标识 ts=7 的数据已经可见了,此刻以后的读操作可以读到版本 ts=7 的数据了。</p>
<p><img src="assets/CioPOWBPCMmARbSjAAC7HQrDF0I862.png" alt="Drawing 4.png" /></p>
<p><img src="assets/CioPOWBPCMmARbSjAAC7HQrDF0I862.png" alt="png" /></p>
<p>接下来就是 commit Secondary Row 了,和 Primary Row 的逻辑是一样的。Secondary Row 成功 commit事务就完成了。</p>
<p>如果 Primary Row commit 成功Secondary Row commit 失败会怎么样,数据的一致性如何保障?由于 Percolator 没有中心化的事务管理器组件,处理这种异常,只能在下次读操作发起时进行。如果一个读请求发现要读的数据存在 Secondary 锁,它会根据 Secondary Row 锁去检查其对应的 Primary Row 的锁是不是还存在若存在说明事务还没有完成若不存在则说明Primary Row 已经 Commit 了,它会清除 Secondary Row 的锁使该行数据变为可见状态commit。这是一个 Roll forward 的概念。</p>
<p>我们可以看到,在这样一个存储系统中,并非所有的行都是数据,还包含了一些事务控制行,或者称为 Commit Row。它的数据 Column 为空,但 write 列包含了可见数据的 TS。它的作用是标示事务完成并指引读请求读到新的数据。随着时间的推移会产生大量冗余的数据行无用的数据行会被 GC 线程定时清理。</p>

View File

@@ -216,7 +216,7 @@ function hide_canvas() {
<li>快照读顾名思义Spanner 实现了 MVCC 和快照隔离故读取操作在整个事务内部是一致的。同时这也暗示了Spanner 可以保存同一份数据的多个版本。</li>
</ol>
<p>了解了事务模型后,我们深入其内部,看看 Spanner 的核心组件都有哪些。下面是一张 Spanner 的架构图。</p>
<p><img src="assets/Cgp9HWBRzJSAaOkIAAHp1kcPK04475.png" alt="Drawing 0.png" /></p>
<p><img src="assets/Cgp9HWBRzJSAaOkIAAHp1kcPK04475.png" alt="png" /></p>
<p>其中我们看到,每个 replica 保存了多个 tablet同时这些 replica 组成了 Paxos Group。Paxos Group 选举出一个 leader 用来在多分片事务中与其他 Paxos Group 的 leader 进行协调(有关 Paxos 算法的细节我将在下一讲中介绍)。</p>
<p>写入操作必须通过 leader 来进行,而读取操作可以在任何一个同步完成的 replica 上进行。同时我们看到 leader 中有锁管理器,用来实现并发控制中提到的锁管理。事务管理器用来处理多分片分布式事务。当进行同步写入操作时,必须要获取锁,而快照读取操作是无锁操作。</p>
<p>我们可以看到,最复杂的操作就是多分片的写入操作。其过程就是由 leader 参与的两阶段提交。在准备阶段,提交的数据写入到协调器的 Paxos Group 中,这解决了如下两个问题。</p>
@@ -233,7 +233,7 @@ function hide_canvas() {
<p>Spanner 引入了很多新技术去改善分布式事务的性能,但我们发现其流程整体还是传统的二阶段提交,并没有在结构上发生重大的改变,而 Calvin 却充满了颠覆性。让我们来看看它是怎么处理分布式事务的。</p>
<p>首先传统分布式事务处理使用到了锁来保证并发竞争的事务满足隔离级别的约束。比如序列化级别保证了事务是一个接一个运行的。而每个副本的执行顺序是无法预测的但结果是可以预测的。Calvin 的方案是让事务在每个副本上的执行顺序达到一致,那么执行结果也肯定是一致的。这样做的好处是避免了众多事务之间的锁竞争,从而大大提高了高并发度事务的吞吐量。同时,节点崩溃不影响事务的执行。因为事务执行步骤已经分配,节点恢复后从失败处接着运行该事务即可,<strong>这种模式使分布式事务的可用性也大大提高</strong>。目前实现了 Calvin 事务模式的数据库是 FaunaDB。</p>
<p>其次,将事务进行排序的组件被称为 sequencer。它搜集事务信息而后将它们拆解为较小的 epoch这样做的目的是减小锁竞争并提高并行度。一旦事务被准备好sequencer 会将它们发送给 scheduler。scheduler 根据 sequencer 处理的结果适时地并行执行部分事务步骤同时也保证顺序执行的步骤不会被并行。因为这些步骤已经排好了顺序scheduler 执行的时候不需要与 sequencer 进行交互从而提高了执行效率。Calvin 事务的处理组件如下图所示。</p>
<p><img src="assets/Cgp9HWBRzJ-AHNkNAAIDRYF-wko605.png" alt="Drawing 1.png" /></p>
<p><img src="assets/Cgp9HWBRzJ-AHNkNAAIDRYF-wko605.png" alt="png" /></p>
<p>Calvin 也使用了 Paxos 算法,不同于 Spanner 每个分片有一个 Paxos Group。Calvin 使用 Paxos 或者异步复制来决定哪个事务需要进入哪个 epoch 里面。</p>
<p>同时 Calvin 事务有 read set 和 write set 的概念。前者表示事务需要读取的数据,后者表示事务影响的数据。这两个集合需要在事务开始前就进行确定,故<strong>Calvin 不支持在事务中查询动态数据而后影响最终结果集的行为。这一点很重要,是这场战争的核心</strong></p>
<p>在你了解了两种事务模型之后我就要带你进入“刺激战场”了。在两位实力相当的选手中Calvin 一派首先挑起了战争。</p>

View File

@@ -213,7 +213,7 @@ function hide_canvas() {
<h3>TiDB使用乐观事务打造悲观事务</h3>
<p>在分布式事务那一讲,我提到 TiDB 的乐观事务使用了 Google 的 Percolator 模式,同时 TiDB 也对该模式进行了改进。可以说一提到 Percolator 模式事务的数据库,国内外都绕不过 TiDB。</p>
<p>TiDB 在整体架构上基本参考了 Google Spanner 和 F1 的设计,分两层为 TiDB 和 TiKV。 TiDB 对应的是 Google F1是一层无状态的 SQL Layer兼容绝大多数 MySQL 语法,对外暴露 MySQL 网络协议,负责解析用户的 SQL 语句,生成分布式的 Query Plan翻译成底层 Key Value 操作发送给 TiKV。TiKV 是真正的存储数据的地方,对应的是 Google Spanner是一个分布式 Key Value 数据库,支持弹性水平扩展,自动地灾难恢复和故障转移,以及 ACID 跨行事务。下面的图展示了 TiDB 的架构。</p>
<p><img src="assets/CioPOWBbAfyAX5EPAAHEn31cNYM835.png" alt="Drawing 0.png" /></p>
<p><img src="assets/CioPOWBbAfyAX5EPAAHEn31cNYM835.png" alt="png" /></p>
<p>对于事务部分TiDB 实现悲观事务的方式是非常简洁的。其团队在仔细研究了 Percolator 的模型后发现,其实只要将在客户端调用 Commit 时候进行两阶段提交这个行为稍微改造一下,将第一阶段上锁和等锁提前到事务中执行 DML 的过程中,就可以简单高效地支持悲观事务场景。</p>
<p>TiDB 的悲观锁实现的原理是,在一个事务执行 DMLUPDATE/DELETE的过程中TiDB 不仅会将需要修改的行在本地缓存,同时还会对这些行直接上悲观锁,这里的悲观锁的格式和乐观事务中的锁几乎一致,但是锁的内容是空的,只是一个占位符,等到 Commit 的时候,直接将这些悲观锁改写成标准的 Percolator 模型的锁,后续流程和原来保持一致即可。</p>
<p>这个方案在很大程度上兼容了原有的事务实现,其扩展性、高可用和灵活性都有保证。同时该方案尽最大可能复用了原有 Percolator 的乐观事务方案,减少了事务模型整体的复杂度。</p>
@@ -239,11 +239,11 @@ function hide_canvas() {
<p>Cassandra 的可调节一致性如我在本模块一致性那一讲介绍的一样,分为写一致性与读一致性。</p>
<h4>写一致性</h4>
<p>写一致性声明了需要写入多少个节点才算一次成功的写入。Cassandra 的写一致性是可以在强一致到弱一致之间进行调整的。我总结了下面的表格来为你说明。</p>
<p><img src="assets/CioPOWBbAgqAAj2MAACioCvzYAU571.png" alt="Drawing 1.png" /></p>
<p><img src="assets/CioPOWBbAgqAAj2MAACioCvzYAU571.png" alt="png" /></p>
<p>我们可以看到 ANY 级别实际上对应了最终一致性。Cassandra 使用了反熵那一讲提到的暗示切换技术来保障写入的数据的可靠,也就是写入节点一旦失败,数据会暂存在暗示切换队列中,等到节点恢复后数据可以被还原出来。</p>
<h4>读一致性</h4>
<p>对于读操作,一致性级别指定了返回数据之前必须有多少个副本节点响应这个读查询。这里同样给你整理了一个表格。</p>
<p><img src="assets/CioPOWBbAhKAF13EAACR3twAd-Q592.png" alt="Drawing 2.png" /></p>
<p><img src="assets/CioPOWBbAhKAF13EAACR3twAd-Q592.png" alt="png" /></p>
<p>Cassandra 在读取的时候使用了读修复来修复副本上的过期数据,该修复过程是一个后台线程,故不会阻塞读取。</p>
<p>以上就是 Apache Cassandra 实现可调节一致性的一些细节。AWS 的 DynamoDB、Azure 的 CosmosDB 都有类似的可调节一致性供用户进行选择。你可以比照 Cassandra 的模式和这些数据库的文档进行学习。</p>
<h3>总结</h3>

View File

@@ -248,10 +248,10 @@ function hide_canvas() {
<p>单体开源数据要向分布式数据库演进,就要解决写入性能不足的问题。</p>
<p>最简单直接的办法就是分库分表。分库分表方案就是在多个单体数据库之前增加代理节点,本质上是增加了 SQL 路由功能。这样,代理节点首先解析客户端请求,再根据数据的分布情况,将请求转发到对应的单体数据库。代理节点分为“客户端 + 单体数据库”和“中间件 + 单体数据库”两个模式。</p>
<p>客户端组件 + 单体数据库通过独立的逻辑层建立数据分片和路由规则,实现单体数据库的初步管理,使应用能够对接多个单体数据库,实现并发、存储能力的扩展。其作为应用系统的一部分,对业务侵入比较深。这种客户端组件的典型产品是 Apache ShardingShpere 的 JDBC 客户端模式,下图就是该模式的架构图。</p>
<p><img src="assets/CioPOWBhewWAL2D3AADkzhnu3iE396.png" alt="Drawing 0.png" /></p>
<p><img src="assets/CioPOWBhewWAL2D3AADkzhnu3iE396.png" alt="png" /></p>
<p>Apache ShardingShpere 的 JDBC 客户端模式架构图</p>
<p>代理中间件 + 单体数据库以独立中间件的方式,管理数据规则和路由规则,以独立进程存在,与业务应用层和单体数据库相隔离,减少了对应用的影响。随着代理中间件的发展,还会衍生出部分分布式事务处理能力。这种中间件的典型产品是 MyCat、Apache ShardingShpere 的 Proxy 模式。</p>
<p><img src="assets/CioPOWBhexWAcIBeAAJA7FyRXw0760.png" alt="Drawing 1.png" /></p>
<p><img src="assets/CioPOWBhexWAcIBeAAJA7FyRXw0760.png" alt="png" /></p>
<p>Apache ShardingShpere 的 Proxy 模式架构图</p>
<p>代理节点需要实现三个主要功能,它们分别是客户端接入、简单的查询处理器和进程管理中的访问控制。另外,分库分表方案还有一个重要的功能,那就是分片信息管理,分片信息就是数据分布情况。不过考虑分片信息也存在多副本的一致性的问题,大多数情况下它会独立出来。显然,如果把每一次的事务写入都限制在一个单体数据库内,业务场景就会很受局限。</p>
<p>因此,跨库事务成为必不可少的功能,但是单体数据库是不感知这个事情的,所以我们就要在代理节点增加分布式事务组件。同时,简单的分库分表不能满足全局性的查询需求,因为每个数据节点只能看到一部分数据,有些查询运算是无法处理的,比如排序、多表关联等。所以,代理节点要增强查询计算能力,支持跨多个单体数据库的查询。更多相关内容我会在下一讲介绍。</p>

View File

@@ -216,7 +216,7 @@ function hide_canvas() {
<p>下面就按照我给出的定义中的关键点来向你详细介绍 NewSQL 数据库。</p>
<h3>创新的架构</h3>
<p>使用创新的数据库架构是 NewSQL 数据库非常引人注目的特性。这种新架构一般不会依靠任何遗留的代码这与我在“22 | 发展与局限:传统数据库在分布式领域的探索”中介绍的依赖传统数据库作为计算存储节点非常不同。我们以 TiDB 这个典型的 NewSQL 数据库为例。</p>
<p><img src="assets/Cgp9HWBmw-WATtXZAAHEn31cNYM969.png" alt="image.png" /></p>
<p><img src="assets/Cgp9HWBmw-WATtXZAAHEn31cNYM969.png" alt="png" /></p>
<p>可以看到其中的创新点有以下几个。</p>
<ol>
<li>存储引擎没有使用传统数据库。而使用的是新型基于 LSM 的 KV 分布式存储引擎,有些数据库使用了完全内存形式的存储引擎,比如 NuoDB。</li>

View File

@@ -211,7 +211,7 @@ function hide_canvas() {
<li>高效的异地数据同步。</li>
</ol>
<p>如下面的架构图所示,应用层通过 Cobar 访问数据库。</p>
<p><img src="assets/Cgp9HWBzrSaABPOUAAH9MECqjKQ062.png" alt="image.png" /></p>
<p><img src="assets/Cgp9HWBzrSaABPOUAAH9MECqjKQ062.png" alt="png" /></p>
<p>其对数据库的访问分为读操作select和写操作update、insert和delete。写操作会在数据库上产生变更记录MySQL 的变更记录叫 binlogOracle 的变更记录叫 redolog。Erosa 产品解析这些变更记录,并以统一的格式缓存至 Eromanga 中后者负责管理变更数据的生产者、Erosa 和消费者之间的关系,负责跨机房数据库同步的 Otter 是这些变更数据的消费者之一。</p>
<p>Cobar 可谓 OLTP 分布式数据库解决方案的先驱,至今其中的思想还可以从现在的中间件,甚至 NewSQL 数据库中看到。但在阿里集团服役三年后,由于人员变动而逐步停止维护。这个时候 MyCAT 开源社区接过了该项目的衣钵,在其上增加了诸多功能并进行 bug 修改,最终使其在多个行业中占用自己的位置。</p>
<p>但是就像我曾经介绍的那样,中间件产品并不是真正的分布式数据库,它有自己的局限。比如 SQL 支持、查询性能、分布式事务、运维能力,等等,都有不可逾越的天花板。而有一些中间件产品幸运地得以继续进阶,最终演化为 NewSQL甚至是云原生产品。阿里云的 PolarDB 就是这种类型的代表,它的前身是阿里云的分库分表中间件产品 DRDS而 DRDS 来源于淘宝系的 TDDL 中间件。</p>