带着问题,我花了不少时间深入了读了一下这部分的源码,终于搞清楚了,借本文分享一下。
本文主要环境是mybatis-plus-boot-starter 3.4.3,不过用的基本上仍然是mybatis的特性。
流程图
以查询为例,可以先看下流程图,大致了解一下整个过程。
关键的类和对象
在流程图里出现了一些类和其实例化的对象,有必要选其中关键的介绍一下。
MappedStatement
类全名org.apache.ibatis.mapping.MappedStatement,是一个final类。不要被名字误导,它和JDBC API中的java.sql.Statement没有实现关系。后者用于执行一条静态SQL并返回结果。
MappedStatement用来维护一个mapper中一个方法(对应一个sql)相关的信息,也就是将xml中的sql实例化成一个对象:
生成时机
使用sql的id通过MybatisConfiguration/Configuration获取。后者内部的Map(mappedStatements)会持有所有的mapper中的语句。
BoundSql
类全名:org.apache.ibatis.mapping.BoundSql
主要用于存储 SQL 语句以及该 SQL 语句所需的参数等信息。
如下图中:
sql字段是经过处理的sql
已经将${}直接替换为实际值,这也就会导致注入风险
#{}则使用?占位
parameterMappings记录参数的映射方法
parameterObject实际的参数值,对应的是java的mapper接口类里的参数。比如接口是9个参数,这里就是18个,其中paramxx是原参数全部用新key存储但是值没变的。这些参数不一定是sql里的?占位符所用到的,可能会多一些。
生成时机
Executor通过MapperStatement生成。
Connection
全名:java.sql.Connection,是JDBC API的一个核心接口。它的功能是:
建立与数据库的连接
创建执行对象,用于执行SQL语句的对象,也就是各种各样的Statement
管理事务,包括开启、提交、回滚。本文以查询为例,探讨分表的路由的原理,因此不会对事务相关话题做展开。
应用配置的是分库分表数据源,对应地,实例化的Connection对象是ShardingSphereConnection,如下:
展开dataSourceMap的value,可以看到更多数据源的配置,包括连接超时时间、jdbcUrl,db的用户名和明文的密码。如果运行容器是Springboot,那么这些配置可以在application.properties里看到。
Statement/ShardingSpherePreparedStatement
Statement接口在jdbc中的地位也很重要,它用于执行一条静态SQL并返回结果。
在分库分表的场景,它会生成各种中间过程的上下文Context,比如ExecutionContext、TrafficContext、RouteContext等,流程图中的LogicSQL也可以看作是一种上下文。Logic的sqlStatementContext把原始SQL进行了结构化的解析,比如from、where、group by等。在from字段可以进行多级的join嵌套,比如下图join的left是另一个join,right是一个表(逻辑表):
对于LogicSQL的where属性,其中的参数会用于参与库表的分片计算。
Statement也会借助其他的工具类,如SQLCheckEngine、KernelProcessor做处理,其中最关键的一点是,在KernelProcessor中生成ExecutionContext的方法内,生成RouteContext时获取物理表名:
public ExecutionContext generateExecutionContext(final LogicSQL logicSQL, final ShardingSphereMetaData metaData, final ConfigurationProperties props) {
RouteContext routeContext = route(logicSQL, metaData, props);
SQLRewriteResult rewriteResult = rewrite(logicSQL, metaData, props, routeContext);
ExecutionContext result = createExecutionContext(logicSQL, metaData, routeContext, rewriteResult);
logSQL(logicSQL, props, result);
return result;
}
上面的route()中,也包含了很多步:
WhereClauseShardingConditionEngine从Logic SQL的from属性获取分表相关的属性和值的代码如下
public List
继续看下去,getDQLRouteEngineForShardingTable如果通过bindingTableRule出要处理的表分片规则一致,那就直接返回,不重复处理。
RouteSQLRewriteEngine将SQL里的逻辑表改写为物理表,调用栈如下:
用于改写的代码:
public final String toSQL() {
if (context.getSqlTokens().isEmpty()) {
return context.getSql();
}
Collections.sort(context.getSqlTokens());
StringBuilder result = new StringBuilder();
result.append(context.getSql(), 0, context.getSqlTokens().get(0).getStartIndex());
for (SQLToken each : context.getSqlTokens()) {
result.append(each instanceof ComposableSQLToken ? getComposableSQLTokenText((ComposableSQLToken) each) : getSQLTokenText(each));
result.append(getConjunctionText(each));
}
return result.toString();
}
ShardingRule
用于存放分表和单表的规则,被Statement使用。为了便于叙述,举例如下:
一共三个数据源:不分表的ds-master、包括分表的ds0、包括分表的ds1
查询的逻辑表名是c_voucher,对应地分表是c_voucher_${companyId}_${subYear},也就是通过companyId和subYear两个参数确定实际的分表。
实际使用时有很多分片规则可以采用,比如按userId第几位路由到第几张表、某个字段取哈希值再取模路由。但是由于目前手上的项目找不到这种例子,不在此处剖析。处理方式是类似的,读者可以自行探索。
与application.properties对应关系(部分)
属性 ShardingRule中的属性名 application.properties配置 备注
数据源名称 dataSourceNames 属性前缀的一部分,比如
["ds-0","ds-1"]对应
spring.shardingsphere.datasource.ds-0.xxx=yyy
spring.shardingsphere.datasource.ds-1.xxx=zzz
分片算法 shardingAlgorithms
(Map) 既有表的也有库的。
对于表的:
spring.shardingsphere.rules.sharding.sharding-algorithms.ts-c-voucher.type=COMPLEX_INLINE
spring.shardingsphere.rules.sharding.sharding-algorithms.ts-c-voucher.props.algorithm-expression=c_voucher_$->{companyId}_$->{subYear}
对于库的:
spring.shardingsphere.rules.sharding.sharding-algorithms.t-database-inline.type=INLINE
spring.shardingsphere.rules.sharding.sharding-algorithms.t-database-inline.props.algorithm-expression=ds-$-> type定义具体分片算法类型,此处是COMPLEX_INLINE,支持比INLINE更复杂的表达式计算。
可以看出表和库的属性规则不太一样,表是ts-xx,库是t-xx
全局唯一键生成算法 keyGenerators
(本例中为null) 本例不涉及 雪花算法、UUID等
表规则 tableRules
(Map) 见下一节
绑定表规则 bindingTableRules 比如有a~i一共9张表,每3张的分片规则一样
spring.shardingsphere.rules.sharding.binding-tables[0]=a,b,c
spring.shardingsphere.rules.sharding.binding-tables[1]=d,e,f
spring.shardingsphere.rules.sharding.binding-tables[2]=g,h,i 将具有关联关系(会进行join)且按照同样的分片规则的表绑定到同一个表规则中,避免笛卡尔积运算
其它 本例不涉及,略
TableRule和application.properties的对应关系
ShardingRule包含了一个TableRule的map,包含了具体表分片的配置。这里单拎出来分析。
属性 TableRule的属性名 application.properties配置 备注
逻辑表名 logicTable -
实际的数据节点 actualDataNodes spring.shardingsphere.rules.sharding.tables.c_vouching_result.actual-data-nodes=ds-$->{0..1}.c_voucher_$->{1..2}_$-> 并不是真实的表名,此处对象中是:
ds-0.c_voucher_1_1
ds-0.c_voucher_2_2
ds-0.c_voucher_2_1
ds-0.c_voucher_2_2
ds-1.c_voucher_1_1
ds-1.c_voucher_2_2
ds-1.c_voucher_2_1
ds-1.c_voucher_2_2
实际的表 actualTables 同上 并不是真实的表名,和actualDataNodes类似
数据节点的索引map dataNodeIndexMap 同上 给actualDataNodes按顺序分配一个序号,本例0~7
表所在库的分片规则 databaseShardingStrategyConfig spring.shardingsphere.rules.sharding.tables.c_voucher.database-strategy.standard.sharding-column=schemaId
spring.shardingsphere.rules.sharding.tables.c_voucher.database-strategy.standard.sharding-algorithm-name=t-database-inline
表的分片规则 tableShardingStrategyConfig spring.shardingsphere.rules.sharding.tables.c_voucher.table-strategy.complex.sharding-columns=companyId,subYear
spring.shardingsphere.rules.sharding.tables.c_voucher.table-strategy.complex.sharding-algorithm-name=ts-c-vouching-result 分片的列、分片算法名称,对应的name是shardingAlgorithms中出现过的拼上前缀的表名
生成主键的列 generateKeyColumn - 不涉及,略
主键生成器名称 keyGeneratorName - 不涉及,略
实际数据源名称 actualDatasourceNames - ds-0,ds-1
数据源对应的分表 datasourceToTablesMap - 分表名同actualDataNodes
实现分库分表路由的关键步骤
根据流程图和上面的类,可以总结如下:
进行数据源的配置,包括库和表的分片规则(算法+来源列),以确保应用启动时组装的Connection包含这些路由信息,从而传递下去。
执行SQL时,组装的Statement根据逻辑表名找到对应的库表分片规则,从而推算出实际的表名:
生成LogicSQL,将SQL结构化
解析LogicSQL的from子句,获取分表相关的参数,并组装逻辑表到物理表的映射RouteUnit(同一个bindingTableRules的表只组装一个)
KernelProcessor将LogicSQL里需要替换的表名按RouteUnit改写成逻辑表名。