Shardingsphere使用配置及内核剖析

导读

  • 本文主要通过源码分析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)
    ,根据我们前面得配置,通过第一个for循环得解析可以将我们配置得分库、分表策略、分库算法、分表算法解析到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.png
    ,发现运行时上下文进行了抽象,分片运行时上下文继承了 MultipleDataSourcesRuntimeContext 多数据源运行时上下文,而多数据源运行时上下文又继承了 AbstractRuntimeContext 抽象上下文。而创建 ShardingRuntimeContext 分片运行时上下文得时候会同时将分片规则保存在抽象类中,
    AbstractRuntimeContext 抽象上下文得初始化

    其中有几步关键点:
    ① 缓存整个分片规则,为后续得分片操作提供依据
    ② 缓存数据库类型,用于后续执行得时候加载对应数据库的元数据
    ③ 创建执行引擎根据当前执行连接是否持有事务(根据我们目前得配置是没有使用得)来决定是异步执行还是同步执行,根据配置得 executor.size 参数决定创建多少个线程得线程池。 默认不配置得话,使用 cachepool配置了就使用固定线程数得线程池
    解析引擎,用于解析SQL为抽象语法树,解析过程分为词法解析和语法解析。从3.0之后解析会全面替换为 ANTLR

ShardingRuntimeContext-> MultipleDataSourcesRuntimeContext -> AbstractRuntimeContext-> ExecutorEngine-> SQLParserEngine

  • 分片处理过程
    当然了,前面那么多得创建初始化过程都是为了分片做准备,我们接着就来着重分析一下分片处理得过程,下面我们通过查询请求来一探数据分片得整个过程:
    Ⅰ.
    org.apache.ibatis.executor.BaseExecutor#query
    ,当一个新鲜出炉的查询语句执行时,首先会经过Mybatis层(前面一系列的过程此处不重点分析,有兴趣的可以看一下作者之前分享的Mybatis流程图),然后调用 queryFromDatabase方法,此方法中,通过模板抽象方法org.apache.ibatis.executor.BaseExecutor#doQuery,来找到具体的查询实现(如果没有特殊配置,此处是SimpleExecutor),并将查询结果存入本地一级缓存中。而在org.apache.ibatis.executor.SimpleExecutor#doQuery中,
    org.apache.ibatis.executor.SimpleExecutor#doQuery
    ,此处会创建一个 Preparestatement实例,而此实例就是ShardingPreparedStatement。到此,我们离Shardingsphere的内核又近了一步
    org.apache.ibatis.executor.SimpleExecutor#prepareStatement

    Ⅱ. 顺着上述思路,我们继续Debug往下走。接着会经过Mybatis的预编译SQL处理器,然后调用PreparedStatement的execute方法
    org.apache.ibatis.executor.statement.PreparedStatementHandler#query
    , 通过①中的分析的我们知道,此处的PreparedStatement是ShardingPreparedStatement,所以调用的是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的执行方法
    ,SQL执行模板 SQLExecuteTemplate类通过委派其成员 ExecutorEngine 执行引擎来执行真正的操作。
    Ⅷ. 执行引擎对拆分的SQL执行单元执行处理,如图:
    ExecutorEngine 执行引擎的核心流程

    ① 并发执行(是否是并发执行通过 是否持有事务来判断的,例如 本地事务但是你修改为非自动提交事务,那么此时就是持有事务状态,则此时就是同步执行语句)
    ② 迭代出SQL执行组的第一个,其余的SQL异步执行
    ③ 同步执行第一个SQL执行组(方便与后面的执行组进行合并起来)
    ④ 通过其内置的线程池来异步执行SQL
    此时一条查询语句到这里就执行完了,接下来我们接着分析对查询结果进行处理的操作
    Ⅸ. 再回到Mybatis中,最后对查询的结果集进行处理( resultSetHandler.<E> handleResultSets(ps),此处是DefaultResultSetHandler 结果集处理器 ),如图:
    org.apache.ibatis.executor.resultset.DefaultResultSetHandler#handleResultSets
    ,首先调用getFirstResultSet去获取第一个结果集:
    DefaultResultSetHandler#getFirstResultSet
    ,此处的 Statement 实例是 ShardingPreparedStatement,所以①此处会调用其 getResultSet方法。
    Ⅹ. 结果归并:将查询返回的结果集进行合并处理,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#newInstance
    ,显然此处是查询语句,那么最终用于合并的引擎就是 ShardingDQLResultMerger,然后执行其merge方法。如图:
    org.apache.shardingsphere.sharding.merge.dql.ShardingDQLResultMerger#merge

    ⑦ 中判断sql中包含哪些关键字,然后创建对应的合并结果,如果条件都不满足,那么默认会使用 遍历流式归并方式合并数据。假设 我们此处SQL中带有 order by关键字,那么创建得合并结果对象就是OrderByStreamMergedResult
    OrderByStreamMergedResult构造器

    ⑧ 对创建出来的排序合并结果进行装饰操作(就是判断有没有别的关键字,例如:Limit,如果有就会创建LimitDecoratorMergedResult 装饰器对象,在之前的排序合并基础上又多一个 Limit功能),再回到 ShardingPreparedStatement中,会创建一个 ShardingResultSet对象设置到当前的成员变量currentResultSet中,并返回。 此时如果是批量的场景,返回的结果集中实际上已经包含了所有的结果集(前面存放在OrderByStreamMergedResult的 orderByValuesQueue 队列中),引用官方的一个图就是:
    排序归并流程
    ,当调用合并结果的 next方法时会执行如图的流程:
    调用Next方法
    ,最后流程又回到Mybatis 结果集处理上了,将结果返回给请求调用方。

写在最后:

  • 如果你从来没接触过Shardingsphere,建议先去了解一下,然后再结合作者本篇源码剖析,可能会对你更有帮助 Shardingsphere 中文官网
  • 忙了很久一段时间(比996还严重得那种😫),好久没有更新了,发现文采都没有以前那么好了🤭,大伙凑合看吧
  1. ☛ 文章要是勘误或者知识点说的不正确,欢迎评论,毕竟这也是作者通过阅读源码获得的知识,难免会有疏忽!
  2. 要是感觉文章对你有所帮助,不妨点个关注,或者移驾看一下作者的其他文集,也都是干活多多哦,文章也在全力更新中。
  3. 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处! ·
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容