导读
- 本文主要通过源码分析Shardingsphere原理
- 关键字 :Shardingsphere使用、Shardingsphere源码、Shardingsphere执行流程
- 版本:Shardingsphere 4.1.1
-
Shardingsphere 配置
- Yaml
- Java Config
- SpringBoot 👍
- Spring命名空间
-
Shardingsphere 功能
- 数据分片 👍
- 读写分离
- 强制路由(可以归为数据分片得一种)
- 数据加密
- 分布式事务
总之,功能很强大!!!
当然,本篇并不会分析所有功能点,而是讲解最常用得数据分片配置用法以及原理。
如何在 SpringBoot中配置数据分片策略 ?
项目中引入依赖:
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.1.1</version>
</dependency>
配置中心(Apollo、Nacos)或者本地项目中引入配置文件,添加如下配置:
spring.shardingsphere.datasource.names=ds0,ds1
spring.shardingsphere.datasource.ds0.type= com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.ds0.driver-class-name= com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds0.url= jdbc:mysql://xxxxxx:3306/ds0
spring.shardingsphere.datasource.ds0.username= xxx
spring.shardingsphere.datasource.ds0.password= xxx
spring.shardingsphere.datasource.ds1.type= com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.ds1.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds1.url=jdbc:mysql://xxxxxx:3306/ds1
spring.shardingsphere.datasource.ds1.username= xxx
spring.shardingsphere.datasource.ds1.password= xxx
spring.shardingsphere.sharding.tables.logical_table.database-strategy.inline.sharding-column = sharding_column
spring.shardingsphere.sharding.tables.logical_table.database-strategy.inline.algorithm-expression = ds$->{ShardingHash.shardingDBValue(sharding_column,2)}
spring.shardingsphere.sharding.tables.logical_table.actual-data-nodes = ds$->{0..1}.logical_table_$->{0..3}
spring.shardingsphere.sharding.tables.logical_table.table-strategy.inline.sharding-column = sharding_column
spring.shardingsphere.sharding.tables.logical_table.table-strategy.inline.algorithm-expression = logical_table_$->{ShardingHash.shardingTBValue(sharding_column,2,4)}
上述配置关键点:
① spring.shardingsphere.datasource.names=ds0,ds1 表示两个数据源(一般测试分片要至少配置两个)
② spring.shardingsphere.sharding.tables.logical_table.database-strategy.inline.sharding-column = sharding_column 表示配置得逻辑表为 logical_table (实际上就是平常SQL文件中DML语句对应得表);数据库得分片字段为 sharding_column
③ spring.shardingsphere.sharding.tables.logical_table.database-strategy.inline.algorithm-expression =ds$ ->{ShardingHash.shardingDBValue(sharding_column,2)}
表示数据分片的算法是内联,并且指定了分片表达式(通过Groovy来解析表达式,当然也有简单的表达式 例如:sharding_column %2)。 $->{}
是标准语法,也可以是 ${}
(不推荐这么配置
④ spring.shardingsphere.sharding.tables.logical_table.actual-data-nodes = ds$->{0..1}.logical_table_$->{0..3}
表示一条sql 会被路由的真实数据节点总共可以有这么多。
例如:此处会有两个数据源:ds0,ds1
; 4张表:logical_table_0 、logical_table_1、logical_table_2、logical_table_3
, 那么组合起来就会存在8种路由节点(意味着sql中若不带分片键,会导致全表路由( 很严重 ),后面会分析)。
⑤ spring.shardingsphere.sharding.tables.logical_table.table-strategy.inline.sharding-column = sharding_column 表示 logical_table逻辑表对应的分表字段是 sharding_column
⑥ spring.shardingsphere.sharding.tables.logical_table.table-strategy.inline.algorithm-expression = logical_table_$->{ShardingHash.shardingTBValue(sharding_column,2,4)} 表示 分表对应的表达式(同上述分库
)
⑦ 以上策略需要根据自己项目中的情况,来选取库分片字段与表分片字段以及相应的分片算法(一般情况下,库表分片字段最好一致,尽量的减少分布式事务发生以及减少实际的路由节点)
到这里,项目中已经集成了 Shardingsphere了。 用法简单的是不是有点怀疑自己漏了一些配置(实际上配置方式就是这么简单~)。
Shardingsphere 如何做到分库分表的 ?
-
配置加载过程
首先由于我们引入得 spring boot starter得依赖,我们大致就可以猜到:Shardingsphere 肯定也有一个 类似得自动装配类,此处是(SpringBootConfiguration),不清楚自动装配得可以参考一下作者之前得文章(一文读懂SpringBoot自动装配原理)。找到了入口,我们就直接看一下源码:
Shardingsphere得入口配置类
① 此处表明,自动装配在DataSourceAutoConfiguration
这个自动装配类之前完成。也就是Shardingsphere创建得数据源就是全局得数据源,项目只要涉及到对数据库得任何操作都会经过ShardingDataSource
得这一层处理(④中创建得)。正是基于此,为后面得数据分片以及一些扩展埋下基础。还有一点就是,如果我们项目中使用了Mybatis这个ORM框架的话,会发现Mybatis得starter启动配置类是在DataSourceAutoConfiguration
装配之后再进行装配得,如图:Mybatis入口配置类
那么此时Mybatis使用得数据源就是 Shardingsphere配置得 ShardingDataSource。
② 将之前配置得规则映射到此配置文件中,为创建数据源得过程提供配置信息。
③dataSourceMap
对象存放得是配置得所有数据源映射信息,为后面获取数据库连接以及数据分片提供基础能力。
④ 通过ShardingDataSourceFactory
这个工厂类来创建ShardingDataSource
数据源,ShardingDataSource得内部结构
① 初始化路由装饰器(路由引擎
,SPI得方式,用户可以扩展)、创建SQL改写上下文装饰器(改写引擎
,同上)、创建结果处理引擎(归并引擎
,用于对查询结果合并处理,同上)
② 创建运行时上下文(全局分片运行时上下文,用于保存分片所需得相关配置),这两处关键步骤后面会单独分析。
由于我们目前演示得是基于分片得策略配置,所以只有ShardingRuleCondition
才满足装配条件。
而在创建数据源得同时,会将配置得规则解析成ShardingRule
,供后续得数据库操作提供分片核心能力。其中有一个重要得配置转换过程。会将分表规则、分库规则、分表算法、分库算法等都解析到对应得ShardingRuleConfiguration
通用分片配置类中, 如图:ShardingRuleConfigurationYamlSwapper#swap(YamlShardingRuleConfiguration)TableRuleConfiguration
中,每一张表都会对应一个配置类:
for (Entry<String, YamlTableRuleConfiguration> entry : yamlConfiguration.getTables().entrySet()) {
YamlTableRuleConfiguration tableRuleConfig = entry.getValue();
tableRuleConfig.setLogicTable(entry.getKey());
result.getTableRuleConfigs().add(tableRuleConfigurationYamlSwapper.swap(tableRuleConfig));
}
SpringBootConfiguration -> ShardingDataSourceFactory -> ShardingRule -> ShardingDataSource -> ShardingRuntimeContext
-
分片运行时上下文创建过程
① 如上面所说,创建数据源得时候会在构造器中将运行时上下文ShardingRuntimeContext
一同创建出来,ShardingRuntimeContext得构造器如下图:
ShardingRuntimeContext得构造器image.pngMultipleDataSourcesRuntimeContext
多数据源运行时上下文,而多数据源运行时上下文又继承了AbstractRuntimeContext
抽象上下文。而创建ShardingRuntimeContext
分片运行时上下文得时候会同时将分片规则保存在抽象类中,AbstractRuntimeContext 抽象上下文得初始化
其中有几步关键点:
① 缓存整个分片规则,为后续得分片操作提供依据
② 缓存数据库类型,用于后续执行得时候加载对应数据库的元数据
③ 创建执行引擎
,根据当前执行连接是否持有事务(根据我们目前得配置是没有使用得)来决定是异步执行还是同步执行
,根据配置得executor.size
参数决定创建多少个线程得线程池。默认不配置得话,使用 cachepool
,配置了就使用固定线程数得线程池
。
④解析引擎
,用于解析SQL为抽象语法树,解析过程分为词法解析和语法解析。从3.0之后解析会全面替换为ANTLR
。
ShardingRuntimeContext-> MultipleDataSourcesRuntimeContext -> AbstractRuntimeContext-> ExecutorEngine-> SQLParserEngine
-
分片处理过程
当然了,前面那么多得创建初始化过程都是为了分片做准备,我们接着就来着重分析一下分片处理得过程,下面我们通过查询请求来一探数据分片得整个过程:
Ⅰ.org.apache.ibatis.executor.BaseExecutor#queryqueryFromDatabase
方法,此方法中,通过模板抽象方法org.apache.ibatis.executor.BaseExecutor#doQuery
,来找到具体的查询实现(如果没有特殊配置,此处是SimpleExecutor
),并将查询结果存入本地一级缓存中。而在org.apache.ibatis.executor.SimpleExecutor#doQuery
中,org.apache.ibatis.executor.SimpleExecutor#doQueryPreparestatement
实例,而此实例就是ShardingPreparedStatement
。到此,我们离Shardingsphere的内核又近了一步org.apache.ibatis.executor.SimpleExecutor#prepareStatement
Ⅱ. 顺着上述思路,我们继续Debug往下走。接着会经过Mybatis的预编译SQL处理器,然后调用PreparedStatement的execute方法org.apache.ibatis.executor.statement.PreparedStatementHandler#queryShardingPreparedStatement
,所以调用的是ShardingPreparedStatement的execute方法。
Ⅲ. 接下来真正开始切入到Shardingsphere的执行逻辑中了。ShardingPreparedStatement#execute
如图在execute方法中,① 首先清理本地PreparedStatementExecutor
中缓存的sql相关信息(创建执行单元的时候会将sql相关信息缓存到本地) ② 然后执行prepare
方法,此方法中有两个很关键的操作:执行路由策略和SQL改写策略
(这两步是分片的核心,另外也都是可供使用者扩展的)。如图:BasePrepareEngine#prepare
Ⅳ.路由引擎
:首先来看一下executeRoute
方法,BasePrepareEngine#executeRoute
① 获取 已经注册的RouteDecorator
类实例(前面创建数据源的时候初始化的,当然使用者也可以通过SPI的方式扩展自己所需要的)过滤掉泛型是BaseRule
类型的(ShardingRule是其子类,所以重新的时候覆写 getType方法时,一定要是BaseRule类型的)
② 实例化路由装饰器
③ 调用模板方法route
,最终会调用到DataNodeRouter 的 executeRoute方法,如图方法:DataNodeRouter#executeRoute解析引擎
: 通过SQLParserEngine
解析SQL(并且此处默认是会将解析后的语句缓存起来,也就证实了前面会什么会先清理缓存),然后通过调用parse0
方法解析SQL并缓存,如图:SQLParserEngine#parse0默认提供的路由装饰器ShardingRouteDecorator
,此处又有两个比较关键的步骤:ShardingRouteDecorator
① 获取分片条件:根据不同的语句创建不同的 条件解析引擎来构造分片条件(获取的分片条件用于在执行路由判断时决定使用哪种分片策略)
② 通过工厂创建出ShardingRouteEngine
实例,一般情况下 会创建出来ShardingStandardRoutingEngine
(没有配置什么骚操作的情况下),然后调用 标准路由执行引擎的 路由方法
Ⅴ. 到这里,终于要执行路由了!!!如图:ShardingStandardRoutingEngine#route
① 根据路由节点生成路由结果RouteResult
② 获取数据节点:此处获取的就是真实的SQL路由情况(比如:ds0.table_0),首先判断是否使用直接路由
(强制路由),若使用则走强制路由的分片算法去计算分片;然后再判断是否根据分片条件去路由,若有的话,则根据配置的分片算法(内联)根据分片值计算出来具体分到哪个库哪张表;若都没有的话,则直接走混合路由的处理逻辑。
Ⅵ. 我们此处分析上述第二种情况,根据分片条件去执行分片。根据分片条件路由
① 首先获取数据的分片路由值,再获取表的分片路由值,然后调用route0
方法根据数据库分片路由值与表分片路由值去获取路由
② 路由数据源
③ 路由表
最后封装成路由节点。
Ⅶ. 回到ShardingPreparedStatement
中,调用initPreparedStatementExecutor())
初始化PreparedStatementExecutor
实例 并将解析出来的执行上下文中的相关SQL语句组设置到缓存中(此处会获取到需要执行的SQL集合,主要是通过
maxConnectionsSizePerQuery每次执行时最大连接数来判断sql执行单元应该分成几组,maxConnectionsSizePerQuery的值默认是1。则表示,如果真实的sql有10条,那么每组拆分10条,总共拆分成1组,此时会判断 maxConnectionsSizePerQuery 是否大于10,小于的话则会选择当前批次执行的是连接限制模式(只允许占用一个库的一个连接),相反则是内存限制模式,不会限制创建的连接数
),然后调用执行器的执行方法:如图:org.apache.shardingsphere.shardingjdbc.executor.PreparedStatementExecutor#execute
① 获取sql执行回调类(真正操作数据库)
② 调用executeCallback
方法,此方法继承自父类AbstractStatementExecutor
,直接来看一下父类中的方法:AbstractStatementExecutor的执行方法SQLExecuteTemplate
类通过委派其成员ExecutorEngine
执行引擎来执行真正的操作。
Ⅷ. 执行引擎对拆分的SQL执行单元执行处理,如图:ExecutorEngine 执行引擎的核心流程
① 并发执行(是否是并发执行通过是否持有事务来判断的
,例如 本地事务但是你修改为非自动提交事务,那么此时就是持有事务状态,则此时就是同步执行语句)
② 迭代出SQL执行组的第一个,其余的SQL异步执行
③ 同步执行第一个SQL执行组(方便与后面的执行组进行合并起来)
④ 通过其内置的线程池来异步执行SQL
此时一条查询语句到这里就执行完了,接下来我们接着分析对查询结果进行处理的操作
Ⅸ. 再回到Mybatis中,最后对查询的结果集进行处理( resultSetHandler.<E> handleResultSets(ps),此处是DefaultResultSetHandler 结果集处理器 ),如图:org.apache.ibatis.executor.resultset.DefaultResultSetHandler#handleResultSetsgetFirstResultSet
去获取第一个结果集:DefaultResultSetHandler#getFirstResultSetgetResultSet
方法。
Ⅹ.结果归并
:将查询返回的结果集进行合并处理,Shardingsphere 的归并引擎功能上划分:遍历归并、排序归并(SQL中存在ORDER BY语句)、分组归并(SQL中有GroupBy子句)、聚合归并(含有聚合函数)、分页归并(含有Limit关键字)
,归并引擎的详细介绍请参阅:归并引擎,如图:image.png
① 获取所有Statement对应的结果集,此处是拿到真正数据源所对应的Statement实例,比如:我现在的数据源是HikariDateSource
,那么拿到的就是HikariProxyPreparedStatement
.如图:ShardingPreparedStatement#getResultSets
②执行合并逻辑
:首先将结果集封装成流式查询结果对象StreamQueryResult
,接着创建合并引擎MergeEngine
,然后调用合并引擎的合并方法:org.apache.shardingsphere.underlying.pluggble.merge.MergeEngine#merge
③ 实例化合并引擎处理器ResultProcessEngine
④ 调用MergeEntry
的 process 方法,委派来进行合并逻辑。org.apache.shardingsphere.underlying.merge.MergeEntry#process
⑤ ⑥ 中,判断若是ResultMergerEngine
类型的合并引擎,则调用其merge
方法执行真正的合并逻辑。如下图ShardingResultMergerEngine 类图ShardingResultMergerEngine#newInstance
方法来实例化真正用于合并数据流的引擎。ShardingResultMergerEngine#newInstanceShardingDQLResultMerger
,然后执行其merge
方法。如图:org.apache.shardingsphere.sharding.merge.dql.ShardingDQLResultMerger#merge
⑦ 中判断sql中包含哪些关键字,然后创建对应的合并结果,如果条件都不满足,那么默认会使用 遍历流式归并方式合并数据。假设 我们此处SQL中带有order by
关键字,那么创建得合并结果对象就是OrderByStreamMergedResult
。OrderByStreamMergedResult构造器
⑧ 对创建出来的排序合并结果进行装饰操作(就是判断有没有别的关键字,例如:Limit,如果有就会创建LimitDecoratorMergedResult
装饰器对象,在之前的排序合并基础上又多一个 Limit功能),再回到ShardingPreparedStatement
中,会创建一个ShardingResultSet
对象设置到当前的成员变量currentResultSet
中,并返回。 此时如果是批量的场景,返回的结果集中实际上已经包含了所有的结果集(前面存放在OrderByStreamMergedResult的orderByValuesQueue
队列中),引用官方的一个图就是:排序归并流程调用Next方法
写在最后:
- 如果你从来没接触过Shardingsphere,建议先去了解一下,然后再结合作者本篇源码剖析,可能会对你更有帮助 Shardingsphere 中文官网
- 忙了很久一段时间(比996还严重得那种😫),好久没有更新了,发现文采都没有以前那么好了🤭,大伙凑合看吧
- ☛ 文章要是勘误或者知识点说的不正确,欢迎评论,毕竟这也是作者通过阅读源码获得的知识,难免会有疏忽!
- ☛ 要是感觉文章对你有所帮助,不妨点个关注,或者移驾看一下作者的其他文集,也都是干活多多哦,文章也在全力更新中。
- ☛ 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处! ·