前言

sharding-jdbc主要功能是分片,我们实现不同分片算法来进行分库分表,另外一个扩展点就是主键生成, 本文主要记录下sharding-jdbc执行流程和分片路由具体实现以及主键生成,在工作中方便排查问题。

主要记录三个问题:

1.sharding-jdbc执行流程

2.自定义分片算法是如何被sharding-jdbc框架调用的

3.主键是在何处何时生成

4.扩展机制spi

1. sharding-jdbc处理流程

操作数据库套路是:数据源获取数据库连接,数据库连接生成Statement,然后执行Statement,获取sql执行结果。

那么对于sharding来说

入口获取数据库连接就是ShardingDataSource.getConnection()

接着生成PreparedStatement:ShardingConnection.prepareStatement(String),生成ShardingPreparedStatement

对于增删改查就是ShardingPreparedStatement的execute()、executeUpdate()、executeQuery()、、

以execute()为例:

//org.apache.shardingsphere.shardingjdbc.jdbc.core.statement.ShardingPreparedStatement.execute()
@Override
public boolean execute() throws SQLException {
    try {
        clearPrevious();//本地缓存清空
        shard();//路由,路由结果保存到this.routeResult。核心功能
        initPreparedStatementExecutor();//初始化执行器
        return preparedStatementExecutor.execute();//真实sql执行jdbc操作
    } finally {
        clearBatch();
    }
}

分析核心路由功能shard()

//org.apache.shardingsphere.shardingjdbc.jdbc.core.statement.ShardingPreparedStatement.shard()
private void shard() {
        routeResult = shardingEngine.shard(sql, getParameters());
    }

//org.apache.shardingsphere.core.BaseShardingEngine.shard(String, List)
public SQLRouteResult shard(final String sql, final Listparameters) {
    ListclonedParameters = cloneParameters(parameters);
    SQLRouteResult result = route(sql, clonedParameters);//路由核心实现
    result.getRouteUnits().addAll(HintManager.isDatabaseShardingOnly() ? convert(sql, clonedParameters, result) : rewriteAndConvert(sql, clonedParameters, result));//非hint,重写sql
    if (shardingProperties.getValue(ShardingPropertiesConstant.SQL_SHOW)) {
        boolean showSimple = shardingProperties.getValue(ShardingPropertiesConstant.SQL_SIMPLE);
        SQLLogger.logSQL(sql, showSimple, result.getSqlStatement(), result.getRouteUnits());//打印真实sql
    }
    return result;
}
//org.apache.shardingsphere.core.PreparedQueryShardingEngine.route(String, List)
@Override
protected SQLRouteResult route(final String sql, final Listparameters) {
    return routingEngine.route(parameters);
}
//org.apache.shardingsphere.core.route.PreparedStatementRoutingEngine.route(List)
public SQLRouteResult route(final Listparameters) {
    if (null == sqlStatement) {
        sqlStatement = shardingRouter.parse(logicSQL, true);//代码@1
    }
    return masterSlaveRouter.route(shardingRouter.route(logicSQL, parameters, sqlStatement));//代码@2
}

代码@1

//org.apache.shardingsphere.core.route.router.sharding.ParsingSQLRouter.parse(String, boolean) 
//解析sql
@Override
public SQLStatement parse(final String logicSQL, final boolean useCache) {
    parsingHook.start(logicSQL);//sharding-jdbc为开发预留的钩子,我们可以实现钩子接口在解析sql前后做一些扩展
    try {
        SQLStatement result = new SQLParsingEngine(databaseType, logicSQL, shardingRule, shardingMetaData.getTable(), parsingResultCache).parse(useCache);//代码@1.1,解析sql的核心
        parsingHook.finishSuccess(result, shardingMetaData.getTable());
        return result;
        // CHECKSTYLE:OFF
    } catch (final Exception ex) {
        // CHECKSTYLE:ON
        parsingHook.finishFailure(ex);
        throw ex;
    }
}

代码@1处解析sql比较复杂,只需要知道是解析sql,解析结果SQLStatement,这个是也不是我们的关注点,知道有个hook接口可以在sql解析前后进行扩展即可,比如通过该Hook可以用作计算sql执行时长。

知道增删改查对对应的SQLStatement如下:

对于insert来说SQLStatement是InsertStatement。DML

对于update delete语句来说SQLStatement是DMLStatement。DML

对于select语句来说SQLStatement是SelectStatement。 DQL

SQLStatement是个逻辑sql。

类关系图如下:

sharding-jdbc处理流程源码分析_sharding-jdbc

代码@2

masterSlaveRouter是读写分离路由,不使用的情况下,可以忽略。

分片的路由核心实现在shardingRouter.route(logicSQL, parameters, sqlStatement),下面分析这个

