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

@@ -221,9 +221,9 @@ function hide_canvas() {
<p>这些优秀的特性,让 ShardingSphere 在分库分表中间件领域占据了领先地位,并被越来越多的知名企业(比如京东、当当、电信、中通快递、哔哩哔哩等)用来构建自己强大而健壮的数据平台。如果你苦于找不到一款成熟稳定的分库分表中间件,那么 ShardingSphere 恰能帮助你解决这个痛点。</p>
<h3>你为什么需要学习这个课程?</h3>
<p>但凡涉及海量数据处理的企业,就一定要用到分库分表。如何进行海量数据的分库分表设计和迁移,有效存储和访问海量业务数据,已经成为很多架构师和开发人员需要规划和落实的一大课题,也成为像拼多多、趣头条、爱库存等很多优质公司高薪诚聘的岗位需求。</p>
<p><img src="assets/Ciqc1F7nA9iABbXVAAA_IpEATYs695.png" alt="image" /></p>
<p><img src="assets/CgqCHl7nA9-AabMiAAA82MVypLo920.png" alt="image" /></p>
<p><img src="assets/Ciqc1F7nA-aAbYVSAABEj2zbJek328.png" alt="image" /></p>
<p><img src="assets/Ciqc1F7nA9iABbXVAAA_IpEATYs695.png" alt="png" /></p>
<p><img src="assets/CgqCHl7nA9-AabMiAAA82MVypLo920.png" alt="png" /></p>
<p><img src="assets/Ciqc1F7nA-aAbYVSAABEj2zbJek328.png" alt="png" /></p>
<p>但优质人才非常短缺,一是因为从事海量数据处理需要相应的应用场景和较高的技术门槛,二是业界也缺乏成熟的框架来完成实际需求。掌握诸如 ShardingSphere 这样的主流分库分表和分布式数据库中间件框架的技术人员也成了各大公司争抢的对象。</p>
<p>鉴于市面上还没有对 ShardingSphere 进行系统化介绍的内容,我希望能来弥补这个空白。此外,分库分表概念虽然比较简单,但在实际开发过程中要落地却也不容易,也需要一个系统的、由浅入深的学习过程。</p>
<h3>课程设计</h3>
@@ -245,7 +245,7 @@ function hide_canvas() {
<p>帮你理解 ShardingSphere 的核心功能特性,来满足日常开发工作所需,同时基于源码给出这些功能的设计原理和实现机制。</p>
<p><strong>2.</strong> <strong>学习优秀的开源框架,提高技术理解与应用能力</strong></p>
<p>技术原理是具有相通性的。以 ZooKeeper 这个分布式协调框架为例ShardingSphere 和 Dubbo 中都使用它来完成了注册中心的构建:</p>
<p><img src="assets/CgqCHl7nA4GAbjKUAABqNKIcNmc812.png" alt="image" /></p>
<p><img src="assets/CgqCHl7nA4GAbjKUAABqNKIcNmc812.png" alt="png" /></p>
<p>在 ShardingSphere 中,我们可以基于 ZooKeeper 提供的动态监听机制来判断某个数据库实例是否可用、是否需要对某个数据库实例进行数据访问熔断等操作,也可以使用 ZooKeeper 的这一功能特性来实现分布式环境下的配置信息动态管理。</p>
<p>随着对 ShardingSphere 的深入学习,你会发现类似的例子还有很多,包括基于 SPI 机制的微内核架构、基于雪花算法的分布式主键、基于 Apollo 的配置中心、基于 Nacos 的注册中心、基于 Seata 的柔性事务、基于 OpenTracing 规范的链路跟踪等。</p>
<p>而这些技术体系在 Dubbo、Spring Cloud 等主流开发框架中也多有体现。因此这个课程除了可以强化你对这些技术体系的系统化理解,还可以让你掌握这些技术体系的具体应用场景和实现方式,从而实现触类旁通。</p>

View File

@@ -216,16 +216,16 @@ function hide_canvas() {
<p>分库分表的表现形式也有很多种,一起来看一下。</p>
<h4>分库分表的表现形式</h4>
<p>分库分表包括分库和分表两个维度,在开发过程中,对于每个维度都可以采用两种拆分思路,即<strong>垂直拆分</strong><strong>水平拆分</strong></p>
<p><img src="assets/Ciqc1F7nGACANU7zAADej4U_wPQ598.png" alt="image" /></p>
<p><img src="assets/Ciqc1F7nGACANU7zAADej4U_wPQ598.png" alt="png" /></p>
<p>先来讨论垂直拆分的应用方式,相比水平拆分,垂直拆分相对比较容易理解和实现。在电商系统中,用户在打开首页时,往往会加载一些用户性别、地理位置等基础数据。对于用户表而言,这些位于首页的基础数据访问频率显然要比用户头像等数据更高。基于这两种数据的不同访问特性,可以把用户单表进行拆分,将访问频次低的用户头像等信息单独存放在一张表中,把访问频次较高的用户信息单独放在另一张表中:</p>
<p><img src="assets/Ciqc1F7nGDCARTTCAAC8BTUPGAU300.png" alt="image" /></p>
<p><img src="assets/Ciqc1F7nGDCARTTCAAC8BTUPGAU300.png" alt="png" /></p>
<p>从这里可以看到,<strong>垂直分表的处理方式就是将一个表按照字段分成多张表,每个表存储其中一部分字段。</strong> 在实现上,我们通常会把头像等 blob 类型的大字段数据或热度较低的数据放在一张独立的表中,将经常需要组合查询的列放在一张表中,这也可以认为是分表操作的一种表现形式。</p>
<p>通过垂直分表能得到一定程度的性能提升,但数据毕竟仍然位于同一个数据库中,也就是把操作范围限制在一台服务器上,每个表还是会竞争同一台服务器中的 CPU、内存、网络 IO 等资源。基于这个考虑,在有了垂直分表之后,就可以进一步引入垂直分库。</p>
<p>对于前面介绍的场景,分表之后的用户信息同样还是跟其他的商品、订单信息存放在同一台服务器中。基于垂直分库思想,这时候就可以把用户相关的数据表单独拆分出来,放在一个独立的数据库中。</p>
<p><img src="assets/CgqCHl7nGD2AVKdWAAFiWjlhmkI041.png" alt="image" /></p>
<p><img src="assets/CgqCHl7nGD2AVKdWAAFiWjlhmkI041.png" alt="png" /></p>
<p>这样的效果就是垂直分库。从定义上讲,垂直分库是指按照业务将表进行分类,然后分布到不同的数据库上。然后,每个库可以位于不同的服务器上,其核心理念是<strong>专库专用</strong>。而从实现上讲,垂直分库很大程度上取决于业务的规划和系统边界的划分。比如说,用户数据的独立拆分就需要考虑到系统用户体系与其他业务模块之间的关联关系,而不是简单地创建一个用户库就可以了。在高并发场景下,垂直分库能够在一定程度上提升 IO 访问效率和数据库连接数,并降低单机硬件资源的瓶颈。</p>
<p>从前面的分析中我们不难明白,垂直拆分尽管实现起来比较简单,但并不能解决单表数据量过大这一核心问题。所以,现实中我们往往需要在垂直拆分的基础上添加水平拆分机制。例如,可以对用户库中的用户信息按照用户 ID 进行取模,然后分别存储在不同的数据库中,这就是水平分库的常见做法:</p>
<p><img src="assets/CgqCHl7piT6AICl8AAEF_s70Scc942.png" alt="image" /></p>
<p><img src="assets/CgqCHl7piT6AICl8AAEF_s70Scc942.png" alt="png" /></p>
<p>可以看到,水平分库是把同一个表的数据按一定规则拆分到不同的数据库中,每个库同样可以位于不同的服务器上。这种方案往往能解决单库存储量及性能瓶颈问题,但由于同一个表被分配在不同的数据库中,数据的访问需要额外的路由工作,因此大大提升了系统复杂度。这里所谓的规则实际上就是一系列的算法,常见的包括:</p>
<ul>
<li><strong>取模算法</strong>,取模的方式有很多,比如前面介绍的按照用户 ID 进行取模,当然也可以通过表的一列或多列字段进行 hash 求值来取模;</li>
@@ -233,7 +233,7 @@ function hide_canvas() {
<li><strong>预定义算法</strong>,是指事先规划好具体库或表的数量,然后直接路由到指定库或表中。</li>
</ul>
<p>按照水平分库的思路,也可以对用户库中的用户表进行水平拆分,效果如下图所示。也就是说,<strong>水平分表是在同一个数据库内,把同一个表的数据按一定规则拆到多个表中</strong></p>
<p><img src="assets/CgqCHl7nGFGAXKk0AACCxF6OwYE181.png" alt="image" /></p>
<p><img src="assets/CgqCHl7nGFGAXKk0AACCxF6OwYE181.png" alt="png" /></p>
<p>显然,系统的数据存储架构演变到现在已经非常复杂了。与拆分前的单库单表相比,现在面临着一系列具有挑战性的问题,比如:</p>
<ul>
<li>如何对多数据库进行高效治理?</li>
@@ -247,10 +247,10 @@ function hide_canvas() {
<p>如果没有很好的工具来支持数据的存储和访问,数据一致性将很难得到保障,这就是以 ShardingSphere 为代表的分库分表中间件的价值所在。</p>
<h4>分库分表与读写分离</h4>
<p>说到分库分表,我们不得不介绍另一个解决数据访问瓶颈的技术体系:<strong>读写分离</strong>,这个技术与数据库主从架构有关。我们知道像 MySQL 这样的数据库提供了完善的主从架构,能够确保主数据库与从数据库之间的数据同步。基于主从架构,就可以按照操作要求对读操作和写操作进行分离,从而提升访问效率。读写分离的基本原理是这样的:</p>
<p><img src="assets/Ciqc1F7nGF-AaBJ0AACkmf13Mrs619.png" alt="image" /></p>
<p><img src="assets/Ciqc1F7nGF-AaBJ0AACkmf13Mrs619.png" alt="png" /></p>
<p>可以看到图中的数据库集群中存在一个主库,也存在一个从库,主库和从库之间通过同步机制实现两者数据的一致性。在互联网系统中,普遍认为对数据库读操作的频率要远远高于写操作,所以瓶颈往往出现在读操作上。通过读写分离,就可以把读操作分离出来,在独立的从库上进行。现实中的主从架构,主库和从库的数量,尤其从库的数量都是可以根据数据量的大小进行扩充的。</p>
<p><strong>读写分离,主要解决的就是高并发下的数据库访问,也是一种常用的解决方案</strong>。但是跟提升服务器配置一样,并不是终极解决方案。终极的解决方案还是前面介绍的分库分表,按照用户 ID 等规则来拆分库或拆分表。但是,请注意,分库分表与读写分离之间的关系并不是互斥的,而是可以相辅相成的,完全可以在分库分表的基础上引入读写分离机制:</p>
<p><img src="assets/CgqCHl7nGGyALUltAAES1F4HWIA969.png" alt="image" /></p>
<p><img src="assets/CgqCHl7nGGyALUltAAES1F4HWIA969.png" alt="png" /></p>
<p>事实上,本课程所要介绍的 ShardingSphere 就实现了图中的架构方案,在分库分表的同时支持读写分离,在后续的课程中将会介绍如何实现这一过程。</p>
<h3>分库分表解决方案和代表框架</h3>
<p>基于前面关于分库分表的讨论,我们可以抽象其背后的一个核心概念,即<strong>分片Sharding</strong>。无论是分库还是分表,都是把数据划分成不同的数据片,并存储在不同的目标对象中。而具体的分片方式涉及实现分库分表的不同解决方案。</p>
@@ -258,20 +258,20 @@ function hide_canvas() {
<h4>客户端分片</h4>
<p>所谓客户端分片,相当于在数据库的客户端就实现了分片规则。显然,这种方式将分片处理的工作进行前置,客户端管理和维护着所有的分片逻辑,并决定每次 SQL 执行所对应的目标数据库和数据表。</p>
<p>客户端分片这一解决方案也有不同的表现形式,其中最为简单的方式就是<strong>应用层分片</strong>,也就是说在应用程序中直接维护着分片规则和分片逻辑:</p>
<p><img src="assets/Ciqc1F7nGHqAdJD9AACzVB1hs2Y585.png" alt="image" /></p>
<p><img src="assets/Ciqc1F7nGHqAdJD9AACzVB1hs2Y585.png" alt="png" /></p>
<p>在具体实现上,我们通常会将分片规则的处理逻辑打包成一个公共 JAR 包,其他业务开发人员只需要在代码工程中引入这个 JAR 包即可。针对这种方案,因为没有独立的服务器组件,所以也不需要专门维护某一个具体的中间件。然而,这种直接在业务代码中嵌入分片组件的方法也有明显的缺点:</p>
<ul>
<li>一方面,由于分片逻辑侵入到了业务代码中,业务开发人员在理解业务的基础上还需要掌握分片规则的处理方式,增加了开发和维护成本;</li>
<li>另一方面,一旦出现问题,也只能依赖业务开发人员通过分析代码来找到原因,而无法把这部分工作抽离出来让专门的中间件团队进行完成。</li>
</ul>
<p>基于以上分析,客户端分片在实现上通常会进一步抽象,把分片规则的管理工作从业务代码中剥离出来,形成单独演进的一套体系。这方面典型的设计思路是重写 JDBC 协议,也就是说在 JDBC 协议层面嵌入分片规则。这样,业务开发人员还是使用与 JDBC 规范完全兼容的一套 API 来操作数据库,但这套 API 的背后却自动完成了分片操作,从而实现了对业务代码的零侵入:</p>
<p><img src="assets/CgqCHl7nGIaAcKbyAADgC4g1mPo753.png" alt="image" />
<p><img src="assets/CgqCHl7nGIaAcKbyAADgC4g1mPo753.png" alt="png" />
客户端分片结构重写JDBC协议</p>
<p>这种解决方案的优势在于,分片操作对于业务而言是完全透明的,从而一定程度上实现业务开发人员与数据库中间件团队在职责上的分离。这样,业务开发人员只需要理解 JDBC 规范就可以完成分库分表,开发难度以及代码维护成本得到降低。</p>
<p>对于客户端分片,典型的中间件包括阿里巴巴的 TDDL 以及本课程将要介绍的 ShardingSphere。因为 TDDL 并没有开源,所以无法判断客户端分片的具体实现方案。而对于 ShardingSphere 而言,它是重写 JDBC 规范以实现客户端分片的典型实现框架。</p>
<h4>代理服务器分片</h4>
<p>代理服务器分片的解决方案也比较明确,也就是采用了代理机制,在应用层和数据库层之间添加一个代理层。有了代理层之后,就可以把分片规则集中维护在这个代理层中,并对外提供与 JDBC 兼容的 API 给到应用层。这样,应用层的业务开发人员就不用关心具体的分片规则,而只需要完成业务逻辑的实现:</p>
<p><img src="assets/Ciqc1F7nGJKABmlgAAD2wWQcUJM917.png" alt="image" /></p>
<p><img src="assets/Ciqc1F7nGJKABmlgAAD2wWQcUJM917.png" alt="png" /></p>
<p>显然,代理服务器分片的优点在于解放了业务开发人员对分片规则的管理工作,而缺点就是添加了一层代理层,所以天生具有代理机制所带来的一些问题,比方说因为新增了一层网络传输对性能所产生的影响。</p>
<p>对于代理服务器分片,常见的开源框架有阿里的 Cobar 以及民间开源社区的 MyCat。而在 ShardingSphere 3.X 版本中,也添加了 Sharding-Proxy 模块来实现代理服务器分片。</p>
<h4>分布式数据库</h4>

View File

@@ -203,11 +203,11 @@ function hide_canvas() {
<p>在上一课时中我详细分析了分库分表的表现形式以及分片架构的解决方案和代表性框架。可以看到ShardingSphere 同时实现了客户端分片和代理服务器组件并提供了分布式数据库的相关功能特性。作为一款优秀的开源软件ShardingSphere 能够取得目前的成就也不是一蹴而就,下面我们先来回顾一下 ShardingSphere 的发展历程。</p>
<h3>ShardingSphere 的发展历程:从 Sharding-JDBC 到 Apache 顶级项目</h3>
<p>说到 ShardingSphere 的起源,我们不得不提 Sharding-JDBC 框架,该框架是一款起源于当当网内部的应用框架,并于 2017 年初正式开源。从 Sharding-JDBC 到 Apache 顶级项目ShardingSphere 的发展经历了不同的演进阶段。纵观整个 ShardingSphere 的发展历史,我们可以得到时间线与阶段性里程碑的演进过程图:</p>
<p><img src="assets/CgqCHl7-6OWAVF7WAACmgRfIB7Y558.png" alt="1.png" /></p>
<p><img src="assets/CgqCHl7-6OWAVF7WAACmgRfIB7Y558.png" alt="png" /></p>
<p>从版本发布角度,我们也可以进一步梳理 ShardingSphere 发展历程中主线版本与核心功能之间的演进关系图:</p>
<p><img src="assets/Ciqc1F7rSMqAGrqLAABmt5OcOuc557.png" alt="2.png" /></p>
<p><img src="assets/Ciqc1F7rSMqAGrqLAABmt5OcOuc557.png" alt="png" /></p>
<p>基于 GitHub 上星数的增长轨迹,也可以从另一个维度很好地反映出 ShardingSphere 的发展历程:</p>
<p><img src="assets/CgqCHl7rSNaAE3gNAADqRUDMk-w922.png" alt="3.png" /></p>
<p><img src="assets/CgqCHl7rSNaAE3gNAADqRUDMk-w922.png" alt="png" /></p>
<h3>ShardingSphere 的设计理念:不是颠覆,而是兼容</h3>
<p>对于一款开源中间件来说,要得到长足的发展,一方面依赖于社区的贡献,另外在很大程度上还取决于自身的设计和发展理念。</p>
<p>ShardingSphere 的定位非常明确就是一种关系型数据库中间件而并非一个全新的关系型数据库。ShardingSphere 认为,在当下,关系型数据库依然占有巨大市场,但凡涉及数据的持久化,关系型数据库仍然是系统的标准配置,也是各个公司核心业务的基石,在可预见的未来中,这点很难撼动。所以,<strong>ShardingSphere 在当前阶段更加关注在原有基础上进行兼容和扩展,而非颠覆</strong>。那么 ShardingSphere 是如何做到这一点呢?</p>
@@ -215,20 +215,20 @@ function hide_canvas() {
<h4>Sharding-JDBC</h4>
<p>ShardingSphere 的前身是 Sharding-JDBC所以这是整个框架中最为成熟的组件。Sharding-JDBC 的定位是一个轻量级 Java 框架,在 JDBC 层提供了扩展性服务。我们知道 JDBC 是一种开发规范,指定了 DataSource、Connection、Statement、PreparedStatement、ResultSet 等一系列接口。而各大数据库供应商通过实现这些接口提供了自身对 JDBC 规范的支持,使得 JDBC 规范成为 Java 领域中被广泛采用的数据库访问标准。</p>
<p>基于这一点Sharding-JDBC 一开始的设计就完全兼容 JDBC 规范Sharding-JDBC 对外暴露的一套分片操作接口与 JDBC 规范中所提供的接口完全一致。开发人员只需要了解 JDBC就可以使用 Sharding-JDBC 来实现分库分表Sharding-JDBC 内部屏蔽了所有的分片规则和处理逻辑的复杂性。显然,<strong>这种方案天生就是一种具有高度兼容性的方案,能够为开发人员提供最简单、最直接的开发支持</strong>。关于 Sharding-JDBC 与 JDBC 规范的兼容性话题,我们将会在下一课时中详细讨论。</p>
<p><img src="assets/CgqCHl7rSOuAXZt6AAC4cmjERnk488.png" alt="4.png" />
<p><img src="assets/CgqCHl7rSOuAXZt6AAC4cmjERnk488.png" alt="png" />
Sharding-JDBC 与 JDBC 规范的兼容性示意图</p>
<p>在实际开发过程中Sharding-JDBC 以 JAR 包的形式提供服务。<strong>开发人员可以使用这个 JAR 包直连数据库,无需额外的部署和依赖管理</strong>。在应用 Sharding-JDBC 时,需要注意到 Sharding-JDBC 背后依赖的是一套完整而强大的分片引擎:</p>
<p><img src="assets/CgqCHl7rSPSAUJHuAADsN1Pqjqs981.png" alt="5.png" /></p>
<p><img src="assets/CgqCHl7rSPSAUJHuAADsN1Pqjqs981.png" alt="png" /></p>
<p>由于 Sharding-JDBC 提供了一套与 JDBC 规范完全一致的 API所以它可以很方便地与遵循 JDBC 规范的各种组件和框架进行无缝集成。例如,用于提供数据库连接的 DBCP、C3P0 等数据库连接池组件,以及用于提供对象-关系映射的 Hibernate、MyBatis 等 ORM 框架。当然作为一款支持多数据库的开源框架Sharding-JDBC 支持 MySQL、Oracle、SQLServer 等主流关系型数据库。</p>
<h4>Sharding-Proxy</h4>
<p>ShardingSphere 中的 Sharding-Proxy 组件定位为一个透明化的数据库代理端所以它是代理服务器分片方案的一种具体实现方式。在代理方案的设计和实现上Sharding-Proxy 同样充分考虑了兼容性。</p>
<p>Sharding-Proxy 所提供的兼容性首先体现在对异构语言的支持上为了完成对异构语言的支持Sharding-Proxy 专门对数据库二进制协议进行了封装,并提供了一个代理服务端组件。其次,从客户端组件上讲,针对目前市面上流行的 Navicat、MySQL Command Client 等客户端工具Sharding-Proxy 也能够兼容遵循 MySQL 和 PostgreSQL 协议的各类访问客户端。当然,和 Sharding-JDBC 一样Sharding-Proxy 也支持 MySQL 和 PostgreSQL 等多种数据库。</p>
<p>接下来,我们看一下 Sharding-Proxy 的整体架构。对于应用程序而言,这种代理机制是完全透明的,可以直接把它当作 MySQL 或 PostgreSQL 进行使用:</p>
<p><img src="assets/CgqCHl7rSQKAC1u6AADoQEq6hys890.png" alt="6.png" /></p>
<p><img src="assets/CgqCHl7rSQKAC1u6AADoQEq6hys890.png" alt="png" /></p>
<p>总结一下,我们可以直接把 Sharding-Proxy 视为一个数据库,用来代理后面分库分表的多个数据库,它屏蔽了后端多个数据库的复杂性。同时,也看到 Sharding-Proxy 的运行同样需要依赖于完成分片操作的分片引擎以及用于管理数据库的治理组件。</p>
<p>虽然 Sharding-JDBC 和 Sharding-Proxy 具有不同的关注点,但事实上,我们完全可以将它们整合在一起进行使用,也就是说这两个组件之间也存在兼容性。</p>
<p>前面已经介绍过,我们使用 Sharding-JDBC 的方式是在应用程序中直接嵌入 JAR 包,这种方式适合于业务开发人员。而 Sharding-Proxy 提供静态入口以及异构语言的支持,适用于需要对分片数据库进行管理的中间件开发和运维人员。基于底层共通的分片引擎,以及数据库治理功能,可以混合使用 Sharding-JDBC 和 Sharding-Proxy以便应对不同的应用场景和不同的开发人员</p>
<p><img src="assets/Ciqc1F7rSRCAS66OAAECtLTiErU161.png" alt="7.png" /></p>
<p><img src="assets/Ciqc1F7rSRCAS66OAAECtLTiErU161.png" alt="png" /></p>
<h4>Sharding-Sidecar</h4>
<p>Sidecar 设计模式受到了越来越多的关注和采用这个模式的目标是把系统中各种异构的服务组件串联起来并进行高效的服务治理。ShardingSphere 也基于该模式设计了 Sharding-Sidecar 组件。截止到目前ShardingSphere 给出了 Sharding-Sidecar 的规划,但还没有提供具体的实现方案,这里不做具体展开。作为 Sidecar 模式的具体实现,我们可以想象 <strong>Sharding-Sidecar</strong>** 的作用就是以 Sidecar 的形式代理所有对数据库的访问**。这也是一种兼容性的设计思路,通过无中心、零侵入的方案将分布式的数据访问应用与数据库有机串联起来。</p>
<h3>ShardingSphere 的核心功能:从数据分片到编排治理</h3>
@@ -239,7 +239,7 @@ Sharding-JDBC 与 JDBC 规范的兼容性示意图</p>
<li>微内核架构</li>
</ul>
<p>ShardingSphere 在设计上采用了<strong>微内核MicroKernel架构模式</strong>,来确保系统具有高度可扩展性。微内核架构包含两部分组件,即内核系统和插件。使用微内核架构对系统进行升级,要做的只是用新插件替换旧插件,而不需要改变整个系统架构:</p>
<p><img src="assets/CgqCHl7rSSCAcY4sAABRDnG4TnQ180.png" alt="8.png" /></p>
<p><img src="assets/CgqCHl7rSSCAcY4sAABRDnG4TnQ180.png" alt="png" /></p>
<p>在 ShardingSphere 中,抽象了一大批插件接口,包含用实现 SQL 解析的 SQLParserEntry、用于实现配置中心的 ConfigCenter、用于数据脱敏的 ShardingEncryptor以及用于数据库治理的注册中心接口 RegistryCenter 等。开发人员完全可以根据自己的需要,基于这些插件定义来提供定制化实现,并动态加载到 ShardingSphere 运行时环境中。</p>
<ul>
<li>分布式主键</li>

View File

@@ -204,7 +204,7 @@ function hide_canvas() {
<p>这个问题非常重要,值得我们专门花一个课时的内容来进行分析和讲解。可以说,<strong>理解 JDBC 规范以及 ShardingSphere 对 JDBC 规范的重写方式,是正确使用 ShardingSphere 实现数据分片的前提</strong>。今天,我们就深入讨论 JDBC 规范与 ShardingSphere 的这层关系,帮你从底层设计上解开其中的神奇之处。</p>
<h3>JDBC 规范简介</h3>
<p>ShardingSphere 提供了与 JDBC 规范完全兼容的实现过程,在对这一过程进行详细展开之前,先来回顾一下 JDBC 规范。<strong>JDBCJava Database Connectivity的设计初衷是提供一套用于各种数据库的统一标准</strong>而不同的数据库厂家共同遵守这套标准并提供各自的实现方案供应用程序调用。作为统一标准JDBC 规范具有完整的架构体系,如下图所示:</p>
<p><img src="assets/CgqCHl7xtaiASay6AAB0vuO1kAA457.png" alt="Drawing 0.png" /></p>
<p><img src="assets/CgqCHl7xtaiASay6AAB0vuO1kAA457.png" alt="png" /></p>
<p>JDBC 架构中的 Driver Manager 负责加载各种不同的驱动程序Driver并根据不同的请求向调用者返回相应的数据库连接Connection。而应用程序通过调用 JDBC API 来实现对数据库的操作。<strong>对于开发人员而言JDBC API 是我们访问数据库的主要途径,也是 ShardingSphere 重写 JDBC 规范并添加分片功能的入口</strong>。如果我们使用 JDBC 开发一个访问数据库的处理流程,常见的代码风格如下所示:</p>
<pre><code>// 创建池化的数据源
PooledDataSource dataSource = new PooledDataSource ();
@@ -239,10 +239,10 @@ connection.close();
}
</code></pre>
<p>可以看到DataSource 接口提供了两个获取 Connection 的重载方法,并继承了 CommonDataSource 接口,该接口是 JDBC 中关于数据源定义的根接口。除了 DataSource 接口之外,它还有两个子接口:</p>
<p><img src="assets/CgqCHl7xtbuALDZqAABj4c2IofU664.png" alt="Drawing 1.png" /></p>
<p><img src="assets/CgqCHl7xtbuALDZqAABj4c2IofU664.png" alt="png" /></p>
<p>其中DataSource 是官方定义的获取 Connection 的基础接口ConnectionPoolDataSource 是从连接池 ConnectionPool 中获取的 Connection 接口。而 XADataSource 则用来实现在分布式事务环境下获取 Connection我们在讨论 ShardingSphere 的分布式事务时会接触到这个接口。</p>
<p><strong>请注意DataSource 接口同时还继承了一个 Wrapper 接口</strong>。从接口的命名上看,可以判断该接口应该起到一种包装器的作用,事实上,由于很多数据库供应商提供了超越标准 JDBC API 的扩展功能所以Wrapper 接口可以把一个由第三方供应商提供的、非 JDBC 标准的接口包装成标准接口。以 DataSource 接口为例,如果我们想要实现自己的数据源 MyDataSource就可以提供一个实现了 Wrapper 接口的 MyDataSourceWrapper 类来完成包装和适配:</p>
<p><img src="assets/CgqCHl7xtdOAZEGNAABnV-ZtNrk288.png" alt="Drawing 2.png" /></p>
<p><img src="assets/CgqCHl7xtdOAZEGNAABnV-ZtNrk288.png" alt="png" /></p>
<p>在 JDBC 规范中,除了 DataSource 之外Connection、Statement、ResultSet 等核心对象也都继承了这个接口。显然ShardingSphere 提供的就是非 JDBC 标准的接口,所以也应该会用到这个 Wrapper 接口,并提供了类似的实现方案。</p>
<h4>Connection</h4>
<p>DataSource 的目的是获取 Connection 对象,我们可以把 Connection 理解为一种<strong>会话Session机制</strong>。Connection 代表一个数据库连接,负责完成与数据库之间的通信。所有 SQL 的执行都是在某个特定 Connection 环境中进行的,同时它还提供了一组重载方法,分别用于创建 Statement 和 PreparedStatement。另一方面Connection 也涉及事务相关的操作为了实现分片操作ShardingSphere 同样也实现了定制化的 Connection 类 ShardingConnection。</p>
@@ -252,14 +252,14 @@ connection.close();
<h4>ResultSet</h4>
<p>一旦通过 Statement 或 PreparedStatement 执行了 SQL 语句并获得了 ResultSet 对象后,那么就可以通过调用 Resulset 对象中的 next() 方法遍历整个结果集。如果 next() 方法返回为 true就意味结果集中存在数据则可以调用 ResultSet 对象的一系列 getXXX() 方法来取得对应的结果值。对于分库分表操作而言因为涉及从多个数据库或数据表中获取目标数据势必需要对获取的结果进行归并。因此ShardingSphere 中也提供了分片环境下的 ShardingResultSet 对象。</p>
<p>作为总结,我们梳理了基于 JDBC 规范进行数据库访问的开发流程图,如下图所示:</p>
<p><img src="assets/Ciqc1F7xteqAQsj5AAB1bj_eu10085.png" alt="Drawing 3.png" /></p>
<p><img src="assets/Ciqc1F7xteqAQsj5AAB1bj_eu10085.png" alt="png" /></p>
<p>ShardingSphere 提供了与 JDBC 规范完全兼容的 API。也就是说开发人员可以基于这个开发流程和 JDBC 中的核心接口完成分片引擎、数据脱敏等操作,我们来看一下。</p>
<h3>基于适配器模式的 JDBC 重写实现方案</h3>
<p>在 ShardingSphere 中,实现与 JDBC 规范兼容性的基本策略就是采用了设计模式中的适配器模式Adapter Pattern<strong>适配器模式通常被用作连接两个不兼容接口之间的桥梁,涉及为某一个接口加入独立的或不兼容的功能。</strong></p>
<p>作为一套适配 JDBC 规范的实现方案ShardingSphere 需要对上面介绍的 JDBC API 中的 DataSource、Connection、Statement 及 ResultSet 等核心对象都完成重写。虽然这些对象承载着不同功能但重写机制应该是共通的否则就需要对不同对象都实现定制化开发显然这不符合我们的设计原则。为此ShardingSphere 抽象并开发了一套基于适配器模式的实现方案,整体结构是这样的,如下图所示:</p>
<p><img src="assets/Ciqc1F7xtfeAIlV7AABhpWkSy7c199.png" alt="Drawing 4.png" /></p>
<p><img src="assets/Ciqc1F7xtfeAIlV7AABhpWkSy7c199.png" alt="png" /></p>
<p>首先,我们看到这里有一个 JdbcObject 接口,这个接口泛指 JDBC API 中的 DataSource、Connection、Statement 等核心接口。前面提到,这些接口都继承自包装器 Wrapper 接口。ShardingSphere 为这个 Wrapper 接口提供了一个实现类 WrapperAdapter这点在图中得到了展示。在 ShardingSphere 代码工程 sharding-jdbc-core 的 org.apache.shardingsphere.shardingjdbc.jdbc.adapter 包中包含了所有与 Adapter 相关的实现类:</p>
<p><img src="assets/Ciqc1F7xtgWAb3PaAAAW8D9SY1w475.png" alt="Drawing 5.png" /></p>
<p><img src="assets/Ciqc1F7xtgWAb3PaAAAW8D9SY1w475.png" alt="png" /></p>
<p>在 ShardingSphere 基于适配器模式的实现方案图的底部,有一个 ShardingJdbcObject 类的定义。这个类也是一种泛指,代表 ShardingSphere 中用于分片的 ShardingDataSource、ShardingConnection、ShardingStatement 等对象。</p>
<p>最后发现 ShardingJdbcObject 继承自一个 AbstractJdbcObjectAdapter而 AbstractJdbcObjectAdapter 又继承自 AbstractUnsupportedOperationJdbcObject这两个类都是抽象类而且也都泛指一组类。两者的区别在于AbstractJdbcObjectAdapter 只提供了针对 JdbcObject 接口的一部分实现方法,这些方法是我们完成分片操作所需要的。而对于那些我们不需要的方法实现,则全部交由 AbstractUnsupportedOperationJdbcObject 进行实现,这两个类的所有方法的合集,就是原有 JdbcObject 接口的所有方法定义。</p>
<p>这样,我们大致了解了 ShardingSphere 对 JDBC 规范中核心接口的重写机制。这个重写机制非常重要,在 ShardingSphere 中应用也很广泛,我们可以通过示例对这一机制做进一步理解。</p>
@@ -267,10 +267,10 @@ connection.close();
<p>通过前面的介绍,我们知道 ShardingSphere 的分片引擎中提供了一系列 ShardingJdbcObject 来支持分片操作,包括 ShardingDataSource、ShardingConnection、ShardingStatement、ShardingPreparedStament 等。这里以最具代表性的 ShardingConnection 为例,来讲解它的实现过程。请注意,今天我们关注的还是重写机制,不会对 ShardingConnection 中的具体功能以及与其他类之间的交互过程做过多展开讲解。</p>
<h4>ShardingConnection 类层结构</h4>
<p>ShardingConnection 是对 JDBC 中 Connection 的适配和包装,所以它需要提供 Connection 接口中定义的方法,包括 createConnection、getMetaData、各种重载的 prepareStatement 和 createStatement 以及针对事务的 setAutoCommit、commit 和 rollback 方法等。ShardingConnection 对这些方法都进行了重写,如下图所示:</p>
<p><img src="assets/CgqCHl7xthSAHp4DAACJDJsvmyk879.png" alt="Drawing 6.png" />
<p><img src="assets/CgqCHl7xthSAHp4DAACJDJsvmyk879.png" alt="png" />
ShardingConnection 中的方法列表图</p>
<p>ShardingConnection 类的一条类层结构支线就是适配器模式的具体应用,这部分内容的类层结构与前面介绍的重写机制的类层结构是完全一致的,如下图所示:</p>
<p><img src="assets/CgqCHl9MudqAOb9wAAEeGv0YTn807.jpeg" alt="111.jpeg" /></p>
<p><img src="assets/CgqCHl9MudqAOb9wAAEeGv0YTn807.jpeg" alt="png" /></p>
<h4>AbstractConnectionAdapter</h4>
<p>我们首先来看看 AbstractConnectionAdapter 抽象类ShardingConnection 直接继承了它。在 AbstractConnectionAdapter 中发现了一个 cachedConnections 属性,它是一个 Map 对象,该对象其实缓存了这个经过封装的 ShardingConnection 背后真实的 Connection 对象。如果我们对一个 AbstractConnectionAdapter 重复使用,那么这些 cachedConnections 也会一直被缓存,直到调用 close 方法。可以从 AbstractConnectionAdapter 的 getConnections 方法中理解具体的操作过程:</p>
<pre><code>public final List&lt;Connection&gt; getConnections(final ConnectionMode connectionMode, final String dataSourceName, final int connectionSize) throws SQLException {

View File

@@ -202,7 +202,7 @@ function hide_canvas() {
<p>在上一课时中,我详细介绍了 ShardingSphere 与 JDBC 规范之间的兼容性关系,我们知道 ShardingSphere 对 JDBC 规范进行了重写,并嵌入了分片机制。基于这种兼容性,开发人员使用 ShardingSphere 时就像在使用 JDBC 规范所暴露的各个接口一样。这一课时,我们将讨论如何在业务系统中使用 ShardingSphere 的具体方式。</p>
<h3>如何抽象开源框架的应用方式?</h3>
<p>当我们自己在设计和实现一款开源框架时如何规划它的应用方式呢作为一款与数据库访问相关的开源框架ShardingSphere 提供了多个维度的应用方式,我们可以对这些应用方式进行抽象,从而提炼出一种模版。这个模版由四个维度组成,分别是<strong>底层工具、基础规范、开发框架和领域框架</strong>,如下图所示:</p>
<p><img src="assets/CgqCHl75qv-AFbZvAACz7F_yXRM280.png" alt="2.png" /></p>
<p><img src="assets/CgqCHl75qv-AFbZvAACz7F_yXRM280.png" alt="png" /></p>
<h4>底层工具</h4>
<p>底层工具指的是这个开源框架所面向的目标工具或所依赖的第三方工具。这种底层工具往往不是框架本身可以控制和管理的,框架的作用只是在它上面添加一个应用层,用于封装对这些底层工具的使用方式。</p>
<p>对于 ShardingSphere 而言,<strong>这里所说的底层工具实际上指的是关系型数据库</strong>。目前ShardingSphere 支持包括 MySQL、Oracle、SQLServer、PostgreSQL 以及任何遵循 SQL92 标准的数据库。</p>
@@ -247,7 +247,7 @@ spring.shardingsphere.datasource.test_datasource.password=root
<h3>开发框架集成</h3>
<p>从上面所介绍的配置信息中,你实际上已经看到了 ShardingSphere 中集成的两款主流开发框架,即 Spring 和 Spring Boot它们都对 JDBC 规范做了封装。当然,对于没有使用或无法使用 Spring 家族框架的场景,我们也可以直接在原生 Java 应用程序中使用 ShardingSphere。</p>
<p>在介绍开发框架的具体集成方式之前,我们来设计一个简单的应用场景。假设系统中存在一个用户表 User这张表的数据量比较大所以我们将它进行分库分表处理计划分成两个数据库 ds0 和 ds1然后每个库中再分成两张表 user0 和 user1</p>
<p><img src="assets/Ciqc1F75qxSADY5yAADgZQ5r488284.png" alt="1.png" /></p>
<p><img src="assets/Ciqc1F75qxSADY5yAADgZQ5r488284.png" alt="png" /></p>
<p>接下来,让我们来看一下如何基于 Java 原生、Spring 及 Spring Boot 开发框架针对这一场景实现分库分表。</p>
<h4>Java 原生</h4>
<p>如果使用 Java 原生的开发方式,相当于我们需要全部通过 Java 代码来创建和管理 ShardingSphere 中与分库分表相关的所有类。如果不做特殊说明,本课程将默认使用 Maven 实现包依赖关系的管理。所以,首先需要引入对 sharding-jdbc-core 组件的 Maven 引用:</p>

View File

@@ -204,7 +204,7 @@ function hide_canvas() {
<p>在引入配置体系的学习之前,我们先来介绍 ShardingSphere 框架为开发人员提供的一个辅助功能,这个功能就是行表达式。</p>
<p><strong>行表达式是 ShardingSphere 中用于实现简化和统一配置信息的一种工具,在日常开发过程中应用得非常广泛。</strong> 它的使用方式非常直观,只需要在配置中使用 ${expression} 或 $-&gt;{expression} 表达式即可。</p>
<p>例如上一课时中使用的&quot;ds${0..1}.user${0..1}&quot;就是一个行表达式,用来设置可用的数据源或数据表名称。基于行表达式语法,${begin..end} 表示的是一个从&quot;begin&quot;&quot;end&quot;的范围区间,而多个 ${expression} 之间可以用&quot;.&quot;符号进行连接,代表多个表达式数值之间的一种笛卡尔积关系。这样,如果采用图形化的表现形式,&quot;ds${0..1}.user${0..1}&quot;表达式最终会解析成这样一种结果:</p>
<p><img src="assets/CgqCHl75rReAY_fbAACRt1sYKS0524.png" alt="image" /></p>
<p><img src="assets/CgqCHl75rReAY_fbAACRt1sYKS0524.png" alt="png" /></p>
<p>当然,类似场景也可以使用枚举的方式来列举所有可能值。行表达式也提供了 ${[enum1, enum2,…, enumx]} 语法来表示枚举值,所以&quot;ds${0..1}.user${0..1}&quot;的效果等同于&quot;ds${[0,1]}.user${[0,1]}&quot;</p>
<p>同样,在上一课时中使用到的 ds${age % 2} 表达式,它表示根据 age 字段进行对 2 取模,从而自动计算目标数据源是 ds0 还是 ds1。所以除了配置数据源和数据表名称之外行表达式在 ShardingSphere 中另一个常见的应用场景就是配置各种分片算法,我们会在后续的示例中大量看到这种使用方法。</p>
<p>由于 ${expression} 与 Spring 本身的属性文件占位符冲突,而 Spring 又是目前主流的开发框架,因此在正式环境中建议你使用 $-&gt;{expression} 来进行配置。</p>
@@ -213,7 +213,7 @@ function hide_canvas() {
<h4>ShardingRuleConfiguration</h4>
<p>我们在上一课时中已经了解了如何通过框架之间的集成方法来创建一个 DataSource这个 DataSource 就是我们使用 ShardingSphere 的入口。我们也看到在创建 DataSource 的过程中使用到了一个 ShardingDataSourceFactory 类,这个工厂类的构造函数中需要传入一个 ShardingRuleConfiguration 对象。显然,从命名上看,这个 ShardingRuleConfiguration 就是用于分片规则的配置入口。</p>
<p>ShardingRuleConfiguration 中所需要配置的规则比较多,我们可以通过一张图例来进行简单说明,在这张图中,我们列举了每个配置项的名称、类型以及个数关系:</p>
<p><img src="assets/Ciqc1F75rTaADRO6AAD5MCLrohA562.png" alt="image" /></p>
<p><img src="assets/Ciqc1F75rTaADRO6AAD5MCLrohA562.png" alt="png" /></p>
<p>这里引入了一些新的概念,包括绑定表、广播表等,这些概念在下一课时介绍到 ShardingSphere 的分库分表操作时都会详细展开,这里不做具体介绍。<strong>事实上,对于 ShardingRuleConfiguration 而言,必须要设置的只有一个配置项,即 TableRuleConfiguration。</strong></p>
<h4>TableRuleConfiguration</h4>
<p>从命名上看TableRuleConfiguration 是表分片规则配置但事实上这个类同时包含了对分库和分表两种场景的设置。TableRuleConfiguration 包含很多重要的配置项:</p>
@@ -235,7 +235,7 @@ function hide_canvas() {
<p>keyGeneratorConfig 代表分布式环境下的自增列生成器配置ShardingSphere 中集成了雪花算法等分布式 ID 的生成器实现。</p>
<h4>ShardingStrategyConfiguration</h4>
<p>我们注意到databaseShardingStrategyConfig 和 tableShardingStrategyConfig 的类型都是一个 ShardingStrategyConfiguration 对象。在 ShardingSphere 中ShardingStrategyConfiguration 实际上是一个空接口,存在一系列的实现类,其中的每个实现类都代表一种分片策略:</p>
<p><img src="assets/CgqCHl75rUiACYZ1AAA8JV6ve54396.png" alt="3.png" />
<p><img src="assets/CgqCHl75rUiACYZ1AAA8JV6ve54396.png" alt="png" />
 ShardingStrategyConfiguration 的类层结构图</p>
<p>在这些具体的分片策略中,通常需要指定一个分片列 shardingColumn 以及一个或多个分片算法 ShardingAlgorithm。当然也有例外例如 HintShardingStrategyConfiguration 直接使用数据库的 Hint 机制实现强制路由,所以不需要分片列。我们会在《路由引擎:如何在路由过程中集成多种分片策略和分片算法?》中对这些策略的实现过程做详细的剖析。</p>
<h4>KeyGeneratorConfiguration</h4>
@@ -440,7 +440,7 @@ spring.shardingsphere.masterslave.slave-data-source-names=dsslave0,dsslave1
}
</code></pre>
<p>可以看到 createDataSource 方法的输入参数是一个 File 对象,我们通过这个 File 对象构建出 YamlRootShardingConfiguration 对象,然后再通过 YamlRootShardingConfiguration 对象获取了 ShardingRuleConfiguration 对象,并交由 ShardingDataSourceFactory 完成目标 DataSource 的构建。这里的调用关系有点复杂,我们来梳理整个过程的类层结构,如下图所示:</p>
<p><img src="assets/Ciqc1F75rqaAGL8qAABR8QxogB0683.png" alt="image" /></p>
<p><img src="assets/Ciqc1F75rqaAGL8qAABR8QxogB0683.png" alt="png" /></p>
<p>显然这里引入了两个新的工具类YamlEngine 和 YamlSwapper。我们来看一下它们在整个流程中起到的作用。</p>
<h4>YamlEngine 和 YamlSwapper</h4>
<p>YamlEngine 的作用是将各种形式的输入内容转换成一个 Yaml 对象,这些输入形式包括 File、字符串、byte[] 等。YamlEngine 包含了一批 unmarshal/marshal 方法来完成数据的转换。以 File 输入为例unmarshal 方法通过加载 FileInputStream 来完成 Yaml 对象的构建:</p>

View File

@@ -213,7 +213,7 @@ function hide_canvas() {
</code></pre>
<h4>XA 事务</h4>
<p>XA 事务提供基于两阶段提交协议的实现机制。所谓两阶段提交,顾名思义分成两个阶段,一个是准备阶段,一个是执行阶段。在准备阶段中,协调者发起一个提议,分别询问各参与者是否接受。在执行阶段,协调者根据参与者的反馈,提交或终止事务。如果参与者全部同意则提交,只要有一个参与者不同意就终止。</p>
<p><img src="assets/CgqCHl8MBruAHHnkAABo-3eRic0694.png" alt="Drawing 0.png" /></p>
<p><img src="assets/CgqCHl8MBruAHHnkAABo-3eRic0694.png" alt="png" /></p>
<p>两阶段提交示意图</p>
<p>目前,业界在实现 XA 事务时也存在一些主流工具库,包括 Atomikos、Narayana 和 Bitronix。ShardingSphere 对这三种工具库都进行了集成,并默认使用 Atomikos 来完成两阶段提交。</p>
<h4>BASE 事务</h4>
@@ -386,14 +386,14 @@ public void insert(){
}
</code></pre>
<p>现在让我们执行这个 processWithXA 方法,看看数据是否已经按照分库的配置写入到目标数据库表中。下面是 ds0 中的 health_record 表和 health_task 表:</p>
<p><img src="assets/Ciqc1F8MB4yADpvNAAAn7gHRWyw024.png" alt="Drawing 2.png" /></p>
<p><img src="assets/Ciqc1F8MB4yADpvNAAAn7gHRWyw024.png" alt="png" /></p>
<p>ds0 中的 health_record 表</p>
<p><img src="assets/Ciqc1F8MCEuAUA1NAAAuoAPD9w4209.png" alt="Drawing 3.png" /></p>
<p><img src="assets/Ciqc1F8MCEuAUA1NAAAuoAPD9w4209.png" alt="png" /></p>
<p>ds0 中的 health_task 表</p>
<p>下面则是 ds1 中的 health_record 表和 health_task 表:
<img src="assets/CgqCHl8MB6SAOFIhAAAoGKuCLOw688.png" alt="Drawing 4.png" /></p>
<img src="assets/CgqCHl8MB6SAOFIhAAAoGKuCLOw688.png" alt="png" /></p>
<p>ds1 中的 health_record 表</p>
<p><img src="assets/Ciqc1F8MCFiAH4szAAAvGNmTj1Y923.png" alt="Drawing 5.png" /></p>
<p><img src="assets/Ciqc1F8MCFiAH4szAAAvGNmTj1Y923.png" alt="png" /></p>
<p>ds1 中的 health_task 表</p>
<p>我们也可以通过控制台日志来跟踪具体的 SQL 执行过程:</p>
<pre><code>2020-06-01 20:11:52.043 INFO 10720 --- [ main] ShardingSphere-SQL : Rule Type: sharding
@@ -454,7 +454,7 @@ public void insert(){
}
</code></pre>
<p>现在,在 src/main/resources 目录下的文件组织形式应该是这样:</p>
<p><img src="assets/Ciqc1F8MB7aAL-kkAAAU1FYPsK0495.png" alt="Drawing 6.png" /></p>
<p><img src="assets/Ciqc1F8MB7aAL-kkAAAU1FYPsK0495.png" alt="png" /></p>
<p>当然,这里我们还是继续沿用前面介绍的分库配置。</p>
<h3>实现 BASE 事务</h3>
<p>基于 ShardingSphere 提供的分布式事务的抽象,我们从 XA 事务转到 BASE 事务唯一要做的事情就是重新设置 TransactionType也就是修改一行代码</p>

View File

@@ -202,7 +202,7 @@ function hide_canvas() {
<p>从今天开始,我们又将开始一个全新的主题:介绍 ShardingSphere 中的数据脱敏功能。所谓数据脱敏,是指对某些敏感信息通过脱敏规则进行数据转换,从而实现敏感隐私数据的可靠保护。在日常开发过程中,数据安全一直是一个非常重要和敏感的话题。相较传统的私有化部署方案,互联网应用对数据安全的要求更高,所涉及的范围也更广。根据不同行业和业务场景的属性,不同系统的敏感信息可能有所不同,但诸如身份证号、手机号、卡号、用户姓名、账号密码等个人信息一般都需要进行脱敏处理。</p>
<h3>ShardingSphere 如何抽象数据脱敏?</h3>
<p>数据脱敏从概念上讲比较容易理解,但在具体实现过程中存在很多方案。在介绍基于数据脱敏的具体开发过程之前,我们有必要先来梳理实现数据脱敏的抽象过程。这里,我将从敏感数据的存储方式、敏感数据的加解密过程以及在业务代码中嵌入加解密的过程这三个维度来抽象数据脱敏。</p>
<p><img src="assets/CgqCHl8P-QmAA0bQAABWInFwGYE998.png" alt="Drawing 0.png" /></p>
<p><img src="assets/CgqCHl8P-QmAA0bQAABWInFwGYE998.png" alt="png" /></p>
<p>针对每一个维度,我也将基于 ShardingSphere 给出这个框架的具体抽象过程,从而方便你理解使用它的方法和技巧,让我们来一起看一下。</p>
<h4>敏感数据如何存储?</h4>
<p>关于这个问题,要讨论的点在于是否需要将敏感数据以明文形式存储在数据库中。这个问题的答案并不是绝对的。</p>
@@ -210,7 +210,7 @@ function hide_canvas() {
<p><strong>但对于用户姓名、手机号等信息,由于统计分析等方面的需要,显然我们不能直接采用不可逆的加密算法对其进行加密,还需要将明文信息进行处理</strong>**。**一种常见的处理方式是将一个字段用两列来进行保存,一列保存明文,一列保存密文,这就是第二种情况。</p>
<p>显然,我们可以将第一种情况看作是第二种情况的特例。也就是说,在第一种情况中没有明文列,只有密文列。</p>
<p>ShardingSphere 同样基于这两种情况进行了抽象,它将这里的明文列命名为 plainColumn而将密文列命名为 cipherColumn。其中 plainColumn 属于选填,而 cipherColumn 则是必填。同时ShardingSphere 还提出了一个逻辑列 logicColumn 的概念,该列代表一种虚拟列,只面向开发人员进行编程使用:</p>
<p><img src="assets/CgqCHl8P-SWAcpV1AABNv8n4KHg426.png" alt="Drawing 2.png" /></p>
<p><img src="assets/CgqCHl8P-SWAcpV1AABNv8n4KHg426.png" alt="png" /></p>
<h4>敏感数据如何加解密?</h4>
<p>数据脱敏本质上就是一种加解密技术应用场景,自然少不了对各种加解密算法和技术的封装。<strong>传统的加解密方式有两种,一种是对称加密,常见的包括 DEA 和 AES另一种是非对称加密常见的包括 RSA。</strong></p>
<p>ShardingSphere 内部也抽象了一个 ShardingEncryptor 组件专门封装各种加解密操作:</p>
@@ -227,7 +227,7 @@ function hide_canvas() {
<h4>业务代码中如何嵌入数据脱敏?</h4>
<p>数据脱敏的最后一个抽象点在于如何在业务代码中嵌入数据脱敏过程,显然这个过程应该尽量做到自动化,并且具备低侵入性,且应该对开发人员足够透明。</p>
<p>我们可以通过一个具体的示例来描述数据脱敏的执行流程。假设系统中存在一张 user 表,其中包含一个 user_name 列。我们认为这个 user_name 列属于敏感数据,需要对其进行数据脱敏。那么按照前面讨论的数据存储方案,可以在 user 表中设置两个字段,一个代表明文的 user_name_plain一个代表密文的 user_name_cipher。然后应用程序通过 user_name 这个逻辑列与数据库表进行交互:</p>
<p><img src="assets/Ciqc1F8P-TaAd-1QAABkT9WjY8E581.png" alt="Drawing 4.png" /></p>
<p><img src="assets/Ciqc1F8P-TaAd-1QAABkT9WjY8E581.png" alt="png" /></p>
<p>针对这个交互过程,我们希望存在一种机制,能够自动将 user_name 逻辑列映射到 user_name_plain 和 user_name_cipher 列。同时,我们希望提供一种配置机制,能够让开发人员根据需要灵活指定脱敏过程中所采用的各种加解密算法。</p>
<p>作为一款优秀的开源框架ShardingSphere 就提供了这样一种机制。那么它是如何做到这一点呢?</p>
<p>首先ShardingSphere 通过对从应用程序传入的 SQL 进行解析,并依据开发人员提供的脱敏配置对 SQL 进行改写从而实现对明文数据的自动加密并将加密后的密文数据存储到数据库中。当我们查询数据时它又从数据库中取出密文数据并自动对其解密最终将解密后的明文数据返回给用户。ShardingSphere 提供了自动化+透明化的数据脱敏过程,业务开发人员可以像使用普通数据那样使用脱敏数据,而不需要关注数据脱敏的实现细节。</p>
@@ -322,7 +322,7 @@ private final Map&lt;String, EncryptTableRuleConfiguration&gt; tables;
}
</code></pre>
<p>作为总结,我们通过一张图罗列出各个配置类之间的关系,以及数据脱敏所需要配置的各项内容:</p>
<p><img src="assets/Ciqc1F8P-VqAZq9CAACLcF2qedw534.png" alt="Drawing 6.png" /></p>
<p><img src="assets/Ciqc1F8P-VqAZq9CAACLcF2qedw534.png" alt="png" /></p>
<p>现在回到代码,为了实现数据脱敏,我们首先需要定义一个数据源,这里命名为 dsencrypt</p>
<pre><code>spring.shardingsphere.datasource.names=dsencrypt
spring.shardingsphere.datasource.dsencrypt.type=com.zaxxer.hikari.HikariDataSource
@@ -348,10 +348,10 @@ spring.shardingsphere.encrypt.tables.encrypt_user.columns.pwd.encryptor=pwd_encr
</code></pre>
<h4>执行数据脱敏</h4>
<p>现在,配置工作一切就绪,我们来执行测试用例。首先执行数据插入操作,下图数据表中对应字段存储的就是加密后的密文数据:</p>
<p><img src="assets/CgqCHl8P-WeAZFtRAABT51HN_2s801.png" alt="Drawing 8.png" />
<p><img src="assets/CgqCHl8P-WeAZFtRAABT51HN_2s801.png" alt="png" />
加密后的表数据结果</p>
<p>在这个过程中ShardingSphere 会把原始的 SQL 语句转换为用于数据脱敏的目标语句:</p>
<p><img src="assets/Ciqc1F8P-W6AVpohAAA833UHvZE135.png" alt="Drawing 9.png" />
<p><img src="assets/Ciqc1F8P-W6AVpohAAA833UHvZE135.png" alt="png" />
SQL 自动转换示意图</p>
<p>然后,我们再来执行查询语句并获取控制台日志:</p>
<pre><code>2020-05-30 15:10:59.174 INFO 31808 --- [ main] ShardingSphere-SQL : Rule Type: encrypt

View File

@@ -206,7 +206,7 @@ function hide_canvas() {
<h4>ShardingSphere 中的配置中心</h4>
<p>关于配置信息的管理,常见的做法是把它们存放在配置文件中,我们可以基于 YAML 格式或 XML 格式的配置文件完成配置信息的维护,这在 ShardingSphere 中也都得到了支持。在单块系统中,配置文件能够满足需求,围绕配置文件展开的配置管理工作通常不会有太大挑战。但在分布式系统中,越来越多的运行时实例使得散落的配置难于管理,并且,配置不同步导致的问题十分严重。将配置集中于配置中心,可以更加有效地进行管理。</p>
<p><strong>采用配置中心也就意味着采用集中式配置管理的设计思想</strong>。在集中式配置中心内,开发、测试和生产等不同的环境配置信息统一保存在配置中心内,这是一个维度。另一个维度就是需要确保分布式集群中同一类服务的所有服务实例保存同一份配置文件并且能够同步更新。配置中心的示意图如下所示:</p>
<p><img src="assets/CgqCHl8VVZeAej3eAABEQzB6x7o265.png" alt="1.png" />
<p><img src="assets/CgqCHl8VVZeAej3eAABEQzB6x7o265.png" alt="png" />
集中式配置管理的设计思想</p>
<p>在 ShardingSphere 中,提供了多种配置中心的实现方案,包括主流的 ZooKeeeper、Etcd、Apollo 和 Nacos。开发人员也可以根据需要实现自己的配置中心并通过 SPI 机制加载到 ShardingSphere 运行时环境中。</p>
<p>另一方面,配置信息不是一成不变的。<strong>对修改后的配置信息的统一分发,是配置中心可以提供的另一个重要能力</strong>。配置中心中配置信息的任何变化都可以实时同步到各个服务实例中。在 ShardingSphere 中,通过配置中心可以支持数据源、数据表、分片以及读写分离策略的动态切换。</p>
@@ -214,7 +214,7 @@ function hide_canvas() {
<h4>ShardingSphere 中的注册中心</h4>
<p>在实现方式上注册中心与配置中心非常类似ShardingSphere 也提供了基于 ZooKeeeper 和 Etcd 这两款第三方工具的注册中心实现方案,而 ZooKeeeper 和 Etcd 同样也可以被用作配置中心。</p>
<p>注册中心与配置中心的不同之处在于两者保存的数据类型。配置中心管理的显然是配置数据,但注册中心存放的是 ShardingSphere 运行时的各种动态/临时状态数据,最典型的运行时状态数据就是当前的 Datasource 实例。那么,保存这些动态和临时状态数据有什么用呢?我们来看一下这张图:</p>
<p><img src="assets/Ciqc1F8VVaeARWcwAABcQXkFH-E790.png" alt="2.png" />
<p><img src="assets/Ciqc1F8VVaeARWcwAABcQXkFH-E790.png" alt="png" />
注册中心的数据存储和监听机制示意图</p>
<p>注册中心一般都提供了分布式协调机制。在注册中心中,所有 DataSource 在指定路径根目录下创建临时节点,所有访问这些 DataSource 的业务服务都会监听该目录。当有新 DataSource 加入时,注册中心实时通知到所有业务服务,由业务服务做相应路由信息维护;而当某个 DataSource 宕机时,业务服务通过监听机制同样会收到通知。</p>
<p>基于这种机制,我们就可以提供针对 DataSource 的治理能力,包括熔断对某一个 DataSource 的数据访问,或禁用对从库 DataSource 的访问等。</p>
@@ -323,10 +323,10 @@ spring.shardingsphere.orchestration.registry.namespace=orchestration-health_ms
<pre><code>
</code></pre>
<p>同时ZooKeeper 服务器端也对来自应用程序的请求作出响应。我们可以使用一些 ZooKeeper 可视化客户端工具来观察目前服务器上的数据。这里,我使用了 ZooInspector 这款工具,由于 ZooKeeper 本质上就是树状结构,~~现在~~所以在根节点中就新增了配置信息:</p>
<p><img src="assets/CgqCHl8VVf2AWu6mAAAPpWnlsUo874.png" alt="3.png" />
<p><img src="assets/CgqCHl8VVf2AWu6mAAAPpWnlsUo874.png" alt="png" />
ZooKeeper 中的配置节点图</p>
<p>我们关注“config”段内容其中“rule”节点包含了读写分离的规则设置</p>
<p><img src="assets/CgqCHl8VVgWAXXOKAAAuZGtB8EQ493.png" alt="4.png" />
<p><img src="assets/CgqCHl8VVgWAXXOKAAAuZGtB8EQ493.png" alt="png" />
ZooKeeper 中的“rule”配置项</p>
<p>而“datasource”节点包含的显然是前面所指定的各个数据源信息。</p>
<p>由于我们在本地配置文件中将 spring.shardingsphere.orchestration.overwrite 配置项设置为 true本地配置的变化就会影响到服务器端配置进而影响到所有使用这些配置的应用程序。如果不希望产生这种影响而是统一使用位于配置中心上的配置应该怎么做呢</p>

View File

@@ -202,15 +202,15 @@ function hide_canvas() {
<p>从本课时开始专栏将进入“ShardingSphere 源码解析之基础设施”的模块。在介绍完 ShardingSphere 所具备的分库分表、读写分离、分布式事务、数据脱敏等各项核心功能之后,我将带领你全面剖析这些核心功能背后的实现原理和机制。我们将通过深入解析 ShardingSphere 源码这一途径来实现这一目标。</p>
<h3>如何系统剖析 ShardingSphere 的代码结构?</h3>
<p>在阅读开源框架时,我们碰到的一大问题在于,<strong>常常会不由自主地陷入代码的细节而无法把握框架代码的整体结构</strong>。市面上主流的、被大家所熟知而广泛应用的代码框架肯定考虑得非常周全,其代码结构不可避免存在一定的复杂性。对 ShardingSphere 而言,情况也是一样,我们发现 ShardingSphere 源码的一级代码结构目录就有 15 个,而这些目录内部包含的具体 Maven 工程则多达 50 余个:</p>
<p><img src="assets/CgqCHl8ZTt2ASVxWAAAShIkwDl8738.png" alt="Drawing 0.png" />
<p><img src="assets/CgqCHl8ZTt2ASVxWAAAShIkwDl8738.png" alt="png" />
ShardingSphere 源码一级代码结构目录</p>
<p><strong>如何快速把握 ShardingSphere 的代码结构呢?这是我们剖析源码时需要回答的第一个问题</strong>,为此我们需要梳理剖析 ShardingSphere 框架代码结构的系统方法。</p>
<p>本课时我们将对如何系统剖析 ShardingSphere 代码结构这一话题进行抽象,梳理出应对这一问题的六大系统方法(如下图):</p>
<p><img src="assets/CgqCHl8ZTuuACx6KAACdjxhg0lw729.png" alt="Drawing 1.png" /></p>
<p><img src="assets/CgqCHl8ZTuuACx6KAACdjxhg0lw729.png" alt="png" /></p>
<p>接下来,我们将结合 ShardingSphere 框架对这些方法进行展开。</p>
<h4>基于可扩展性设计阅读源码</h4>
<p>ShardingSphere 在设计上采用了微内核架构模式来确保系统具有高度的可扩展性,并使用了 JDK 提供的 SPI 机制来具体实现微内核架构。在 ShardingSphere 源代码的根目录下,存在一个独立工程 shardingsphere-spi。显然从命名上看这个工程中应该包含了 ShardingSphere 实现 SPI 的相关代码。该工程中存在一个 TypeBasedSPI 接口,它的类层结构比较丰富,课程后面将要讲到的很多核心接口都继承了该接口,包括实现配置中心的 ConfigCenter、注册中心的 RegistryCenter 等,如下所示:</p>
<p><img src="assets/CgqCHl8ZTvyAET3QAABeRzWl3zI113.png" alt="Drawing 3.png" />
<p><img src="assets/CgqCHl8ZTvyAET3QAABeRzWl3zI113.png" alt="png" />
ShardingSphere 中 TypeBasedSPI 接口的类层结构</p>
<p>这些接口的实现都遵循了 JDK 提供的 SPI 机制。在我们阅读 ShardingSphere 的各个代码工程时,<strong>一旦发现在代码工程中的 META-INF/services 目录里创建了一个以服务接口命名的文件,就说明这个代码工程中包含了用于实现扩展性的 SPI 定义</strong></p>
<p>在 ShardingSphere 中,大量使用了微内核架构和 SPI 机制实现系统的扩展性。只要掌握了微内核架构的基本原理以及 SPI 的实现方式就会发现,原来在 ShardingSphere 中很多代码结构上的组织方式就是为了满足这些扩展性的需求。ShardingSphere 中实现微内核架构的方式就是直接对 JDK 的 ServiceLoader 类进行一层简单的封装,并添加属性设置等自定义的功能,其本身并没有太多复杂的内容。</p>
@@ -219,7 +219,7 @@ ShardingSphere 中 TypeBasedSPI 接口的类层结构</p>
<p>分包Package设计原则可以用来设计和规划开源框架的代码结构。<strong>对于一个包结构而言,最核心的设计要点就是高内聚和低耦合</strong>。我们刚开始阅读某个框架的源码时,为了避免过多地扎进细节而只关注某一个具体组件,同样可以使用这些原则来管理我们的学习预期。</p>
<p>以 ShardingSphere 为例,我们在分析它的路由引擎时发现了两个代码工程,一个是 sharding-core-route一个是 sharding-core-entry。从代码结构上讲尽管这两个代码工程都不是直接面向业务开发人员但 sharding-core-route 属于路由引擎的底层组件,包含了路由引擎的核心类 ShardingRouter。</p>
<p>而 sharding-core-entry 则位于更高的层次,提供了 PreparedQueryShardingEngine 和 SimpleQueryShardingEngine 类,分包结构如下所示:</p>
<p><img src="assets/Ciqc1F8ZTxWAcdkRAACUdiRq_TI476.png" alt="Drawing 4.png" /></p>
<p><img src="assets/Ciqc1F8ZTxWAcdkRAACUdiRq_TI476.png" alt="png" /></p>
<p>图中我们可以看到两个清晰的代码结构层次关系,这是 ShardingSphere 中普遍采用的分包原则中,具有代表性的一种,即根据类的所属层级来组织包结构。</p>
<h4>基于基础开发规范阅读源码</h4>
<p>对于 ShardingSphere 而言,在梳理它的代码结构时有一个非常好的切入点,那就是基于 JDBC 规范。我们知道 ShardingSphere 在设计上一开始就完全兼容 JDBC 规范,它对外暴露的一套分片操作接口与 JDBC 规范中所提供的接口完全一致。只要掌握了 JDBC 中关于 DataSource、Connection、Statement 等核心接口的使用方式,就可以非常容易地把握 ShardingSphere 中暴露给开发人员的代码入口,进而把握整个框架的代码结构。</p>
@@ -234,13 +234,13 @@ ShardingSphere 中 TypeBasedSPI 接口的类层结构</p>
</code></pre>
<p>通过这个工厂类,我们很容易就找到了创建支持分片机制的 DataSource 入口,从而引出其背后的 ShardingConnection、ShardingStatement 等类。</p>
<p>事实上,在 ShardingSphere 中存在一批 DataSourceFactory 工厂类以及对应的 DataSource 类:</p>
<p><img src="assets/Ciqc1F8ZTyqAEYJUAACZMuFSODk999.png" alt="Drawing 6.png" /></p>
<p><img src="assets/Ciqc1F8ZTyqAEYJUAACZMuFSODk999.png" alt="png" /></p>
<p>在阅读 ShardingSphere 源码时JDBC 规范所提供的核心接口及其实现类,为我们高效梳理代码入口和组织方式提供了一种途径。</p>
<h4>基于核心执行流程阅读源码</h4>
<p>事实上,还有一个比较容易理解和把握的方法可以帮我们梳理代码结构,这就是代码的执行流程。任何系统行为都可以认为是流程的组合。通过分析,看似复杂的代码结构一般都能梳理出一条贯穿全局的主流程。只要我们抓住这条主流程,就能把握框架的整体代码结构。</p>
<p>那么,对于 ShardingSphere 框架而言什么才是它的主流程呢这个问题其实不难回答。事实上JDBC 规范为我们实现数据存储和访问提供了基本的开发流程。我们可以从 DataSource 入手,逐步引入 Connection、Statement 等对象,并完成 SQL 执行的主流程。这是从框架提供的核心功能角度梳理的一种主流程。</p>
<p>对于框架内部的代码组织结构而言,实际上也存在着核心流程的概念。最典型的就是 ShardingSphere 的分片引擎结构,整个分片引擎执行流程可以非常清晰的分成五个组成部分,<strong>分别是解析引擎、路由引擎、改写引擎、执行引擎和归并引擎</strong></p>
<p><img src="assets/Ciqc1F8ZTzuASMVSAACEHFtHTxA442.png" alt="Drawing 8.png" /></p>
<p><img src="assets/Ciqc1F8ZTzuASMVSAACEHFtHTxA442.png" alt="png" /></p>
<p>ShardingSphere 对每个引擎都进行了明确地命名,在代码工程的组织结构上也做了对应的约定,例如 sharding-core-route 工程用于实现路由引擎sharding-core-execute 工程用于实现执行引擎sharding-core-merge 工程用于实现归并引擎等。这是从框架内部实现机制角度梳理的一种主流程。</p>
<p>在软件建模领域,可以通过一些工具和手段对代码执行流程进行可视化,例如 UML 中的活动图和时序图。在后续的课时中,我们会基于这些工具帮你梳理 ShardingSphere 中很多有待挖掘的代码执行流程。</p>
<h4>基于框架演进过程阅读源码</h4>
@@ -265,13 +265,13 @@ ShardingSphere 中 TypeBasedSPI 接口的类层结构</p>
</code></pre>
<p>注意,这里基于装饰器模式实现了两个 SQLRewriteContextDecorator一个是 ShardingSQLRewriteContextDecorator另一个是 EncryptSQLRewriteContextDecorator而后者是在前者的基础上完成装饰工作。也就是说我们首先可以单独使用 ShardingSQLRewriteContextDecorator 来完成对 SQL 的改写操作。</p>
<p>随着架构的演进,我们也可以在原有 EncryptSQLRewriteContextDecorator 的基础上添加新的面向数据脱敏的功能,这就体现了一种架构演进的过程。通过阅读这两个装饰器类,以及 SQL 改写上下文对象 SQLRewriteContext我们就能更好地把握代码的设计思想和实现原理</p>
<p><img src="assets/Ciqc1F8ZT32ASVKBAACFTeG0vcw337.png" alt="Drawing 10.png" /></p>
<p><img src="assets/Ciqc1F8ZT32ASVKBAACFTeG0vcw337.png" alt="png" /></p>
<p>关于数据脱敏以及装饰器模式的具体实现细节我们会在《数据脱敏:如何基于改写引擎实现低侵入性数据脱敏方案?》中进行详细展开。</p>
<h4>基于通用外部组件阅读源码</h4>
<p>在《开篇寄语:如何正确学习一款分库分表开源框架?》中,我们提出了一种观点,即<strong>技术原理存在相通性</strong>。这点同样可以帮助我们更好地阅读 ShardingSphere 源码。</p>
<p>在 ShardingSphere 中集成了一批优秀的开源框架包括用于实现配置中心和注册中心的Zookeeper、Apollo、Nacos用于实现链路跟踪的 SkyWalking用于实现分布式事务的 Atomikos 和 Seata 等。</p>
<p>我们先以分布式事务为例ShardingSphere 提供了一个 sharding-transaction-core 代码工程,用于完成对分布式事务的抽象。然后又针对基于两阶段提交的场景,提供了 sharding-transaction-2pc 代码工程,以及针对柔性事务提供了 sharding-transaction-base 代码工程。而在 sharding-transaction-2pc 代码工程内部,又包含了如下所示的 5 个子代码工程。</p>
<p><img src="assets/CgqCHl8ZT5KASWyUAAAJVU7jHKk131.png" alt="Drawing 12.png" />
<p><img src="assets/CgqCHl8ZT5KASWyUAAAJVU7jHKk131.png" alt="png" />
sharding-transaction-2pc 代码工程下的子工程</p>
<p>在翻阅这些代码工程时,会发现每个工程中的类都很少,原因就在于,<strong>这些类都只是完成与第三方框架的集成而已</strong>。所以,只要我们对这些第三方框架有一定了解,阅读这部分代码就会显得非常简单。</p>
<p>再举一个例子,我们知道 ZooKeeper 可以同时用来实现配置中心和注册中心。作为一款主流的分布式协调框架,基本的工作原理就是采用了它所提供的临时节点以及监听机制。基于 ZooKeeper 的这一原理,我们可以把当前 ShardingSphere 所使用的各个 DataSource 注册到 ZooKeeper 中,并根据 DataSource 的运行时状态来动态对数据库实例进行治理,以及实现访问熔断机制。<strong>事实上ShardingSphere 能做到这一点,依赖的就是 ZooKeeper 所提供的基础功能</strong>。只要我们掌握了这些功能,理解这块代码就不会很困难,而 ShardingSphere 本身并没有使用 ZooKeeper 中任何复杂的功能。</p>

View File

@@ -206,13 +206,13 @@ function hide_canvas() {
<p><strong>微内核架构本质上是为了提高系统的扩展性</strong> 。所谓扩展性,是指系统在经历不可避免的变更时所具有的灵活性,以及针对提供这样的灵活性所需要付出的成本间的平衡能力。也就是说,当在往系统中添加新业务时,不需要改变原有的各个组件,只需把新业务封闭在一个新的组件中就能完成整体业务的升级,我们认为这样的系统具有较好的可扩展性。</p>
<p>就架构设计而言,扩展性是软件设计的永恒话题。而要实现系统扩展性,一种思路是提供可插拔式的机制来应对所发生的变化。当系统中现有的某个组件不满足要求时,我们可以实现一个新的组件来替换它,而整个过程对于系统的运行而言应该是无感知的,我们也可以根据需要随时完成这种新旧组件的替换。</p>
<p>比如在下个课时中我们将要介绍的 ShardingSphere 中提供的分布式主键功能,分布式主键的实现可能有很多种,而扩展性在这个点上的体现就是, <strong>我们可以使用任意一种新的分布式主键实现来替换原有的实现,而不需要依赖分布式主键的业务代码做任何的改变</strong></p>
<p><img src="assets/CgqCHl8esVaAVlUFAACJmGjQZDA482.png" alt="image.png" /></p>
<p><img src="assets/CgqCHl8esVaAVlUFAACJmGjQZDA482.png" alt="png" /></p>
<p>微内核架构模式为这种实现扩展性的思路提供了架构设计上的支持ShardingSphere 基于微内核架构实现了高度的扩展性。在介绍如何实现微内核架构之前,我们先对微内核架构的具体组成结构和基本原理做简要的阐述。</p>
<h4>什么是微内核架构?</h4>
<p>从组成结构上讲, <strong>微内核架构包含两部分组件:内核系统和插件</strong> 。这里的内核系统通常提供系统运行所需的最小功能集,而插件是独立的组件,包含自定义的各种业务代码,用来向内核系统增强或扩展额外的业务能力。在 ShardingSphere 中,前面提到的分布式主键就是插件,而 ShardingSphere 的运行时环境构成了内核系统。</p>
<p><img src="assets/CgqCHl8esWOAJ-5cAACfxz06p_E616.png" alt="image" /></p>
<p><img src="assets/CgqCHl8esWOAJ-5cAACfxz06p_E616.png" alt="png" /></p>
<p>那么这里的插件具体指的是什么呢?这就需要我们明确两个概念,一个概念就是经常在说的 <strong>API</strong> ,这是系统对外暴露的接口。而另一个概念就是 <strong>SPI</strong>Service Provider Interface服务提供接口这是插件自身所具备的扩展点。就两者的关系而言API 面向业务开发人员,而 SPI 面向框架开发人员,两者共同构成了 ShardingSphere 本身。</p>
<p><img src="assets/Ciqc1F8esXOADonEAACE9HEUTJc298.png" alt="image" /></p>
<p><img src="assets/Ciqc1F8esXOADonEAACE9HEUTJc298.png" alt="png" /></p>
<p>可插拔式的实现机制说起来简单,做起来却不容易,我们需要考虑两方面内容。一方面,我们需要梳理系统的变化并把它们抽象成多个 SPI 扩展点。另一方面, <strong>当我们实现了这些 SPI 扩展点之后,就需要构建一个能够支持这种可插拔机制的具体实现,从而提供一种 SPI 运行时环境</strong></p>
<p>那么ShardingSphere 是如何实现微内核架构的呢?让我们来一起看一下。</p>
<h3>如何实现微内核架构?</h3>
@@ -261,7 +261,7 @@ public class Main {
</code></pre>
<p>如果我们调整 META-INF/services/com.tianyilan.KeyGenerator 文件中的内容,去掉 com.tianyilan.UUIDKeyGenerator 的定义,并重新打成 jar 包供 SPI 服务的使用者进行引用。再次执行 Main 函数,则只会得到基于 SnowflakeKeyGenerator 的输出结果。</p>
<p>至此, 完整 的 SPI 提供者和使用者的实现过程演示完毕。我们通过一张图,总结基于 JDK 的 SPI 机制实现微内核架构的开发流程:</p>
<p><img src="assets/Ciqc1F8esYqAdXABAADVVh6mYnA926.png" alt="image" /></p>
<p><img src="assets/Ciqc1F8esYqAdXABAADVVh6mYnA926.png" alt="png" /></p>
<p>这个示例非常简单,但却是 ShardingSphere 中实现微内核架构的基础。接下来,就让我们把话题转到 ShardingSphere看看 ShardingSphere 中应用 SPI 机制的具体方法。</p>
<h3>ShardingSphere 如何基于微内核架构实现扩展性?</h3>
<p>ShardingSphere 中微内核架构的实现过程并不复杂,基本就是对 JDK 中 SPI 机制的封装。让我们一起来看一下。</p>
@@ -365,13 +365,13 @@ public class Main {
</code></pre>
<p>可以看到,这里并没有使用前面介绍的 TypeBasedSPIServiceLoader 来加载实例,而是直接使用更为底层的 NewInstanceServiceLoader。</p>
<p>这里引入的 SQLParserEntry 接口就位于 shardingsphere-sql-parser-spi 工程的 org.apache.shardingsphere.sql.parser.spi 包中。显然,从包的命名上看,该接口是一个 SPI 接口。在 SQLParserEntry 类层结构接口中包含一批实现类,分别对应各个具体的数据库:</p>
<p><img src="assets/Ciqc1F8ed26ANXCOAAArBJH3uDs890.png" alt="Drawing 4.png" /></p>
<p><img src="assets/Ciqc1F8ed26ANXCOAAArBJH3uDs890.png" alt="png" /></p>
<p>SQLParserEntry 实现类图</p>
<p>我们先来看针对 MySQL 的代码工程 shardingsphere-sql-parser-mysql在 META-INF/services 目录下,我们找到了一个 org.apache.shardingsphere.sql.parser.spi.SQLParserEntry 文件:</p>
<p><img src="assets/CgqCHl8ed3aABqWdAABTnSG89Jg177.png" alt="Drawing 5.png" /></p>
<p><img src="assets/CgqCHl8ed3aABqWdAABTnSG89Jg177.png" alt="png" /></p>
<p>MySQL 代码工程中的 SPI 配置</p>
<p>可以看到这里指向了 org.apache.shardingsphere.sql.parser.MySQLParserEntry 类。再来到 Oracle 的代码工程 shardingsphere-sql-parser-oracle在 META-INF/services 目录下,同样找到了一个 org.apache.shardingsphere.sql.parser.spi.SQLParserEntry 文件:</p>
<p><img src="assets/Ciqc1F8ed4GABKlZAABTzlYzJvc755.png" alt="Drawing 6.png" /></p>
<p><img src="assets/Ciqc1F8ed4GABKlZAABTzlYzJvc755.png" alt="png" /></p>
<p>Oracle 代码工程中的 SPI 配置</p>
<p>显然,这里应该指向 org.apache.shardingsphere.sql.parser.OracleParserEntry 类,通过这种方式,系统在运行时就会根据类路径动态加载 SPI。</p>
<p>可以注意到,在 SQLParserEntry 接口的类层结构中,实际并没有使用到 TypeBasedSPI 接口 ,而是完全采用了 JDK 原生的 SPI 机制。</p>
@@ -402,7 +402,7 @@ public class Main {
<p>那么它是如何实现的呢? 首先ConfigCenterServiceLoader 类通过 NewInstanceServiceLoader.register(ConfigCenter.class) 语句将所有 ConfigCenter 注册到系统中,这一步会通过 JDK 的 ServiceLoader 工具类加载类路径中的所有 ConfigCenter 实例。</p>
<p>我们可以看到在上面的 load 方法中,通过父类 TypeBasedSPIServiceLoader 的 newService 方法,基于类型创建了 SPI 实例。</p>
<p>以 ApolloConfigCenter 为例,我们来看它的使用方法。在 sharding-orchestration-config-apollo 工程的 META-INF/services 目录下,应该存在一个名为 org.apache.shardingsphere.orchestration.config.api.ConfigCenter 的配置文件,指向 ApolloConfigCenter 类:</p>
<p><img src="assets/CgqCHl8eekGAa88DAABIbz4-Q20783.png" alt="Drawing 7.png" /></p>
<p><img src="assets/CgqCHl8eekGAa88DAABIbz4-Q20783.png" alt="png" /></p>
<p>Apollo 代码工程中的 SPI 配置</p>
<p>其他的 ConfigCenter 实现也是一样,你可以自行查阅 sharding-orchestration-config-zookeeper-curator 等工程中的 SPI 配置文件。</p>
<p>至此,我们全面了解了 ShardingSphere 中的微内核架构,也就可以基于 ShardingSphere 所提供的各种 SPI 扩展点提供满足自身需求的具体实现。</p>

View File

@@ -272,7 +272,7 @@ function hide_canvas() {
<p>回顾上一课时的内容,我们不难理解 ShardingKeyGeneratorServiceLoader 类的作用。ShardingKeyGeneratorServiceLoader 继承了 TypeBasedSPIServiceLoader 类,并在静态方法中通过 NewInstanceServiceLoader 注册了类路径中所有的 ShardingKeyGenerator。然后ShardingKeyGeneratorServiceLoader 的 newService 方法基于类型参数通过 SPI 创建实例,并赋值 Properties 属性。</p>
<p>通过继承 TypeBasedSPIServiceLoader 类来创建一个新的 ServiceLoader 类,然后在其静态方法中注册相应的 SPI 实现,这是 ShardingSphere 中应用微内核模式的常见做法,很多地方都能看到类似的处理方法。</p>
<p>我们在 sharding-core-common 工程的 META-INF/services 目录中看到了具体的 SPI 定义:</p>
<p><img src="assets/Ciqc1F8iliiAVywgAAByh__z6Bw582.png" alt="1.png" /></p>
<p><img src="assets/Ciqc1F8iliiAVywgAAByh__z6Bw582.png" alt="png" /></p>
<p>分布式主键 SPI 配置</p>
<p>可以看到,这里有两个 ShardingKeyGenerator分别是 SnowflakeShardingKeyGenerator 和 UUIDShardingKeyGenerator它们都位于org.apache.shardingsphere.core.strategy.keygen 包下。</p>
<h3>ShardingSphere 中的分布式主键实现方案</h3>
@@ -296,7 +296,7 @@ function hide_canvas() {
</code></pre>
<h4>SnowflakeShardingKeyGenerator</h4>
<p>再来看 SnowFlake雪花算法SnowFlake 是 ShardingSphere 默认的分布式主键生成策略。它是 Twitter 开源的分布式 ID 生成算法,其核心思想是使用一个 64bit 的 long 型数字作为全局唯一 ID且 ID 引入了时间戳基本上能够保持自增。SnowFlake 算法在分布式系统中的应用十分广泛SnowFlake 算法中 64bit 的详细结构存在一定的规范:</p>
<p><img src="assets/CgqCHl8ilkuAHxUeAAHYgqa5Z0Q435.png" alt="2.png" /></p>
<p><img src="assets/CgqCHl8ilkuAHxUeAAHYgqa5Z0Q435.png" alt="png" /></p>
<p>64bit 的 ID 结构图</p>
<p>在上图中,我们把 64bit 分成了四个部分:</p>
<ul>

View File

@@ -201,7 +201,7 @@ function hide_canvas() {
<div><h1>15 解析引擎SQL 解析流程应该包括哪些核心阶段?(上)</h1>
<p>你好,欢迎进入第 15 课时的学习,结束了对 ShardingSphere 中微内核架构等基础设施相关实现机制的介绍后,今天我们将正式进入到分片引擎的学习。</p>
<p>对于一款分库分表中间件而言,分片是其最核心的功能。下图展示了整个 ShardingSphere 分片引擎的组成结构,我们已经在[《12 | 从应用到原理:如何高效阅读 ShardingSphere 源码》]这个课时中对分片引擎中所包含的各个组件进行了简单介绍。我们知道,对于分片引擎而言,第一个核心组件就是 SQL 解析引擎。</p>
<p><img src="assets/Ciqc1F8nypyARZV3AACJf1UYtf4213.png" alt="Drawing 0.png" /></p>
<p><img src="assets/Ciqc1F8nypyARZV3AACJf1UYtf4213.png" alt="png" /></p>
<p>对于多数开发人员而言SQL 解析是一个陌生的话题但对于一个分库分表中间件来说却是一个基础组件目前主流的分库分表中间件都包含了对解析组件的实现策略。可以说SQL 解析引擎所生成的结果贯穿整个 ShardingSphere。如果我们无法很好地把握 SQL 的解析过程,在阅读 ShardingSphere 源码时就会遇到一些障碍。</p>
<p>另一方面SQL 的解析过程本身也很复杂,你在拿到 ShardingSphere 框架的源代码时可能首先会问这样一个问题SQL 的解析过程应该包含哪些核心阶段呢?接下来我将带你深度剖析这个话题。</p>
<h3>从 DataSource 到 SQL 解析引擎入口</h3>
@@ -225,14 +225,14 @@ shardingRuleConfig.setDefaultTableShardingStrategyConfig(new StandardShardingStr
return ShardingDataSourceFactory.createDataSource(createDataSourceMap(), shardingRuleConfig, new Properties());
</code></pre>
<p>可以看到,上述代码构建了几个数据源,加上分库、分表策略以及分片规则,然后通过 ShardingDataSourceFactory 获取了目前数据源 DataSource 。显然,对于应用开发而言,<strong>DataSource 就是我们使用 ShardingSphere 框架的入口</strong>。事实上,对于 ShardingSphere 内部的运行机制而言DataSource 同样是引导我们进入分片引擎的入口。围绕 DataSource通过跟踪代码的调用链路我们可以得到如下所示的类层结构图</p>
<p><img src="assets/Ciqc1F8nyriAPY8tAAB8wwhtMU4809.png" alt="Drawing 2.png" /></p>
<p><img src="assets/Ciqc1F8nyriAPY8tAAB8wwhtMU4809.png" alt="png" /></p>
<p>上图已经引出了 ShardingSphere 内核中的很多核心对象,但今天我们只关注位于整个链路的最底层对象,即图中的 SQLParseEngine。一方面在 DataSource 的创建过程中,最终初始化了 SQLParseEngine另一方面负责执行路由功能的 ShardingRouter 也依赖于 SQLParseEngine。这个 SQLParseEngine 就是 ShardingSphere 中负责整个 SQL 解析过程的入口。</p>
<h3>从 SQL 解析引擎到 SQL 解析内核</h3>
<p>在 ShardingSphere 中存在一批以“Engine”结尾的引擎类。从架构思想上看这些类在设计和实现上普遍采用了外观模式。外观Facade模式的意图可以描述为子系统中的一组接口提供一个一致的界面。外观模式定义了一个高层接口这个接口使得这一子系统更加容易使用。该模式的示意图如下图所示</p>
<p><img src="assets/Ciqc1F8nysKAKGdhAABINS6qFpI839.png" alt="Drawing 4.png" /></p>
<p><img src="assets/Ciqc1F8nysKAKGdhAABINS6qFpI839.png" alt="png" /></p>
<p>从作用上讲,外观模式能够起到<strong>客户端与后端服务之间的隔离作用</strong>,随着业务需求的变化和时间的演进,外观背后各个子系统的划分和实现可能需要进行相应的调整和升级,这种调整和升级需要做到<strong>对客户端透明</strong>。在设计诸如 ShardingSphere 这样的中间件框架时,这种隔离性尤为重要。</p>
<p>对于 SQL 解析引擎而言情况同样类似。不同之处在于SQLParseEngine 本身并不提供外观作用,而是把这部分功能委托给了另一个核心类 SQLParseKernel。从命名上看这个类才是 SQL 解析的内核类也是所谓的外观类。SQLParseKernel 屏蔽了后端服务中复杂的 SQL 抽象语法树对象 SQLAST、SQL 片段对象 SQLSegment ,以及最终的 SQL 语句 SQLStatement 对象的创建和管理过程。上述这些类之间的关系如下所示:</p>
<p><img src="assets/CgqCHl8nytiAcb6GAABVel2mPvE115.png" alt="Drawing 6.png" /></p>
<p><img src="assets/CgqCHl8nytiAcb6GAABVel2mPvE115.png" alt="png" /></p>
<h4>1.SQLParseEngine</h4>
<p>从前面的类层结构图中可以看到AbstractRuntimeContext 是 SQLParseEngine 的构建入口。顾名思义RuntimeContext 在 ShardingSphere 中充当一种运行时上下文,保存着与运行时环境下相关的分片规则、分片属性、数据库类型、执行引擎以及 SQL 解析引擎。作为 RuntimeContext 接口的实现类AbstractRuntimeContex 在其构造函数中完成了对 SQLParseEngine 的构建,构建过程如下所示:</p>
<pre><code>protected AbstractRuntimeContext(final T rule, final Properties props, final DatabaseType databaseType) {
@@ -335,7 +335,7 @@ private final SQLStatementFillerEngine fillerEngine;
<li>通过 SQLStatementFiller 填充 SQLStatement</li>
</ul>
<p>这三个阶段便是 ShardingSphere 新一代 SQL 解析引擎的核心组成部分。其整体架构如下图所示:</p>
<p><img src="assets/Ciqc1F8nyz2AaMf0AACQcl1OWTw870.png" alt="Drawing 8.png" /></p>
<p><img src="assets/Ciqc1F8nyz2AaMf0AACQcl1OWTw870.png" alt="png" /></p>
<p>至此,我们看到由<strong>解析、提取和填充</strong>这三个阶段所构成的整体 SQL 解析流程已经完成。现在能够根据一条 SQL 语句解析出对应的 SQLStatement 对象,供后续的 ShardingRouter 等路由引擎进行使用。</p>
<p>本课时我们首先关注流程中的第一阶段,即如何生成一个 SQLAST后两个阶段会在后续课时中讲解。这部分的实现过程位于 SQLParserEngine 的 parse 方法,如下所示:</p>
<pre><code>public SQLAST parse() {
@@ -388,7 +388,7 @@ private final SQLStatementFillerEngine fillerEngine;
<p>从这种实现方式上看,我们可以断定 SQLParserEntry 是一个 SPI 接口。通过查看 SQLParserEntry 所处的代码包结构,更印证了这一观点,因为该类位于 shardingsphere-sql-parser-spi 工程的 org.apache.shardingsphere.sql.parser.spi 包中。</p>
<p>关于 SQLParser 和 SQLParserEntry 这一对接口,还有一点值得探讨。注意到 SQLParser 接口位于 shardingsphere-sql-parser-spi 工程的 org.apache.shardingsphere.sql.parser.api 包中,所示它是一个 API 接口。</p>
<p>从定位上讲SQLParser 是解析器对外暴露的入口,而 SQLParserEntry 是解析器的底层实现,两者共同构成了 SQL 解析器本身。更宽泛的从架构设计层次上讲API 面向高层业务开发人员,而 SPI 面向底层框架开发人员,两者的关系如下图所示。作为一款优秀的中间件框架,这种 API 和 SPI 的对应关系在 ShardingSphere 中非常普遍,也是我们正确理解 ShardingSphere 架构设计上的一个切入点。</p>
<p><img src="assets/CgqCHl8ny3WAAR7gAABKWeCFeTg698.png" alt="Drawing 10.png" /></p>
<p><img src="assets/CgqCHl8ny3WAAR7gAABKWeCFeTg698.png" alt="png" /></p>
<p>SQLParser 和 SQLParserEntry 这两个接口的定义和实现都与基于 ANTLR4 的 AST 生成机制有关。ANTLR 是 Another Tool for Language Recognition 的简写是一款能够根据输入自动生成语法树的开源语法分析器。ANTLR 可以将用户编写的 ANTLR 语法规则直接生成 Java、Go 语言的解析器,在 ShardingSphere 中就使用了 ANTLR4 来生成 AST。</p>
<p>我们注意到 SQLParserEngine 的 parse 方法最终返回的是一个 SQLAST该类的定义如下所示。</p>
<pre><code>public final class SQLAST {

View File

@@ -200,7 +200,7 @@ function hide_canvas() {
<p id="tip" align="center"></p>
<div><h1>16 解析引擎SQL 解析流程应该包括哪些核心阶段?(下)</h1>
<p>我们知道整个 SQL 解析引擎可以分成三个阶段(如下图所示),上一课时我们主要介绍了 ShardingSphere 中 SQL 解析引擎的第一个阶段,那么今天我将承接上一课时,继续讲解 ShardingSphere 中 SQL 解析流程中剩余的两个阶段。</p>
<p><img src="assets/Ciqc1F8ry7-AWFaOAACKmUmdLPs289.png" alt="Drawing 0.png" /></p>
<p><img src="assets/Ciqc1F8ry7-AWFaOAACKmUmdLPs289.png" alt="png" /></p>
<h3>SQL 解析引擎的三大阶段</h3>
<p>在 SQL 解析引擎的第一阶段中,我们详细介绍了 ShardingSphere 生成 SQL 抽象语法树的过程,并引出了 SQLStatementRule 规则类。今天我们将基于这个规则类来分析如何提取 SQLSegment 以及如何填充 SQL 语句的实现机制。</p>
<h4>1.第二阶段:提取 SQL 片段</h4>
@@ -222,7 +222,7 @@ function hide_canvas() {
&lt;/sql-statement-rule-definition&gt;
</code></pre>
<p>基于 ParseRuleRegistry 类进行规则获取和处理过程,涉及一大批实体对象以及用于解析 XML 配置文件的 JAXB 工具类的定义,内容虽多但并不复杂。核心类之间的关系如下图所示:</p>
<p><img src="assets/Ciqc1F8ry9CAPtDdAACEYYKrCTU070.png" alt="Drawing 2.png" /></p>
<p><img src="assets/Ciqc1F8ry9CAPtDdAACEYYKrCTU070.png" alt="png" /></p>
<p>ParseRuleRegistry 类层结构图</p>
<p>当获取规则之后,对于具体某种数据库类型的每条 SQL 而言,都会有一个 SQLStatementRule 对象。我们注意到每个 SQLStatementRule 都定义了一个“context”以及一个“sql-statement-class”。</p>
<p>这里的 context 实际上就是通过 SQL 解析所生成的抽象语法树 SQLAST 中的 ParserRuleContext包括 CreateTableContext、SelectContext 等各种 StatementContext。而针对每一种 context都有专门的一个 SQLStatement 对象与之对应,那么这个 SQLStatement 究竟长什么样呢?我们来看一下。</p>
@@ -254,7 +254,7 @@ function hide_canvas() {
<pre><code>SELECT task_id, task_name FROM health_task WHERE user_id = 'user1' AND record_id = 2
</code></pre>
<p>通过解析,我们获取了如下所示的抽象语法树:</p>
<p><img src="assets/Ciqc1F8ry_WAEwAzAACKQ3CnEFw961.png" alt="Drawing 4.png" /></p>
<p><img src="assets/Ciqc1F8ry_WAEwAzAACKQ3CnEFw961.png" alt="png" /></p>
<p>抽象语法树示意图</p>
<p>我们发现,对于上述抽象语法树中的某些节点(如 SELECT、FROM 和 WHERE没有子节点而对于如 FIELDS、TABLES 和 CONDITIONS 节点而言,本身也是一个树状结构。显然,这两种节点的提取规则应该是不一样的。</p>
<p>因此ShardingSphere 提供了两种 SQLSegmentExtractor一种是针对单节点的 OptionalSQLSegmentExtractor另一种是针对树状节点的 CollectionSQLSegmentExtractor。由于篇幅因素这里以 TableExtractor 为例,展示如何提取 TableSegment 的过程TableExtractor 的实现方法如下所示:</p>
@@ -363,11 +363,11 @@ function hide_canvas() {
}
</code></pre>
<p>这段代码在实现上采用了回调机制来完成对象的注入。在 ShardingSphere 中,基于回调的处理方式也非常普遍。本质上,回调解决了因为类与类之间的相互调用而造成的循环依赖问题,回调的实现策略通常采用了如下所示的类层结构:</p>
<p><img src="assets/Ciqc1F8rzBeAL-gtAAAtxVTlOkM440.png" alt="Drawing 6.png" /></p>
<p><img src="assets/Ciqc1F8rzBeAL-gtAAAtxVTlOkM440.png" alt="png" /></p>
<p>回调机制示意图</p>
<p>TableFiller 中所依赖的 TableSegmentAvailable 和 TableSegmentsAvailable 接口就类似于上图中的 Callback 接口,具体的 SQLStatement 就是 Callback 的实现类,而 TableFiller 则是 Callback 的调用者。以 TableFiller 为例,我们注意到,如果对应的 SQLStatement 实现了这两个接口中的任意一个,那么就可以通过 TableFiller 注入对应的 TableSegment从而完成 SQLSegment 的填充。</p>
<p>这里以 TableSegmentAvailable 接口为例,它有一组实现类,如下所示:</p>
<p><img src="assets/CgqCHl8rzC2ADPHvAAAxxRKUUYw921.png" alt="Drawing 8.png" /></p>
<p><img src="assets/CgqCHl8rzC2ADPHvAAAxxRKUUYw921.png" alt="png" /></p>
<p>TableSegmentAvailable实现类</p>
<p>以上图中的 CreateTableStatement 为例,该类同时实现了 TableSegmentAvailable 和 IndexSegmentsAvailable 这两个回调接口,所以就可以同时操作 TableSegment 和 IndexSegment 这两个 SQLSegment。CreateTableStatement 类的实现如下所示:</p>
<pre><code>public final class CreateTableStatement extends DDLStatement implements TableSegmentAvailable, IndexSegmentsAvailable {
@@ -380,7 +380,7 @@ function hide_canvas() {
}
</code></pre>
<p>至此,我们通过一个示例解释了与填充操作相关的各个类之间的协作关系,如下所示的类图展示了这种协作关系的整体结构。</p>
<p><img src="assets/CgqCHl8rzDqAVtDCAAB-8xyeFnI893.png" alt="Drawing 9.png" /></p>
<p><img src="assets/CgqCHl8rzDqAVtDCAAB-8xyeFnI893.png" alt="png" /></p>
<p>SQLStatement类层结构图</p>
<p>有了上图的基础,我们理解填充引擎 SQLStatementFillerEngine 就显得比较简单了SQLStatementFillerEngine 类的实现如下所示:</p>
<pre><code>public final class SQLStatementFillerEngine {

View File

@@ -203,7 +203,7 @@ function hide_canvas() {
<p>从今天开始,我们将进入 <strong>ShardingSphere 的路由Routing引擎部分的源码解析</strong>。从流程上讲,<strong>路由引擎</strong>是整个分片引擎执行流程中的第二步,即基于 SQL 解析引擎所生成的 SQLStatement通过解析执行过程中所携带的上下文信息来获取匹配数据库和表的分片策略并生成路由结果。</p>
<h3>分层:路由引擎整体架构</h3>
<p>与介绍 SQL 解析引擎时一样,我们通过翻阅 ShardingSphere 源码,首先梳理了如下所示的包结构:</p>
<p><img src="assets/CgqCHl8xJpiAQ0onAACcyggRCJ4878.png" alt="Drawing 0.png" /></p>
<p><img src="assets/CgqCHl8xJpiAQ0onAACcyggRCJ4878.png" alt="png" /></p>
<p>上述包图总结了与路由机制相关的各个核心类,我们可以看到整体呈一种对称结构,即根据是 <strong>PreparedStatement</strong> 还是<strong>普通 Statement</strong> 分成两个分支流程。</p>
<p>同时,我们也可以把这张图中的类按照其所属的包结构<strong>分成两个层次</strong>:位于底层的 sharding-core-route 和位于上层的 sharding-core-entry这也是 ShardingSphere 中所普遍采用的一种分包原则,即<strong>根据类的所属层级来组织包结构</strong>。关于 ShardingSphere 的分包原则我们在 [《12 | 从应用到原理:如何高效阅读 ShardingSphere 源码?》]中也已经进行了介绍,接下来我们具体分析这一原则在路由引擎中的应用。</p>
<h4>1.sharding-core-route 工程</h4>
@@ -246,7 +246,7 @@ private final EncryptRule encryptRule;
</code></pre>
<p>ShardingRule 的内容非常丰富但其定位更多是提供规则信息而不属于核心流程因此我们先不对其做详细展开。作为基础规则类ShardingRule 会贯穿整个分片流程,在后续讲解过程中我们会穿插对它的介绍,这里先对上述变量的名称和含义有简单认识即可。</p>
<p>我们回到 ShardingRouter 类,发现其核心方法只有一个,即 route 方法。这个方法的逻辑比较复杂,我们梳理它的执行步骤,如下图所示:</p>
<p><img src="assets/CgqCHl8xJyqAHmcfAACVSxCxm4s053.png" alt="image" /></p>
<p><img src="assets/CgqCHl8xJyqAHmcfAACVSxCxm4s053.png" alt="png" /></p>
<p>ShardingRouter 是路由引擎的核心类,<strong>在接下来的内容中,我们将对上图中的 6 个步骤分别一 一 详细展开,帮忙你理解一个路由引擎的设计思想和实现机制。</strong></p>
<h4>1.分片合理性验证</h4>
<p>我们首先来看 ShardingRouter 的第一个步骤,即验证分片信息的合理性,验证方式如下所示:</p>
@@ -414,7 +414,7 @@ RoutingResult routingResult = routingEngine.route();
}
</code></pre>
<p>这些 RoutingEngine 的具体介绍我们放在下一课时《18 | 路由引擎:如何实现数据访问的分片路由和广播路由?》中进行详细介绍,这里只需要了解 ShardingSphere 在包结构的设计上把具体的 RoutingEngine 分成了六大类即广播broadcast路由、混合complex路由、默认数据库defaultdb路由、无效ignore路由、标准standard路由以及单播unicast路由如下所示</p>
<p><img src="assets/Ciqc1F8xJvuALcqiAAA5dODyQeU720.png" alt="Drawing 3.png" /></p>
<p><img src="assets/Ciqc1F8xJvuALcqiAAA5dODyQeU720.png" alt="png" /></p>
<p>不同类型的 RoutingEngine 实现类</p>
<p>RoutingEngine 的执行结果是 RoutingResult而 RoutingResult 中包含了一个 RoutingUnit集合RoutingUnit 中的变量定义如下所示,可以看到有两个关于 DataSource 名称的变量以及一个 TableUnit 列表:</p>
<pre><code>//真实数据源名
@@ -466,10 +466,10 @@ private RoutingResult routingResult;
}
</code></pre>
<p>这里的 SQLUnit 中就是最终的一条 SQL 语句以及相应参数的组合。因为路由结果对象 SQLRouteResult 会继续传递到分片引擎的后续流程,且内部结构比较复杂,所以这里通过如下所示的类图对其包含的各种变量进行总结,方便你进行理解。</p>
<p><img src="assets/CgqCHl8xJ0eAMp1GAABywd2SYFQ497.png" alt="Drawing 4.png" /></p>
<p><img src="assets/CgqCHl8xJ0eAMp1GAABywd2SYFQ497.png" alt="png" /></p>
<p>至此,我们把 ShardingRouter 类的核心流程做了介绍。在 ShardingSphere 的路由引擎中ShardingRouter 可以说是一个承上启下的核心类,向下我们可以挖掘各种 RoutingEngine 的具体实现;向上我们可以延展到读写分离等面向应用的具体场景。</p>
<p>下图展示了 ShardingRouter 的这种定位关系。关于各种 RoutingEngine 的介绍是我们下一课时的内容,今天我们先将基于 ShardingRouter 讨论它的上层结构,从而引出了 ShardingEngine。</p>
<p><img src="assets/CgqCHl8xJ1WAbAmHAAB_-h8F66g956.png" alt="Drawing 6.png" /></p>
<p><img src="assets/CgqCHl8xJ1WAbAmHAAB_-h8F66g956.png" alt="png" /></p>
<h3>从底层 ShardingRouter 到上层 ShardingEngine</h3>
<p>我们的思路仍然是从下往上,先来看上图中的 StatementRoutingEngine其实现如下所示</p>
<pre><code>public final class StatementRoutingEngine {
@@ -537,7 +537,7 @@ protected abstract SQLRouteResult route(String sql, List&lt;Object&gt; parameter
}
</code></pre>
<p>至此,关于 ShardingSphere 路由引擎部分的内容基本都介绍完毕。对于上层结构而言,我们以 SimpleQueryShardingEngine 为例进行了展开,对于 PreparedQueryShardingEngine 的处理方式也是类似。作为总结,我们通过如下所示的时序图来梳理这些路由的主流程。</p>
<p><img src="assets/CgqCHl8xJ2aAQabtAACUcSURKVc544.png" alt="Drawing 8.png" /></p>
<p><img src="assets/CgqCHl8xJ2aAQabtAACUcSURKVc544.png" alt="png" /></p>
<h3>从源码解析到日常开发</h3>
<p>分包设计原则可以用来设计和规划开源框架的代码结构。在今天的内容中,我们看到了 ShardingSphere 中非常典型的一种分层和分包实现策略。通过 sharding-core-route 和 sharding-core-entry 这两个工程,我们把路由引擎中位于底层的核心类 ShardingRouter 和位于上层的 PreparedQueryShardingEngine 及 SimpleQueryShardingEngine 类进行了合理的分层管理。ShardingSphere 对于分层和分包策略的应用有很多具体的表现形式,随着课程的不断演进,我们还会看到更多的应用场景。</p>
<h3>小结与预告</h3>

View File

@@ -201,7 +201,7 @@ function hide_canvas() {
<div><h1>18 路由引擎:如何实现数据访问的分片路由和广播路由?</h1>
<p>在上一课时中,我们看到起到承上启下作用的 ShardingRouter 会调用 RoutingEngine 获取路由结果,而在 ShardingSphere 中存在多种不同类型的 RoutingEngine分别针对不同的应用场景。</p>
<p>我们可以按照<strong>是否携带分片键信息</strong>将这些路由方式分成两大类,即分片路由和广播路由,而这两类路由中又存在一些常见的 RoutingEngine 实现类型,如下图所示:</p>
<p><img src="assets/Ciqc1F81FdqANHr4AACO1I-IihE703.png" alt="image" /></p>
<p><img src="assets/Ciqc1F81FdqANHr4AACO1I-IihE703.png" alt="png" /></p>
<p>我们无意对所有这些 RoutingEngine 进行详细 的 展开,但在接下来的内容中,我们会分别对分片路由和广播路由中具有代表性的 RoutingEngine 进行讨论。</p>
<h3>分片路由</h3>
<p>对于分片路由而言,我们将重点介绍<strong>标准路由</strong>,标准路由是 ShardingSphere 推荐使用的分片方式。</p>

View File

@@ -210,7 +210,7 @@ function hide_canvas() {
}
</code></pre>
<p>可以看到 ShardingStrategy 包含两个核心方法:一个用于指定分片的 Column而另一个负责执行分片并返回目标 DataSource 和 Table。ShardingSphere 中为我们提供了一系列的分片策略实例,类层结构如下所示:</p>
<p><img src="assets/Ciqc1F86ZlmARCBiAAEMHwjkZPk259.png" alt="Drawing 0.png" /></p>
<p><img src="assets/Ciqc1F86ZlmARCBiAAEMHwjkZPk259.png" alt="png" /></p>
<p>ShardingStrategy 实现类图</p>
<p>如果我们翻阅这些具体 ShardingStrategy 实现类的代码,会发现每个 ShardingStrategy 中都会包含另一个与路由相关的核心概念,即<strong>分片算法 ShardingAlgorithm</strong>,我们发现 ShardingAlgorithm 是一个空接口,但包含了<strong>四个继承接口</strong>,即</p>
<ul>
@@ -220,10 +220,10 @@ function hide_canvas() {
<li>HintShardingAlgorithm</li>
</ul>
<p>而这四个接口又分别具有一批实现类ShardingAlgorithm 的类层结构如下所示:</p>
<p><img src="assets/Ciqc1F86ZmSAaVqsAACWpLDZQm8610.png" alt="Drawing 1.png" /></p>
<p><img src="assets/Ciqc1F86ZmSAaVqsAACWpLDZQm8610.png" alt="png" /></p>
<p>ShardingAlgorithm 子接口和实现类图</p>
<p>请注意ShardingStrategy 与 ShardingAlgorithm 之间并不是一对一的关系。<strong>在一个 ShardingStrategy 中,可以同时使用多个 ShardingAlgorithm 来完成具体的路由执行策略</strong>。因此,我们具有如下所示的类层结构关系图:</p>
<p><img src="assets/Ciqc1F86Zm-AE3bOAACLylkVuks873.png" alt="Drawing 2.png" /></p>
<p><img src="assets/Ciqc1F86Zm-AE3bOAACLylkVuks873.png" alt="png" /></p>
<p>由于分片算法的独立性ShardingSphere 将其进行单独抽离。从关系上讲,分片策略中包含了分片算法和分片键,我们可以把分片策略的组成结构简单抽象成如下所示的公式:</p>
<p><strong>分片策略 = 分片算法 + 分片键</strong></p>
<h3>ShardingSphere 分片策略详解</h3>
@@ -443,10 +443,10 @@ private final Closure&lt;?&gt; closure;
}
</code></pre>
<p>最后,作为总结,我们要注意所有的 ShardingStrategy 相关类都位于 sharding-core-common 工程的 org.apache.shardingsphere.core.strategy 包下:</p>
<p><img src="assets/CgqCHl86Zp-AOn75AAII97S_QVE429.png" alt="Drawing 4.png" /></p>
<p><img src="assets/CgqCHl86Zp-AOn75AAII97S_QVE429.png" alt="png" /></p>
<p>ShardingStrategy 相关类的包结构</p>
<p>而所有的 ShardingAlgorithm 相关类则位于 sharding-core-api 工程的 org.apache.shardingsphere.api.sharding 包下:</p>
<p><img src="assets/CgqCHl86ZqeAEn76AAGjHlgYljM135.png" alt="Drawing 5.png" /></p>
<p><img src="assets/CgqCHl86ZqeAEn76AAGjHlgYljM135.png" alt="png" /></p>
<p>ShardingAlgorithm 相关类的包结构</p>
<p>我们在前面已经提到过 ShardingStrategy 的创建依赖于 ShardingStrategyConfigurationShardingSphere 也提供了一个 ShardingStrategyFactory 工厂类用于创建各种具体的 ShardingStrategy</p>
<pre><code>public final class ShardingStrategyFactory {
@@ -468,17 +468,17 @@ private final Closure&lt;?&gt; closure;
}
</code></pre>
<p>而这里用到的各种 ShardingStrategyConfiguration 也都位于 sharding-core-api 工程的org.apache.shardingsphere.api.sharding.strategy 包下:</p>
<p><img src="assets/CgqCHl86ZrGAJLi5AADslitwfjk537.png" alt="Drawing 6.png" /></p>
<p><img src="assets/CgqCHl86ZrGAJLi5AADslitwfjk537.png" alt="png" /></p>
<p>ShardingStrategyConfiguration 相关类的包结构</p>
<p>这样,通过对路由引擎的介绍,我们又接触到了一大批 ShardingSphere 中的源代码。</p>
<p>至此,关于 ShardingSphere 路由引擎部分的内容基本都介绍完毕。作为总结我们在《17 | 路由引擎:如何理解分片路由核心类 ShardingRouter 的运作机制?》中所给出的时序图中添加了 ShardingStrategy 和 ShardingAlgorithm 部分的内容,如下所示:</p>
<p><img src="assets/CgqCHl86ZrmAcGiLAADURjzyD4w363.png" alt="Drawing 7.png" /></p>
<p><img src="assets/CgqCHl86ZrmAcGiLAADURjzyD4w363.png" alt="png" /></p>
<h3>从源码解析到日常开发</h3>
<p>在我们设计软件系统的过程中,面对复杂业务场景时,<strong>职责分离</strong>始终是需要考虑的一个设计点。ShardingSphere 对于分片策略的设计和实现很好地印证了这一观点。</p>
<p>分片策略在 ShardingSphere 中实际上是一个比较复杂的概念,但通过将分片的具体算法分离出去并提炼 ShardingAlgorithm 接口,并构建 ShardingStrategy 和 ShardingAlgorithm 之间一对多的灵活关联关系,我们可以更好地把握整个分片策略体系的类层结构,这种职责分离机制同样可以应用与日常开发过程中。</p>
<h3>小结与预告</h3>
<p>承接上一课时的内容,今天我们全面介绍了 ShardingSphere 中的五大分片策略和四种分片算法以及它们之间的组合关系。</p>
<p><img src="assets/CgqCHl86ZsaAQx7cAABspIBuz1Y073.png" alt="Drawing 9.png" /></p>
<p><img src="assets/CgqCHl86ZsaAQx7cAABspIBuz1Y073.png" alt="png" /></p>
<p>ShardingSphere 路由引擎中执行路由的过程正是依赖于这些分片策略和分片算法的功能特性。当然,作为一款具有高扩展性的开源框架,我们也可以基于自身的业务需求,实现特定的分片算法并嵌入到具体的分片策略中。</p>
<p>这里给你留一道思考题ShardingSphere 中分片策略与分片算法之间是如何协作的? 欢迎你在留言区与大家讨论,我将一一点评解答。</p>
<p>在路由引擎的基础上,下一课时将进入 ShardingSphere 分片引擎的另一个核心阶段,即改写引擎。</p>

View File

@@ -229,7 +229,7 @@ function hide_canvas() {
}
</code></pre>
<p>这段代码虽然内容不多,但却完整描述了实现 SQL 改写的整体流程,我们对核心代码都添加了注释,这里面涉及的<strong>核心类</strong>也很多,值得我们进行深入分析,相关核心类的整体结构如下:</p>
<p><img src="assets/CgqCHl8-KY-AfKIDAACvtfju_F4857.png" alt="image.png" /></p>
<p><img src="assets/CgqCHl8-KY-AfKIDAACvtfju_F4857.png" alt="png" /></p>
<p>可以看到在整个类图中SQLRewriteContext 处于中间位置,改写引擎 SQLRewriteEngine 和装饰器 SQLRewriteContextDecorator 都依赖于它。</p>
<p>所以接下来,让我们先来看一下这个 SQLRewriteContext并基于自增主键功能引出 SQL 改写引擎的基础组件 SQLToken。</p>
<h3>从自增主键功能看改写引擎中的核心类</h3>
@@ -533,7 +533,7 @@ public String toString(final Map&lt;String, String&gt; logicAndActualTables) {
}
</code></pre>
<p>而 BindingTableRule 又依赖于 TableRule 中保存的 ActualDataNodes 来完成 ActualTableIndex和ActualTable 的计算。回想起我们在案例中配置的分库分表规则,这里再次感受到了以 TableRule 和 BindingTableRule为 代表的各种 Rule 对象在 ShardingSphere 的串联作用:</p>
<p><img src="assets/Ciqc1F8-KfuASC1zAAB-5yBwv_o382.png" alt="image" /></p>
<p><img src="assets/Ciqc1F8-KfuASC1zAAB-5yBwv_o382.png" alt="png" /></p>
<p>当 ShardingSQLBuilder 完成 SQL 的构建之后,我们再回到 ShardingSQLRewriteEngine这个时候我们对它的 rewrite 方法就比较明确了:</p>
<pre><code>@Override
public SQLRewriteResult rewrite(final SQLRewriteContext sqlRewriteContext) {

View File

@@ -221,7 +221,7 @@ function hide_canvas() {
<p>然后,我们又分别找到了 SQLExecuteTemplate 和 SQLExecutePrepareTemplate 类,这两个是典型的<strong>SQL 执行模板类</strong></p>
<p>根据到目前为止对 ShardingSphere 组件设计和代码分层风格的了解可以想象在层次关系上ShardingExecuteEngine 是底层对象SQLExecuteTemplate 应该依赖于 ShardingExecuteEngine而 StatementExecutor、PreparedStatementExecutor 和 BatchPreparedStatementExecutor 属于上层对象,应该依赖于 SQLExecuteTemplate。我们通过简单阅读这些核心类之前的引用关系印证了这种猜想。</p>
<p>基于以上分析,我们可以给出 SQL 执行引擎的整体结构图(如下图),其中横线以上部分位于 sharding-core-execute 工程,属于底层组件;而直线以下部分位于 sharding-jdbc-core 中属于上层组件。这种分析源码的能力也是《12 | 从应用到原理:如何高效阅读 ShardingSphere 源码?》中提到的“基于分包设计原则阅读源码”的一种具体表现:</p>
<p><img src="assets/CgqCHl9Dei6AMqoCAACpyMuj2MI683.png" alt="Drawing 0.png" /></p>
<p><img src="assets/CgqCHl9Dei6AMqoCAACpyMuj2MI683.png" alt="png" /></p>
<p>ShardingSphere 执行引擎核心类的分层结构图</p>
<p>另一方面,我们在上图中还看到 SQLExecuteCallback 和 SQLExecutePrepareCallback显然它们的作用是完成 SQL 执行过程中的回调处理,这也是一种非常典型的扩展性处理方式。</p>
<h3>ShardingExecuteEngine</h3>

View File

@@ -200,7 +200,7 @@ function hide_canvas() {
<p id="tip" align="center"></p>
<div><h1>22 执行引擎:如何把握 ShardingSphere 中的 Executor 执行模型?(上)</h1>
<p>在上一课时中,我们对 ShardingGroupExecuteCallback 和 SQLExecuteTemplate 做了介绍。从设计上讲,前者充当 ShardingExecuteEngine 的回调入口;而后者则是一个模板类,完成对 ShardingExecuteEngine 的封装并提供了对外的统一入口,这些类都位于底层的 sharding-core-execute 工程中。</p>
<p><img src="assets/CgqCHl9HalOAccqPAACp0Ky_Tl8886.png" alt="image.png" /></p>
<p><img src="assets/CgqCHl9HalOAccqPAACp0Ky_Tl8886.png" alt="png" /></p>
<p>从今天开始,我们将进入到 sharding-jdbc-core 工程,来看看 ShardingSphere 中执行引擎上层设计中的几个核心类。</p>
<h3>AbstractStatementExecutor</h3>
<p>如上图所示,根据上一课时中的执行引擎整体结构图,可以看到<strong>SQLExecuteTemplate</strong>的直接使用者是<strong>AbstractStatementExecutor 类</strong>,今天我们就从这个类开始展开讨论,该类的变量比较多,我们先来看一下:</p>
@@ -269,7 +269,7 @@ private final Collection&lt;ShardingExecuteGroup&lt;StatementExecuteUnit&gt;&gt;
</code></pre>
<p>显然,在这里应该使用 SQLExecuteTemplate 模板类来完成具体回调的执行过程。同时,我可以看到这里还有一个 refreshMetaDataIfNeeded 辅助方法用来刷选元数据。</p>
<p>AbstractStatementExecutor 有两个实现类:一个是普通的 StatementExecutor一个是 PreparedStatementExecutor接下来我将分别进行讲解。</p>
<p><img src="assets/Ciqc1F9HamWACCzmAABPdP2Sna8714.png" alt="image" /></p>
<p><img src="assets/Ciqc1F9HamWACCzmAABPdP2Sna8714.png" alt="png" /></p>
<h3>StatementExecutor</h3>
<p>我们来到 StatementExecutor先看它的用于执行初始化操作的 init 方法:</p>
<pre><code>public void init(final SQLRouteResult routeResult) throws SQLException {
@@ -341,7 +341,7 @@ private final Collection&lt;ShardingExecuteGroup&lt;StatementExecuteUnit&gt;&gt;
<pre><code>ConnectionMode connectionMode = maxConnectionsSizePerQuery &lt; sqlUnits.size() ? ConnectionMode.CONNECTION_STRICTLY : ConnectionMode.MEMORY_STRICTLY;
</code></pre>
<p>关于这个判断条件,我们可以使用一张简单的示意图来进行说明,如下所示:</p>
<p><img src="assets/Ciqc1F9HaoaAYskMAACJIb5G6C8859.png" alt="image" /></p>
<p><img src="assets/Ciqc1F9HaoaAYskMAACJIb5G6C8859.png" alt="png" /></p>
<p>如上图所示,我们可以看到如果每个数据库连接所指向的 SQL 数多于一条时,走的是内存限制模式,反之走的是连接限制模式。</p>
<h4>3.StreamQueryResult VS MemoryQueryResult</h4>
<p>在了解了 ConnectionMode连接模式 的设计理念后,我们再来看 StatementExecutor 的 executeQuery 方法返回的是一个 QueryResult。</p>

View File

@@ -219,7 +219,7 @@ private ResultSet currentResultSet;
</code></pre>
<p>在继续介绍 ShardingStatement 之前,我们先梳理一下与它相关的类层结构。我们在 <strong>“06 | 规范兼容JDBC 规范与 ShardingSphere 是什么关系?”</strong> 中的 ShardingConnection 提到ShardingSphere 通过适配器模式包装了自己的实现类,除了已经介绍的 ShardingConnection 类之外,还包含今天要介绍的 ShardingStatement 和 ShardingPreparedStament。</p>
<p>根据这一点,我们可以想象 ShardingStatement 应该具备与 ShardingConnection 类似的类层结构:</p>
<p><img src="assets/CgqCHl9MzLGAdeNfAACM0dnojxQ073.png" alt="Drawing 0.png" /></p>
<p><img src="assets/CgqCHl9MzLGAdeNfAACM0dnojxQ073.png" alt="png" /></p>
<p>然后我们来到上图中 AbstractStatementAdapter 类,这里的很多方法的风格都与 ShardingConnection 的父类 AbstractConnectionAdapter 一致,例如如下所示的 setPoolable 方法:</p>
<pre><code>public final void setPoolable(final boolean poolable) throws SQLException {
this.poolable = poolable;
@@ -367,7 +367,7 @@ private final List&lt;Object&gt; parameters = new ArrayList&lt;&gt;();
}
</code></pre>
<p>关于 AbstractShardingPreparedStatementAdapter 还需要注意的是它的<strong>类层结构</strong>,如下图所示,可以看到 AbstractShardingPreparedStatementAdapter 继承了 AbstractUnsupportedOperationPreparedStatement 类;而 AbstractUnsupportedOperationPreparedStatement 却又继承了 AbstractStatementAdapter 类并实现了 PreparedStatement</p>
<p><img src="assets/Ciqc1F9MzNeACiagAACzQd-8eig186.png" alt="Drawing 2.png" /></p>
<p><img src="assets/Ciqc1F9MzNeACiagAACzQd-8eig186.png" alt="png" /></p>
<p>形成这种类层结构的原因在于PreparedStatement 本来就是在 Statement 的基础上添加了各种参数设置功能换句话说Statement 的功能 PreparedStatement 都应该有。</p>
<p>所以一方面 AbstractStatementAdapter 提供了所有 Statement 的功能另一方面AbstractShardingPreparedStatementAdapter 首先把 AbstractStatementAdapter 所有的功能继承过来,但它自身可能有一些无法实现的关于 PreparedStatement 的功能,所以同样提供了 AbstractUnsupportedOperationPreparedStatement 类,并被最终的 AbstractShardingPreparedStatementAdapter 适配器类所继承。</p>
<p>这样就形成了如上图所示的复杂类层结构。</p>

View File

@@ -214,7 +214,7 @@ result = getResultSet(mergeEngine);
<p>所谓<strong>归并</strong>,就是将从各个数据节点获取的多数据结果集,通过一定的策略组合成为一个结果集并正确的返回给请求客户端的过程。</p>
<p><strong>按照不同的 SQL 类型以及应用场景划分</strong>,归并的类型可以分为遍历、排序、分组、分页和聚合 5 种类型,这 5 种类型是组合而非互斥的关系。</p>
<p>其中遍历归并是最简单的归并,而排序归并是最常用地归并,在下文我会对两者分别详细介绍。</p>
<p><img src="assets/CgqCHl9QzC6AA5U7AABkojINfPw834.png" alt="Lark20200903-185718.png" /></p>
<p><img src="assets/CgqCHl9QzC6AA5U7AABkojINfPw834.png" alt="png" /></p>
<p>归并的五大类型</p>
<p><strong>按照归并实现的结构划分</strong>ShardingSphere 中又存在流式归并、内存归并和装饰者归并这三种归并方案。</p>
<ul>
@@ -224,7 +224,7 @@ result = getResultSet(mergeEngine);
</ul>
<p>显然,流式归并和内存归并是互斥的,装饰者归并可以在流式归并和内存归并之上做进一步的处理。</p>
<p><strong>归并方案</strong>与归并类型之间同样存在一定的关联关系,其中遍历、排序以及流式分组都属于流式归并的一种,内存归并可以作用于统一的分组、排序以及聚合,而装饰者归并有分页归并和聚合归并这 2 种类型,它们之间的对应关系如下图所示:</p>
<p><img src="assets/CgqCHl9QzD6AG8aFAACQddjkR3E088.png" alt="Lark20200903-185710.png" /></p>
<p><img src="assets/CgqCHl9QzD6AG8aFAACQddjkR3E088.png" alt="png" /></p>
<p>归并类型与归并方案之间的对应关系图</p>
<h4>2.归并引擎</h4>
<p>讲完概念回到代码,我们首先来到 shardingsphere-merge 代码工程中的 MergeEngine 接口:</p>
@@ -234,7 +234,7 @@ result = getResultSet(mergeEngine);
}
</code></pre>
<p>可以看到 MergeEngine 接口非常简单,只有一个 merge 方法。在 ShardingSphere 中,该接口存在五个实现类,其类层结构如下所示:</p>
<p><img src="assets/CgqCHl9Qli6AUetjAAAuTLxL5xw054.png" alt="Drawing 2.png" /></p>
<p><img src="assets/CgqCHl9Qli6AUetjAAAuTLxL5xw054.png" alt="png" /></p>
<p>MergeEngine 类层结构图</p>
<p>从命名上看可以看到名称中带有“Encrypt”的两个 MergeEngine 与数据脱敏相关,放在后续专题中再做讲解,其余的三个我们会先做一些分析。</p>
<p>在此之前,我们还要来关注一下代表归并结果的 MergedResult 接口:</p>
@@ -337,11 +337,11 @@ public Object getValue(final int columnIndex, final Class&lt;?&gt; type) throws
<p>当在多个数据库中执行某一条 SQL 语句时,我们可以做到在每个库的内部完成排序功能。也就是说,我们的执行结果中保存着内部排好序的多个 QueryResult然后要做的就是把它们放在一个地方然后进行全局的排序。因为每个 QueryResult 内容已经是有序的,因此只需要将 QueryResult 中当前游标指向的数据值进行排序即可,相当于对多个有序的数组进行排序。</p>
<p>这个过程有点抽象,我们通过如下的示意图进行进一步说明。假设,在我们的健康任务 health_task 表中,存在一个健康点数字段 health_point用于表示完成这个健康任务能够获取的健康分数。</p>
<p>然后,我们需要根据这个 health_point 进行排序归并,初始的数据效果如下图所示:</p>
<p><img src="assets/Ciqc1F9QzRSADVUyAABkYnJfMvs829.png" alt="Lark20200903-190058.png" /></p>
<p><img src="assets/Ciqc1F9QzRSADVUyAABkYnJfMvs829.png" alt="png" /></p>
<p>三张 health_task 表中的初始数据</p>
<p>上图中展示了 3 张表返回的数据结果集,每个数据结果集都已经根据 health_point 字段进行了排序,但是 3 个数据结果集之间是无序的。排序归并的做法就是将 3 个数据结果集的当前游标指向的数据值进行排序,并放入到一个排序好的队列中。</p>
<p>在上图中可以看到 health_task0 的第一个 health_point 最小health_task1 的第一个 health_point 最大health_task2 的第一个 health_point 次之,因此队列中应该按照 health_task1health_task2 和 health_task0 的方式排序队列,效果如下:</p>
<p><img src="assets/CgqCHl9QzHiAf33WAABsaH9vLR0050.png" alt="Lark20200903-185846.png" />
<p><img src="assets/CgqCHl9QzHiAf33WAABsaH9vLR0050.png" alt="png" />
队列中已排序的三张 health_task 表</p>
<p>在 OrderByStreamMergedResult 中,我们可以看到如下所示的队列定义,用到了 JDK 中的 Queue 接口:</p>
<pre><code>private final Queue&lt;OrderByValue&gt; orderByValuesQueue;
@@ -422,10 +422,10 @@ public boolean next() throws SQLException {
}
</code></pre>
<p>这个过程同样需要用一系列图来进行解释。当进行第一次 next 调用时,排在队列首位的 health_task1 将会被弹出队列,并且将当前游标指向的数据值 50 返回。同时,我们还会将游标下移一位之后,重新把 health_task1 放入优先级队列。而优先级队列也会根据 health_task1 的当前数据结果集指向游标的数据值 45 进行排序根据当前数值health_task1 将会被排列在队列的第三位。如下所示:</p>
<p><img src="assets/Ciqc1F9QzJaANhr0AABsaCCFqA0376.png" alt="Lark20200903-185915.png" /></p>
<p><img src="assets/Ciqc1F9QzJaANhr0AABsaCCFqA0376.png" alt="png" /></p>
<p>第一次 next 之后的优先级队列中的三张 health_task 表</p>
<p>之前队列中排名第二的 health_task2 的数据结果集则自动排在了队列首位。而在进行第二次 next 时,只需要将目前排列在队列首位的 health_task2 弹出队列,并且将其数据结果集游标指向的值返回。当然,对于 health_task2 而言,我们同样下移游标,并继续将它加入优先级队列中,以此类推。</p>
<p><img src="assets/CgqCHl9QzJ2AMQzxAABrQS5M0oA899.png" alt="Lark20200903-185920.png" /></p>
<p><img src="assets/CgqCHl9QzJ2AMQzxAABrQS5M0oA899.png" alt="png" /></p>
<p>第二次 next 之后的优先级队列中的三张 health_task 表</p>
<p>可以看到,基于上述的设计和实现方法,对于每个数据结果集内部数据有序、而多数据结果集整体无序的情况下,我们无需将所有的数据都加载至内存即可进行排序。</p>
<p>因此ShardingSphere 在这里使用的是流式归并的方式,充分提高了归并效率。</p>

View File

@@ -213,7 +213,7 @@ function hide_canvas() {
</code></pre>
<p>显然,上述 SQL 的分组项与排序项完全一致,都是用到了 task_name 列,所以取得的数据是连续的。这样,分组所需的数据全部存在于各个数据结果集的当前游标所指向的数据值,因此可以采用流式归并。</p>
<p>如下图所示,我们在每个 health_task 结果集中,根据 task_name 进行了排序:</p>
<p><img src="assets/CgqCHl9V6UOALvzxAAB7G9wGDzY482.png" alt="Drawing 0.png" /></p>
<p><img src="assets/CgqCHl9V6UOALvzxAAB7G9wGDzY482.png" alt="png" /></p>
<p>我们先来看一些代码的初始化工作,回到 DQLMergeEngine找到用于分组归并的 getGroupByMergedResult 方法,如下所示:</p>
<pre><code>private MergedResult getGroupByMergedResult(final Map&lt;String, Integer&gt; columnLabelIndexMap) throws SQLException {
return selectSQLStatementContext.isSameGroupByAndOrderByItems()
@@ -229,13 +229,13 @@ function hide_canvas() {
</ul>
<p>这样当进行第一次 next 调用时,排在队列首位的 health_task0 将会被弹出队列并且将分组值同为“task1”其他结果集中的数据一同弹出队列。然后在获取了所有的 task_name 为“task1”的 health_point 之后,我们进行了累加操作。</p>
<p>所以在第一次 next 调用结束后,取出的结果集是 <strong>“task1”</strong> 的分数总和,即 46+43+40=129如下图所示</p>
<p><img src="assets/CgqCHl9V6V6AO3mBAAB_3I9Nrm8196.png" alt="Drawing 2.png" /></p>
<p><img src="assets/CgqCHl9V6V6AO3mBAAB_3I9Nrm8196.png" alt="png" /></p>
<ul>
<li><strong>第二次 next 调用</strong></li>
</ul>
<p>与此同时所有数据结果集中的游标都将下移至“task1”的下一个不同的数据值并且根据数据结果集当前游标指向的值进行重排序。在上图中我们看到第二个“task2”同时存在于 health_task0 和 health_task1 中这样包含名字为“task2”的相关数据结果集则排在的队列的前列。</p>
<p>当再次执行 next 调用时,我们获取了 <strong>“task2”</strong> 的分数并进行了累加,即 42+50=92如下图中所示</p>
<p><img src="assets/Ciqc1F9V8tmAFpx-AAB_pY0rk9I059.png" alt="Lark20200907-164326.png" /></p>
<p><img src="assets/Ciqc1F9V8tmAFpx-AAB_pY0rk9I059.png" alt="png" /></p>
<p>对于接下去的 next 方法,我们也是采用类似的处理机制,分别找到这三种 health_task 表中的“task3”“task4”“task5”等数据记录并依次类推。</p>
<p>有了对流式分组归并的感性认识之后,让我们回到源代码。我们先来看代表结果的 GroupByStreamMergedResult我们发现 GroupByStreamMergedResult 实际上是继承了上一课时中介绍的用于排序归并的 OrderByStreamMergedResult因此也用到了前面介绍的优先级队列 PriorityQueue 和 OrderByValue 对象。</p>
<p>但考虑到需要保存一些中间变量以管理运行时状态GroupByStreamMergedResult 中添加了如下所示的代表当前结果记录的 currentRow 和代表当前分组值的 currentGroupByValues 变量:</p>

View File

@@ -267,7 +267,7 @@ function hide_canvas() {
</code></pre>
<p>可以看到,当 loadBalanceStrategyConfiguration 配置不存在时,会直接使用 serviceLoader.newService() 方法完成 SPI 实例的创建。我们回顾 <strong>“13 | 微内核架构ShardingSphere 如何实现系统的扩展性?”</strong> 中的介绍,就会知道该方法会获取系统中第一个可用的 SPI 实例。</p>
<p>我们同样在 sharding-core-common 工程中找到了 SPI 的配置信息,如下所示:</p>
<p><img src="assets/Ciqc1F9Z3gGABwnKAAB1KuzlwD4571.png" alt="1.png" /></p>
<p><img src="assets/Ciqc1F9Z3gGABwnKAAB1KuzlwD4571.png" alt="png" /></p>
<p>针对 MasterSlaveLoadBalanceAlgorithm 的 SPI 配置</p>
<p>按照这里的配置信息,第一个获取的 SPI 实例应该是 RoundRobinMasterSlaveLoadBalanceAlgorithm<strong>轮询策略</strong>,它的 getDataSource 方法实现如下:</p>
<pre><code>@Override
@@ -395,7 +395,7 @@ public int executeUpdate() throws SQLException {
</code></pre>
<p>至此ShardingSphere 中与读写分离相关的核心类以及主要流程介绍完毕。总体而言,这部分的内容因为不涉及分片操作,所以整体结构还是比较直接和明确的。尤其是我们在了解了分片相关的 ShardingDataSource、ShardingConnection、ShardingStatement 和 ShardingPreparedStatement 之后再来理解今天的内容就显得特别简单,很多底层的适配器模式等内容前面都介绍过。</p>
<p>作为总结,我们还是简单梳理一下读写分离相关的类层结构,如下所示:</p>
<p><img src="assets/CgqCHl9Z3jGAH6CLAAByFyKIpQ0068.png" alt="image.png" /></p>
<p><img src="assets/CgqCHl9Z3jGAH6CLAAByFyKIpQ0068.png" alt="png" /></p>
<h3>从源码解析到日常开发</h3>
<p>在今天的内容中,我们接触到了分布式系统开发过程中非常常见的一个话题,即<strong>负载均衡</strong>。负载均衡的场景就类似于在多个从库中选择一个目标库进行路由一样通常需要依赖于一定的负载均衡算法ShardingSphere 中就提供了<strong>随机</strong><strong>轮询</strong>这两种常见的实现,我们可以在日常开发过程中参考它的实现方法。</p>
<p>当然,因为 MasterSlaveLoadBalanceAlgorithm 接口是一个 SPI所以我们也可以定制化新的负载均衡算法并动态加载到 ShardingSphere。</p>

View File

@@ -324,7 +324,7 @@ private Collection&lt;ResourceDataSource&gt; getResourceDataSources(final Map&lt
<p>要理解基于 XA 协议的 ShardingTransactionManager我们同样需要具备一定的理论知识。XA 是由 X/Open 组织提出的两阶段提交协议是一种分布式事务的规范XA 规范主要定义了面向全局的事务管理器 TransactionManagerTM和面向局部的资源管理器 ResourceManagerRM之间的接口。</p>
<p>XA 接口是双向的系统接口,在 TransactionManager以及一个或多个 ResourceManager 之间形成通信桥梁。通过这样的设计TransactionManager 控制着全局事务,管理事务生命周期,并协调资源,而 ResourceManager 负责控制和管理包括数据库相关的各种实际资源。</p>
<p>XA 的整体结构以及 TransactionManager 和 ResourceManager 之间的交互过程参考下图:</p>
<p><img src="assets/Ciqc1F9fO8mADc8eAAClhM8LyC0111.png" alt="Drawing 0.png" /></p>
<p><img src="assets/Ciqc1F9fO8mADc8eAAClhM8LyC0111.png" alt="png" /></p>
<p>XA 协议组成结构图</p>
<p>所有关于分布式事务的介绍中都必然会讲到两阶段提交,因为它是实现 XA 分布式事务的关键。我们知道在两阶段提交过程中存在协调者和参与者两种角色。在上图中XA 引入的 TransactionManager 充当着全局事务中的“协调者”角色,而图中的 ResourceManager 相当于“参与者”角色,对自身内部的资源进行统一管理。</p>
<p>理解了这些概念之后,我们再来看 Java 中的实现。作为 Java 平台中的事务规范JTAJava Transaction API也定义了对 XA 事务的支持。实际上JTA 是基于 XA 架构进行建模的,在 JTA 中,事务管理器抽象为 javax.transaction.TransactionManager 接口,并通过底层事务服务进行实现。</p>
@@ -372,16 +372,16 @@ public void commit() {
<p>这里的 XATransactionManager 就是对各种第三方 XA 事务管理器的一种抽象,封装了对</p>
<p>Atomikos、Bitronix 等第三方工具的实现方式。我们会在下一课时中对这个 XATransactionManager 以及 XAShardingTransactionManager 进行具体展开。</p>
<p>作为总结,我们梳理在 ShardingSphere 中与 XA 两阶段提交相关的核心类之间的关系,如下图所示:</p>
<p><img src="assets/Ciqc1F9fO-GALidCAABl39blOv8975.png" alt="Drawing 2.png" /></p>
<p><img src="assets/Ciqc1F9fO-GALidCAABl39blOv8975.png" alt="png" /></p>
<h4>2.SeataATShardingTransactionManager</h4>
<p>介绍完 XAShardingTransactionManager 之后,我们来看上图中 ShardingTransactionManager 接口的另一个实现类 SeataATShardingTransactionManager。因为基于不同技术体系和工作原理所以 SeataATShardingTransactionManager 中的实现方法也完全不同,让我们来看一下。</p>
<p>在介绍 SeataATShardingTransactionManager 之前,我们同样有必要对 Seata 本身做一些展开。与 XA 不同,<strong>Seata 框架</strong>中一个分布式事务包含三种角色,除了 XA 中同样具备的 TransactionManagerTM和 ResourceManagerRM 之外,还存在一个事务协调器 TransactionCoordinator (TC),维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。</p>
<p>其中TM 是一个分布式事务的发起者和终结者TC 负责维护分布式事务的运行状态,而 RM 则负责本地事务的运行。</p>
<p>Seata 的整体架构图如下所示:</p>
<p><img src="assets/Ciqc1F9fO-uAZyMCAAEoW9aLAuQ436.png" alt="Drawing 4.png" /></p>
<p><img src="assets/Ciqc1F9fO-uAZyMCAAEoW9aLAuQ436.png" alt="png" /></p>
<p>Seata 分布式事务组成结构图(来自 Seata 官网)</p>
<p>基于Seata 框架,一个分布式事务的执行流程包含如下五个步骤:</p>
<p><img src="assets/Ciqc1F9fO_WAHRyqAACL8p3Fa_E119.png" alt="Drawing 5.png" /></p>
<p><img src="assets/Ciqc1F9fO_WAHRyqAACL8p3Fa_E119.png" alt="png" /></p>
<p>我们同样会在下一课时中对这些步骤,以及其中涉及的核心类进行具体展开。</p>
<h3>从源码解析到日常开发</h3>
<p>今天的内容我们主要关注于 ShardingSphere 中对分布式事务的抽象过程,本身没有涉及过多的源码分析。我们学习的关注点在于掌握 XA 协议的特点和核心类,以及基于 Seata 框架完成一次分布式事务执行的过程。</p>

View File

@@ -288,7 +288,7 @@ function hide_canvas() {
}
</code></pre>
<p>在 ShardingSphere 中,继承 DatabaseTypeAwareSPI 接口的就只有 XADataSourceDefinition 接口,而后者存在一批实现类,整体的类层结构如下所示:</p>
<p><img src="assets/Ciqc1F9jCmiAI4cLAAE2ATnYWp4900.png" alt="Drawing 0.png" /></p>
<p><img src="assets/Ciqc1F9jCmiAI4cLAAE2ATnYWp4900.png" alt="png" /></p>
<p>XADataSourceDefinition 的实现类</p>
<p>这里以 MySQLXADataSourceDefinition 为例展开讨论,该类分别实现了 DatabaseTypeAwareSPI 和 XADataSourceDefinition 这两个接口中所定义的三个方法:</p>
<pre><code>public final class MySQLXADataSourceDefinition implements XADataSourceDefinition {
@@ -327,7 +327,7 @@ function hide_canvas() {
}
</code></pre>
<p>同样,在 sharding-transaction-xa-core 工程中,我们也发现了如下所示的 SPI 配置信息:</p>
<p><img src="assets/Ciqc1F9jCoWAOFRpAACUXKjEF6o633.png" alt="Drawing 1.png" /></p>
<p><img src="assets/Ciqc1F9jCoWAOFRpAACUXKjEF6o633.png" alt="png" /></p>
<p>sharding-transaction-xa-core 工程中的 SPI 配置</p>
<p>当根据数据库类型获取了对应的 XADataSourceDefinition 之后,我们就可以根据 XADriverClassName 来创建具体的 XADataSource</p>
<pre><code>private static XADataSource loadXADataSource(final String xaDataSourceClassName) {
@@ -360,7 +360,7 @@ function hide_canvas() {
}
</code></pre>
<p>DataSourcePropertyProvider 的实现类有两个,一个是 DefaultDataSourcePropertyProvider另一个是 HikariCPPropertyProvider。ShardingSphere 默认使用的是 HikariCPPropertyProvider这点可以从如下所示的 SPI 配置文件中得到确认:</p>
<p><img src="assets/Ciqc1F9jCpSAGChUAAB8-cv8fCU688.png" alt="Drawing 2.png" /></p>
<p><img src="assets/Ciqc1F9jCpSAGChUAAB8-cv8fCU688.png" alt="png" /></p>
<p>DataSourcePropertyProvider 的 SPI 配置</p>
<p>HikariCPPropertyProvider 实现了 DataSourcePropertyProvider 接口,并包含了对这些基础信息的定义:</p>
<pre><code>public final class HikariCPPropertyProvider implements DataSourcePropertyProvider {
@@ -398,7 +398,7 @@ function hide_canvas() {
}
</code></pre>
<p>至此,我们对 XADataSource 的构建过程描述完毕。这个过程不算复杂,但涉及的类比较多,值得我们以 XADataSourceFactory 为中心画一张类图作为总结:</p>
<p><img src="assets/Ciqc1F9jCqGAYmlZAACYlVXsQ44048.png" alt="image.png" /></p>
<p><img src="assets/Ciqc1F9jCqGAYmlZAACYlVXsQ44048.png" alt="png" /></p>
<h4>2.XAConnection</h4>
<p>讲完 XADataSource我们接着来讲 XAConnectionXAConnection 同样是 JDBC 规范中的接口。</p>
<p>负责创建 XAConnection 的工厂类 XAConnectionFactory 如下所示:</p>
@@ -429,7 +429,7 @@ function hide_canvas() {
}
</code></pre>
<p>XAConnectionWrapper 接口只有一个方法,即根据传入的 XADataSource 和一个普通 Connection 对象创建出一个新的 XAConnection 对象。XAConnectionWrapper 接口的类层结构如下所示:</p>
<p><img src="assets/Ciqc1F9jCrCAXTkWAAD4zJLBg8I622.png" alt="Drawing 4.png" /></p>
<p><img src="assets/Ciqc1F9jCrCAXTkWAAD4zJLBg8I622.png" alt="png" /></p>
<p>XAConnectionWrapper 接口的实现类</p>
<p>MySQLXAConnectionWrapper 中的 warp 方法如下所示:</p>
<pre><code>@Override

View File

@@ -328,7 +328,7 @@ public void rollback() {
</code></pre>
<p>对上述代码的理解也依赖与对 bitronix 框架的熟悉程度,整个封装过程简单明了。我们无意对 bitronix 框架做过多展开,而是更多关注于 ShardingSphere 中对 XATransactionManager 的抽象过程。</p>
<p>作为总结,我们在上一课时的基础上,进一步梳理了 XA 两阶段提交相关的核心类之间的关系,如下图所示:</p>
<p><img src="assets/CgqCHl9oXe6AK8JkAAByEfwPBs0489.png" alt="image.png" /></p>
<p><img src="assets/CgqCHl9oXe6AK8JkAAByEfwPBs0489.png" alt="png" /></p>
<h4>2.ShardingConnection</h4>
<p>上图展示了整个流程的源头是在 ShardingConnection 类。我们在 ShardingConnection 的构造函数中发现了创建 ShardingTransactionManager 的过程,如下所示:</p>
<pre><code>shardingTransactionManager = runtimeContext.getShardingTransactionManagerEngine().getTransactionManager(transactionType);
@@ -403,7 +403,7 @@ private void initSeataRPCClient() {
<p>回想我们在“09 | 分布式事务:如何使用强一致事务与柔性事务?”中关于 Seata 使用方式的介绍,不难理解这里通过 seata.conf 配置文件中所配置的 application.id 和 transaction.service.group 这两个配置项来执行初始化操作。</p>
<p>同时,对于 Seata 而言,它也提供了一套构建在 JDBC 规范之上的实现策略这点和“03 | 规范兼容JDBC 规范与 ShardingSphere 是什么关系?”中介绍的 ShardingSphere 与 JDBC 规范之间的兼容性类似。</p>
<p>而在命名上Seata 更为直接明了,使用 DataSourceProxy 和 ConnectionProxy 这种代理对象。以 DataSourceProxy 为例,我们可以梳理它的类层结构如下:</p>
<p><img src="assets/CgqCHl9oXgKACi15AAA7sb7XKlo735.png" alt="image" /></p>
<p><img src="assets/CgqCHl9oXgKACi15AAA7sb7XKlo735.png" alt="png" /></p>
<p>可以看到 DataSourceProxy 实现了自己定义的 Resource 接口,然后继承了抽象类 AbstractDataSourceProxy而后者则实现了 JDBC 中的 DataSource 接口。</p>
<p>所以,在我们初始化 Seata 框架时,同样需要根据输入的 DataSource 对象来构建 DataSourceProxy并通过 DataSourceProxy 获取 ConnectionProxy。SeataATShardingTransactionManager 类中的相关代码如下所示:</p>
<pre><code>@Override

View File

@@ -204,7 +204,7 @@ function hide_canvas() {
<p>与普通的编程模式一样,对于数据脱敏而言,我们同样先获取一个 DataSource 作为整个流程的入口,当然这里获取的不是一个普通的 DataSource而是一个专门针对数据脱敏的 EncryptDataSource。对于数据脱敏模块我们的思路还是从上到下从 EncryptDataSource 开始进入到 ShardingSphere 数据脱敏的世界中。</p>
<p>同时,我们这次讲解数据脱敏模块不是零基础,因为在前面介绍 ShardingDataSource、ShardingConnection、ShardingStatement 等内容时,已经对整个 SQL 执行流程的抽象过程做了全面介绍,所涉及的很多内容对于数据脱敏模块而言也都是适用的。</p>
<p>让我们结合下图来做一些回顾:</p>
<p><img src="assets/CgqCHl9sS86ASFTbAAB-yuAnnt4924.png" alt="image" /></p>
<p><img src="assets/CgqCHl9sS86ASFTbAAB-yuAnnt4924.png" alt="png" /></p>
<p>上图中,可以看到与数据脱敏模块相关的类实际上都继承了一个抽象类,而这些抽象类在前面的内容都已经做了介绍。因此,我们对数据脱敏模块将重点关注于几个核心类的讲解,对于已经介绍过的内容我们会做一些回顾,但不会面面俱到。</p>
<p>基于上图,我们从 EncryptDataSource 开始入手EncryptDataSource 的创建依赖于工厂类 EncryptDataSourceFactory其实现如下所示</p>
<pre><code>public final class EncryptDataSourceFactory {
@@ -238,7 +238,7 @@ private EncryptRuleConfiguration ruleConfiguration;
</code></pre>
<p>ShardingEncryptor 接口中存在一对用于加密和解密的方法,同时该接口也继承了 TypeBasedSPI 接口,意味着会通过 SPI 的方式进行动态类加载。</p>
<p>ShardingEncryptorServiceLoader 完成了这个工作,同时在 sharding-core-common 工程中,我们也找到了 SPI 的配置文件,如下所示:</p>
<p><img src="assets/CgqCHl9jFdmATyqTAAC9ufzW9Ag886.png" alt="Drawing 1.png" /></p>
<p><img src="assets/CgqCHl9jFdmATyqTAAC9ufzW9Ag886.png" alt="png" /></p>
<p>ShardingEncryptor 的 SPI 配置文件</p>
<p>可以看到这里有两个实现类,分别是 MD5ShardingEncryptor 和 AESShardingEncryptor。对于 MD5 算法而言我们知道它是单向散列的无法根据密文反推出明文MD5ShardingEncryptor 的实现类如下所示:</p>
<pre><code>public final class MD5ShardingEncryptor implements ShardingEncryptor {
@@ -501,7 +501,7 @@ public void rewrite(final ParameterBuilder parameterBuilder, final SQLStatementC
}
</code></pre>
<p>关于 EncryptAssignmentParameterRewriter 的实现,这里面涉及的类也比较多,我们可以先来画张图作为后续讨论的基础,如下所示:</p>
<p><img src="assets/CgqCHl9sS-OAJsx-AACWGMkVQXg279.png" alt="imag" /></p>
<p><img src="assets/CgqCHl9sS-OAJsx-AACWGMkVQXg279.png" alt="png" /></p>
<h4>3.EncryptAssignmentTokenGenerator</h4>
<p>讨论完 EncryptParameterRewriterBuilder 之后,我们再来讨论 EncryptTokenGenerateBuilder。这里我们也是以 EncryptAssignmentTokenGenerator 为例来进行展开,在这个类中,核心方法是 generateSQLTokens如下所示</p>
<pre><code>@Override

View File

@@ -324,7 +324,7 @@ private static final String PROPS_NODE = &quot;props&quot;;
private final String name;
</code></pre>
<p>基于 ShardingSphere 中对这些配置项的管理方式,我们可以将这些配置项与具体的存储结构相对应,如下所示:</p>
<p><img src="assets/Ciqc1F90KZeARJhmAACZRODYuiA394.png" alt="image" /></p>
<p><img src="assets/Ciqc1F90KZeARJhmAACZRODYuiA394.png" alt="png" /></p>
<p>有了配置项之后我们就需要对其进行保存ConfigurationService 的 persistConfiguration 方法完成了这一目的,如下所示:</p>
<pre><code>public void persistConfiguration(final String shardingSchemaName, final Map&lt;String, DataSourceConfiguration&gt; dataSourceConfigs, final RuleConfiguration ruleConfig,
final Authentication authentication, final Properties props, final boolean isOverwrite) {
@@ -359,7 +359,7 @@ private final String name;
<h4>2.StateService</h4>
<p>介绍完 ConfigurationService 类之后,我们来关注 ShardingOrchestrationFacade 类中的另一个核心变量 StateService。</p>
<p>从命名上讲StateService 这个类名有点模糊,更合适的叫法应该是 InstanceStateService用于管理数据库实例的状态即创建数据库运行节点并区分不同数据库访问实例。存放在注册中心中的数据结构包括 instances 和 datasources 节点,存储结构如下所示:</p>
<p><img src="assets/CgqCHl90KaaAYI5wAABQK1SKfyo990.png" alt="image" /></p>
<p><img src="assets/CgqCHl90KaaAYI5wAABQK1SKfyo990.png" alt="png" /></p>
<p>StateService 中保存着 StateNode 对象StateNode 中的变量与上面的数据结构示例相对应,如下所示:</p>
<pre><code>private static final String ROOT = &quot;state&quot;;
private static final String INSTANCES_NODE_PATH = &quot;instances&quot;;