2月第二讲:mybatis是如何识别分表位的

带着问题,我花了不少时间深入了读了一下这部分的源码,终于搞清楚了,借本文分享一下。

本文主要环境是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()中,也包含了很多步:

抖下d载阴


抖下d载阴


抖下d载阴


抖下d载阴


抖下d载阴


抖下d载阴

WhereClauseShardingConditionEngine从Logic SQL的from属性获取分表相关的属性和值的代码如下
public List createShardingConditions(final SQLStatementContext sqlStatementContext, final List parameters) {
    if (!(sqlStatementContext instanceof WhereAvailable)) {
        return Collections.emptyList();
    }
    List result = new ArrayList<>();
    for (WhereSegment each : ((WhereAvailable) sqlStatementContext).getWhereSegments()) {
        result.addAll(createShardingConditions(sqlStatementContext, each.getExpr(), parameters));
    }
    return result;
}
构造condition后,ShardingRouteEngineFactory会把from中和逻辑表名不一致的表剔除掉,这个怎么理解呢?比如SQL里一共涉及3张表table_a、table_b、table_c,其中table_c其中table_c写作table_c_${},直接通过${}把分表名拼接好了,变成table_c_001。table_c_001这个表名是在分表规则里找不到的,因此也不会应用任何分表规则。
https://m.56.com/view/id-MTkyODM5ODY2.html?e1dy
https://www.56.com/u21/v_MTkyODM5ODY2.html?f1dy
https://m.56.com/view/id-MTkzMDM5NzE4.html?k1dy
https://www.56.com/u25/v_MTkzMDM5NzE4.html?k1dy
https://i.56.com/u/shunm_56124919816/
https://i.56.com/u/shunm_56124948477/
private static ShardingRouteEngine getDQLRoutingEngine(final ShardingRule shardingRule, final ShardingSphereSchema schema, final SQLStatementContext sqlStatementContext, 
                                                       final ShardingConditions shardingConditions, final ConfigurationProperties props) {
    Collection tableNames = sqlStatementContext.getTablesContext().getTableNames();
    if (shardingRule.isAllBroadcastTables(tableNames)) {
        return sqlStatementContext.getSqlStatement() instanceof SelectStatement ? new ShardingUnicastRoutingEngine(tableNames) : new ShardingDatabaseBroadcastRoutingEngine();
    }
    if (sqlStatementContext.getSqlStatement() instanceof DMLStatement && shardingConditions.isAlwaysFalse() || tableNames.isEmpty()) {
        return new ShardingUnicastRoutingEngine(tableNames);
    }
    // from子句里的表名,如果不是逻辑表名,不会按照分表处理
    Collection shardingLogicTableNames = shardingRule.getShardingLogicTableNames(tableNames);
    if (shardingLogicTableNames.isEmpty()) {
        return new ShardingIgnoreRoutingEngine();
    }
    return getDQLRouteEngineForShardingTable(shardingRule, schema, sqlStatementContext, shardingConditions, props, shardingLogicTableNames);
}

继续看下去,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改写成逻辑表名。
 

                          
 

你可能感兴趣的:(mybatis)