//org.apache.shardingsphere.core.route.router.sharding.ParsingSQLRouter.route(String, List, SQLStatement)
public SQLRouteResult route(final String logicSQL, final Listparameters, final SQLStatement sqlStatement) {
        OptionalgeneratedKey = sqlStatement instanceof InsertStatement
                ? GeneratedKey.getGenerateKey(shardingRule, parameters, (InsertStatement) sqlStatement) : Optional.absent();//代码@2.1
        SQLRouteResult result = new SQLRouteResult(sqlStatement, generatedKey.orNull());//代码@2.2
        OptimizeResult optimizeResult = OptimizeEngineFactory.newInstance(shardingRule, sqlStatement, parameters, generatedKey.orNull()).optimize();//代码@2.3
        if (generatedKey.isPresent()) {
            setGeneratedKeys(result, generatedKey.get());//代码@2.4
        }
        boolean needMerge = false;
        if (sqlStatement instanceof SelectStatement) {
            needMerge = isNeedMergeShardingValues((SelectStatement) sqlStatement);//代码@2.5
        }
        if (needMerge) {
            checkSubqueryShardingValues(sqlStatement, optimizeResult.getShardingConditions());
            mergeShardingValues(optimizeResult.getShardingConditions());//代码@2.6
        }
        RoutingResult routingResult = RoutingEngineFactory.newInstance(shardingRule, shardingMetaData.getDataSource(), sqlStatement, optimizeResult).route();//代码@2.7
        if (sqlStatement instanceof SelectStatement && null != ((SelectStatement) sqlStatement).getLimit() && !routingResult.isSingleRouting()) {
            result.setLimit(getProcessedLimit(parameters, (SelectStatement) sqlStatement));//代码@2.8
        }
        if (needMerge) {
            Preconditions.checkState(1 == routingResult.getTableUnits().getTableUnits().size(), "Must have one sharding with subquery.");//代码@2.9
        }
        result.setRoutingResult(routingResult);//代码@2.10
        result.setOptimizeResult(optimizeResult);
        return result;//代码@2.11
    }

从上面路由核心代码可以看出,ShardingRouter是解析和路由的核心接口,其实现类为ParsingSQLRouter,它使用四个引擎对sql进行解析、解析和重写,这四个引擎为:

  • SQLParsingEngine
    解析sql,返回SQLStatement作为解析的结果。

  • OptimizeEngine
    对SQLStatement进行优化,返回ShardingConditions对象。

  • RoutingEngine
    根据库表分片配置以及ShardingConditions找到目标库表,返回RoutingResult对象。

  • SQLRewriteEngine
    根据路由结果重写sql。

代码@2.1处:如果是insert,则生成分布式主键,GeneratedKey封装了分片键和分布式主键值。比如,insert语句,这里使用Snowflake算法生成分布式主键。

代码@2.2处:创建sql路由结果对象SQLRouteResult,封装SQLStatement和分布式主键对象GeneratedKey。此时SQLRouteResult只是包含了sql语句和主键值,并没有生成实际待执行sql。

代码@2.3处:使用OptimizeEngine对SQLStatement进行优化,返回OptimizeResult对象。该对象在重写sql时候用到,作用就是对SQLStatement进行优化,返回ShardingConditions对象。

代码@2.4处:保存生成的分布式主键。

代码@2.5处:select语句是否需要合并结果

代码@2.6处:需要合并查询结果,则合并

代码@2.7处:使用不同的RoutingEngine生成路由结果RoutingResult。比如标准分片是StandardRoutingEngine、复合分片是ComplexRoutingEngine、广播是DatabaseBroadcastRoutingEngine、不分片是DefaultDatabaseRoutingEngine等。 这里是核心代码,总体功能就是路由,找到实际的数据源和真实表

sharding-jdbc处理流程源码分析_sharding-jdbc_02

代码@2.8处:select语句设置limit。既然分库分表了,通常也就不使用分页了。

代码@2.9处:预检,需要合并结果,需要分片键在查询结果上。

代码@2.10处:把路由结果RoutingResult、优化结果OptimizeResult保存到SQLRouteResult。

代码@2.11处:返回sql路由结果对象SQLRouteResult,该对象封装了路由结果,知道要到哪个真实库去执行哪个真实表。

核心代码@2.7处分析

RoutingEngineFactory.newInstance()根据不同的分片规则采用对应的RoutingEngine生成路由结果RoutingResult,以标准分片路由为例

//org.apache.shardingsphere.core.route.type.standard.StandardRoutingEngine.route()
@Override
public RoutingResult route() {
    return generateRoutingResult(getDataNodes(shardingRule.getTableRule(logicTableName)));//1.getTableRule根据逻辑表获取TableRule,2.getDataNodes根据TableRule和分片算法获取真实的数据源和真实表Collection}
//shardingRule.getTableRule(logicTableName)根据逻辑表从分片规则ShardingRule获取表规则TableRule,TableRule信息封装的较多,有逻辑表、全部数据源等

//org.apache.shardingsphere.core.route.type.standard.StandardRoutingEngine.getDataNodes(TableRule)
//获取真实节点,真实数据源和真实表。DataNode封装了真实数据源和真实表
private CollectiongetDataNodes(final TableRule tableRule) {
    if (shardingRule.isRoutingByHint(tableRule)) {//hint路由
        return routeByHint(tableRule);
    }
    if (isRoutingByShardingConditions(tableRule)) {//条件路由,即非hint路由
        return routeByShardingConditions(tableRule);
    }
    return routeByMixedConditions(tableRule);
}

private CollectionrouteByShardingConditions(final TableRule tableRule) {
    return optimizeResult.getShardingConditions().getShardingConditions().isEmpty() ? route(tableRule, Collections.emptyList(), Collections.emptyList())
        : routeByShardingConditionsWithCondition(tableRule);
}

private Collectionroute(final TableRule tableRule, final ListdatabaseShardingValues, final ListtableShardingValues) {
    CollectionroutedDataSources = routeDataSources(tableRule, databaseShardingValues);//获取真实数据源
    Collectionresult = new LinkedList<>();
    for (String each : routedDataSources) {
        result.addAll(routeTables(tableRule, each, tableShardingValues));//获取真实表
    }
    return result;
}

//根据分片键获取数据源
private CollectionrouteDataSources(final TableRule tableRule, final ListdatabaseShardingValues) {
    CollectionavailableTargetDatabases = tableRule.getActualDatasourceNames();
    if (databaseShardingValues.isEmpty()) {
        return availableTargetDatabases;
    }
    Collectionresult = new LinkedHashSet<>(shardingRule.getDatabaseShardingStrategy(tableRule).doSharding(availableTargetDatabases, databaseShardingValues));//这里通过分片策略调用自定义的分片算法
    Preconditions.checkState(!result.isEmpty(), "no database route info");
    return result;
}

//根据分片键获取DataNode,即数据源+真实表
private CollectionrouteTables(final TableRule tableRule, final String routedDataSource, final ListtableShardingValues) {
    CollectionavailableTargetTables = tableRule.getActualTableNames(routedDataSource);
    CollectionroutedTables = new LinkedHashSet<>(tableShardingValues.isEmpty() ? availableTargetTables
                                                          : shardingRule.getTableShardingStrategy(tableRule).doSharding(availableTargetTables, tableShardingValues));//这里通过分片策略调用自定义的分片算法
    Preconditions.checkState(!routedTables.isEmpty(), "no table route info");
    Collectionresult = new LinkedList<>();
    for (String each : routedTables) {
        result.add(new DataNode(routedDataSource, each));
    }
    return result;
}

可以看到route()方法是入口,此方法首先通过ShardingRule获取到逻辑表所对应的TableRule对象,在sharding-jdbc启动阶段,TableRule保存了逻辑表对应的实际的库表关系集合,接着根据库和表的ShardingStrategy的类型走了三个不同的方法:routeByHint()、routeByShardingConditions()、routeByMixedConditions(),不管走哪个方法最终都会执行到含有三个参数的route()方法,此方法先调用routeDataSources()方法路由数据源(库),接着调用routeTables()方法路由表,路由库表的方法也很简单:

从TableRule中获取可用的库表集合。
从TableRule中获取库表的分片策略ShardingStrategy对象。
执行ShardingStrategy持有的分片算法ShardingAlgorithm的doSharding()方法返回路由到的库表。
路由的结果以RoutingResult的形式返回,接着调用SQLRewriteEngine重写sql,因为此时sql中的表还只是逻辑表名,并不是具体的哪个表,接着生成SQLUnit,并最终以SQLRouteResult形式返回路由结果。

重点是个SQLRouteResult,关系较复杂,类图封装关系如下

sharding-jdbc处理流程源码分析_sharding-jdbc_03

使用xmind画出的处理流程

sharding-jdbc处理流程源码分析_sharding-jdbc_04

思维导图地址:https://gitee.com/yulewo123/mdpicture/blob/master/document/sharding-jdbc%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B.xmind

总结:

sharding-jdbc的处理流程核心就是路由,即根据分片键以及算法从从TableRule.actualDataNodes获取真实库表对象DataNode。那么TableRule是怎么来的呢?是ShardingRule根据逻辑表获取,而ShardingRule是核心,在sharding-jdbc启动时候就创建完成。

路由获取后,就可以重写sql,然后通过jdbc执行sql到真实的数据源执行真实sql。

关键debug点记录如下,工作中遇到问题,方便快速回顾debug定为问题

org.apache.shardingsphere.core.route.router.sharding.ParsingSQLRouter.route(String, List