【Sharding-Sphere】- 01 SQL路由

396 阅读18分钟
原文链接: mp.weixin.qq.com

【Nacos源码分析】- 02 获取配置流程

【Nacos源码分析】- 01 ConfigService创建流程

【Java并发编程】- 03 MESI、内存屏障

【Spring源码】- 11  Spring AOP之编程式事务

【编程开发】- 01 日志框架

概述

SQL路由Sharding-jdbc数据分片核心流程中非常重要的一个流程(见下图),通过SQL路由决定了 SQL具体会在哪些库上触发,以及具体对应的物理表是哪些等等,这节就来分析下SQL路由的流程。

如下图,先对表t_order和表 t_order_item进行分库分表,它们分片规则都一致:根据user_id % 2 逻辑进行分库,并根据order_id % 2 逻辑进行分表。

对应编程方式实现如下:

package binding;/** * @author zhang_zhang * @Copyright © 2020 tiger Inc. All rights reserved. * @create 2020-06-07 21:30 */@Slf4jpublic class BindingTableTest {    //数据源    private Map<String, DataSource> dataSourceMap = new HashMap<>();    //分片规则    private ShardingRule shardingRule;    //数据库类型    private MySQLDatabaseType databaseType = new MySQLDatabaseType();    /**     * 创建数据源     * @return     */    private void dataSourceMap(){        // 配置第一个数据源        HikariDataSource dataSource1 = new HikariDataSource();        dataSource1.setJdbcUrl("jdbc:mysql://localhost:3306/ds0?serverTimezone=UTC");        dataSource1.setUsername("root");        dataSource1.setPassword("123456");        dataSourceMap.put("ds0", dataSource1);        // 配置第二个数据源        HikariDataSource dataSource2 = new HikariDataSource();        dataSource2.setJdbcUrl("jdbc:mysql://localhost:3306/ds1?serverTimezone=UTC");        dataSource2.setUsername("root");        dataSource2.setPassword("123456");        dataSourceMap.put("ds1", dataSource2);    }    private void shardingRule(){        //1.创建ShardingRuleConfiguration        ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();        shardingRuleConfig.getTableRuleConfigs().add(orderRule());        shardingRuleConfig.getTableRuleConfigs().add(orderItemRule());        List<String> dbs = Arrays.asList("ds0", "ds1");        this.shardingRule = new ShardingRule(shardingRuleConfig, dbs);       /* List<TableRule> tableRules = (List<TableRule>) this.shardingRule.getTableRules();        BindingTableRule bindingTableRule1 = new BindingTableRule(Arrays.asList(tableRules.get(0)));        BindingTableRule bindingTableRule2 = new BindingTableRule(Arrays.asList(tableRules.get(1)));        this.shardingRule.getBindingTableRules().add(bindingTableRule1);        this.shardingRule.getBindingTableRules().add(bindingTableRule2);*/    }    private TableRuleConfiguration orderRule(){        // 配置Order表规则        TableRuleConfiguration orderTableRuleConfig =                new TableRuleConfiguration("t_order","ds${0..1}.t_order_${0..1}");        // 配置分库 + 分表策略        orderTableRuleConfig.setDatabaseShardingStrategyConfig(new StandardShardingStrategyConfiguration("user_id", new MyPreciseShardingAlgorithm()));        orderTableRuleConfig.setTableShardingStrategyConfig(new StandardShardingStrategyConfiguration("order_id", new MyPreciseShardingAlgorithm()));        return orderTableRuleConfig;    }    private TableRuleConfiguration orderItemRule(){        // 配置Order表规则        TableRuleConfiguration orderTableRuleConfig =                new TableRuleConfiguration("t_order_item","ds${0..1}.t_order_item_${0..1}");        // 配置分库 + 分表策略        orderTableRuleConfig.setDatabaseShardingStrategyConfig(new StandardShardingStrategyConfiguration("user_id", new MyPreciseShardingAlgorithm()));        orderTableRuleConfig.setTableShardingStrategyConfig(new StandardShardingStrategyConfiguration("order_id", new MyPreciseShardingAlgorithm()));        return orderTableRuleConfig;    }    @Before    public void init(){        this.dataSourceMap();        this.shardingRule();    }}

现在我们要执行表t_order和表 t_order_item关联查询(如下),现在我们就来分析下SQL路由是如何处理这条 查询SQL得到。

String sql = "SELECT i.* FROM t_order o JOIN t_order_item i ON o.order_id=i.order_id WHERE (o.order_id = 10 and o.user_id=20) or o.order_id = 11";

核心入口

SQL路由入口是 ShardingRouteDecorator#decorate这个方法:

public RouteContext decorate(final RouteContext routeContext, final ShardingSphereMetaData metaData, final ShardingRule shardingRule, final ConfigurationProperties properties) {    //SQLStatementContext主要包含SQLStatement,而SQLStatement是对SQL解析后内容的封装    SQLStatementContext sqlStatementContext = routeContext.getSqlStatementContext();    //sql中?对应的实参列表    List<Object> parameters = routeContext.getParameters();    //InsertStatement和UpdateStatement会存在validator    ShardingStatementValidatorFactory.newInstance(            sqlStatementContext.getSqlStatement()).ifPresent(validator -> validator.validate(shardingRule, sqlStatementContext.getSqlStatement(), parameters));    /**     * 1、创建ShardingConditions,其中包含一组ShardingCondition;     * 2、每个ShardingCondition对应一个AndPredicate;     * 3、每个AndPredicate又对应一组PredicateSegment;     * 4、每个PredicateSegment对应一个column和rightValue,即每个PredicateSegment对应where子句中的一个限制列     *     * 比如:SELECT i.* FROM t_order o JOIN t_order_item i ON o.order_id=i.order_id WHERE (o.order_id = 10 and o.user_id=20) or o.order_id = 11     * 1、对where子句可以拆分出两个AndPredicate,(o.order_id = 10 and o.user_id=20)和o.order_id = 11各对应一个AndPredicate,因此,这里会解析出两个ShardingCondition     * 2、第一个AndPredicate可以解析出两个PredicateSegment,分别对应:o.order_id = 10和o.user_id=20     *      注意:AndPredicate解析PredicateSegment只会涉及到参与分片的列,比如这里的库分片列user_id和表分片列order_id,对其它列会忽略,     *      比如o.order_id = 10 and o.user_id=20 and o.id=30,解析结果一样,id由于不是分片列会被忽略掉     * 3、每个AndPredicate都会被封装成ShardingCondition,存储到ShardingConditions中,     *     * 所以,从上面分析来看,ShardingConditions就是从SQL的where子句中提取中分片列的过滤信息,后续就是根据这个信息进行SQL路由     */    ShardingConditions shardingConditions = getShardingConditions(parameters, sqlStatementContext, metaData.getSchema(), shardingRule);    //是否需要合并分片数据,查询且带有子查询,且查询的表是分片表,才返回true    boolean needMergeShardingValues = isNeedMergeShardingValues(sqlStatementContext, shardingRule);    if (sqlStatementContext.getSqlStatement() instanceof DMLStatement && needMergeShardingValues) {        checkSubqueryShardingValues(sqlStatementContext, shardingRule, shardingConditions);        mergeShardingConditions(shardingConditions);    }    //创建SQL路由引擎    ShardingRouteEngine shardingRouteEngine = ShardingRouteEngineFactory.newInstance(shardingRule, metaData, sqlStatementContext, shardingConditions, properties);    //SQL路由引擎进行路由解析    RouteResult routeResult = shardingRouteEngine.route(shardingRule);    if (needMergeShardingValues) {        Preconditions.checkState(1 == routeResult.getRouteUnits().size(), "Must have one sharding with subquery.");    }    //返回一个已经完成分库分表的RouteContext对象,注意:这里是新创建的    return new RouteContext(sqlStatementContext, parameters, routeResult);}

大致说明:

  • SQLStatement是对 SQL解析的封装(见下图):

    • projections:列部分信息
    • tableReferences:关联表信息
    • where:where子句解析信息
    • groupBy:group by子句解析信息
    • orderBy:order by子句解析信息
    • limit:limit子句解析信息
  • ShardingConditions就是从 SQLwhere子句中提取中分片列的过滤信息,后续就是根据这个信息进行 SQL路由,解析出的ShardingConditions如下图所示:

    注意:ShardingConditions涉及到的解析 where子句中限制列都只会处理分片列(库分片列或表分片列),因为主要解析出这些列用于后续的SQL路由,其它列对它来说没有意义会被忽略。

    • 首先,这里会被解析出两个ShardingCondition,上面分析过,比较好理解;
    • 每个ShardingCondition里面会包含一组 RouteValue,每个RouteValue包含那个表哪个列以及限制值三部分,直白说就是where子句中每个关于分片列的限制都会被解析成一个 RouteValue对象,比如o.order_id = 10 and o.user_id=20,因为 order_iduser_id都是分片列,这里会解析出两个 RouteValue
  • ShardingConditions提取完成,这时 SQL路由真正的主角ShardingRouteEngine登场,从名称就可以看出,它是 SQL路由引擎,由它来处理SQL路由ShardingRouteEngine不是单例模式,而是每个SQL都会对应一个实例。

路由引擎

下面我们就进入到ShardingRouteEngine#route,看看 SQL路由引擎处理流程。这里进入的是ShardingComplexRoutingEngine#route方法,这个方法完成对整个 SQL路由解析:

/** * 1、用于对真个SQL语句进行SQL路由,每个SQL语句对应一个ShardingComplexRoutingEngine实例,它内部又会按照逻辑表进行拆分, * 2、一个SQL语句可能对应多个逻辑表,比如管理查询join,每个逻辑表又会对应一个ShardingStandardRoutingEngine对象用于对逻辑表进行SQL路由, * 3、每个逻辑表SQL路由结果是RouteResult,如果存在多张逻辑表,则使用ShardingCartesianRoutingEngine路由引擎进行笛卡尔积操作 * 4、经过上述流程就完成对整个SQL语句SQL路由处理 * * @param shardingRule sharding rule * @return */@Overridepublic RouteResult route(final ShardingRule shardingRule) {    /**     * 每个逻辑表的SQL路由信息都会封装成一个RouteResult,RouteResult封装了SQL路由信息:originalDataNodes和routeUnits     */    Collection<RouteResult> result = new ArrayList<>(logicTables.size());    Collection<String> bindingTableNames = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);    for (String each : logicTables) {        Optional<TableRule> tableRule = shardingRule.findTableRule(each);        if (tableRule.isPresent()) {            if (!bindingTableNames.contains(each)) {                /**                 * 每张逻辑表生成一个RouteResult                 * RouteResult中包含RouteUnit类型集合,每个RouteUnit实际上代码一张具体的物理表,比如ds0.t_order0等                 * 对每张逻辑表生成一个ShardingStandardRoutingEngine实例,然后调用route()方法进行该逻辑表SQL路由                 */                result.add(new ShardingStandardRoutingEngine(tableRule.get().getLogicTable(), sqlStatementContext, shardingConditions, properties).route(shardingRule));            }            shardingRule.findBindingTableRule(each).ifPresent(bindingTableRule -> bindingTableNames.addAll(                    bindingTableRule.getTableRules().stream().map(TableRule::getLogicTable).collect(Collectors.toList())));        }    }    if (result.isEmpty()) {        throw new ShardingSphereException("Cannot find table rule and default data source with logic tables: '%s'", logicTables);    }    if (1 == result.size()) {//如果size=1,则表示没有没有多表关联查询        return result.iterator().next();    }    /**     * 这里表示存在多表关联查询,即对多个逻辑表的SQL路由进行笛卡尔积     */    return new ShardingCartesianRoutingEngine(result).route(shardingRule);}

大致流程:

  • 一条SQL可能存在多个 逻辑表,比如上面关联查询join就涉及两张逻辑表: t_ordert_order_item
  • 这个方法就对SQL路由解析拆分成对 SQL中每个逻辑表进行路由解析,每张逻辑表SQL路由解析结果封装成 RouteResult
  • 当对所有逻辑表进行SQL路由解析完成后,然后就进行判断,如果只有一张表,很简单直接返回这张表 SQL路由解析结果RouteResult即可,如果是多张表就使用笛卡尔积: new ShardingCartesianRoutingEngine(result).route(shardingRule)处理;注意:这里的笛卡尔积只会发生在同库下,不会出现跨库操作,Sharding-jdbc是不支持跨库操作的,在业务开发中千万注意跨库,不然会出现问题。

单表路由

上面我们分析了SQL路由会被拆分成对多张逻辑表进行 SQL路由,然后使用笛卡尔积方式,下面我们继续跟踪下代码看下如何对单张逻辑表进行SQL路由

对逻辑表SQL路由会创建 ShardingStandardRoutingEngine实例,然后调用route()方法:


                                        /** * 用于对具体某张逻辑表进行SQL路由 * @param shardingRule sharding rule * 
                                            @return */@Overridepublic RouteResult 
                                                route(final ShardingRule shardingRule) {    //不支持多表INSERT、UPDATE、DELETE操作    
                                                    if (isDMLForModify(sqlStatementContext) && 1 != ((TableAvailable) sqlStatementContext).getAllTables().size()) {        
                                                        throw new ShardingSphereException("Cannot support Multiple-Table for '%s'.", sqlStatementContext.getSqlStatement());    }    
                                                            /**     * getDataNodes()进行分库分表,返回的是DataNode集合,每个DataNode包含数据库名称和表名称,代码的就是数据库中具体的一张物理表     * generateRouteResult()将分库分表返回的DataNode集合包装成RouteResult     */    
                                                                return generateRouteResult(getDataNodes(shardingRule, shardingRule.getTableRule(logicTableName)));}
                                                                    

这个方法最关键的是getDataNodes(shardingRule, shardingRule.getTableRule(logicTableName))generateRouteResult()方法只是用来完成对SQL路由后的结果进行封装。

那下面我们就来看下getDataNodes()方法:


                                                                            private Collection<DataNode> getDataNodes
                                                                                (final ShardingRule shardingRule, final TableRule tableRule) {    
                                                                                    //hint方式分片    if (isRoutingByHint(shardingRule, tableRule)) {        
                                                                                        return routeByHint(shardingRule, tableRule);    }    //ShardingConditions方式分片    
                                                                                            if (isRoutingByShardingConditions(shardingRule, tableRule)) {        return routeByShardingConditions(shardingRule, tableRule);    }    
                                                                                                //复合方式    return routeByMixedConditions(shardingRule, tableRule);}
                                                                                                    

这里会判断分片方式:HintCondition或混合方式,这里我们跟踪下更常规的Condition方式 routeByShardingConditions()

private Collection<DataNode> routeByShardingConditions(final ShardingRule shardingRule, final TableRule tableRule) {    /**     * 1、如果ShardingCondition空,route0(shardingRule, tableRule, Collections.emptyList(), Collections.emptyList())则获取到的是该表配置的全库全表路由     * 2、ShardingCondition存在时,则根据ShardingCondition信息进行SQL路由     */    return shardingConditions.getConditions().isEmpty()            ? route0(shardingRule, tableRule, Collections.emptyList(), Collections.emptyList()) : routeByShardingConditionsWithCondition(shardingRule, tableRule);}

这里有个判断,如果shardingConditions集合为空,则说明 SQLwhere子句中没有关于分片列信息,这里返回的是全库全表路由,在实际开发中一定要注意这点可能会优化你 SQL

继续跟踪:

/** * routeByShardingConditionsWithCondition():处理一张逻辑表根据shardingConditions信息解析出需要路由到哪些节点上,即获取DataNode集合, * DataNode对应的就是一张具体的物理表,封装了库名称dataSourceName和表名称tableName * * 一个ShardingCondition相当于where中的一个条件,多个ShardingCondition之间是or关系,一个逻辑表解析SQL路由又可以拆分成对 一个逻辑表+ShardingCondition 进行SQL路由解析,调用的是route0()方法 * 将查询语句所有的shardingCondition集合都解析完成,就是这个逻辑表全部的路由数据 * * @param shardingRule * @param tableRule * @return */private Collection<DataNode> routeByShardingConditionsWithCondition(final ShardingRule shardingRule, final TableRule tableRule) {    Collection<DataNode> result = new LinkedList<>();    for (ShardingCondition each : shardingConditions.getConditions()) {        /**         * route0()是对一个 一个逻辑表+shardingCondition进行分库分表路由解析,生成DataNode集合,每个DataNode对应一张具体物理表,比如:{"dataSourceName":"ds0", "tableName":"t_order0"}         * 将SQL解析出的所有shardingCondition遍历一遍,就是这张逻辑表对应的SQL路由信息         *         * getShardingValuesFromShardingConditions():一个ShardingCondition中可能包含多个RouteValue,比如有库分片的RouteValue,也有表分片的RouteValue,这个方法就是获取具体相关的RouteValue         * 比如下面,第一个就是获取库分片相关的RouteValue,第二个就是获取表分片相关的RouteValue,将这些RouteValue传递给具体的分片算法,获取到SQL路由信息         *         */        Collection<DataNode> dataNodes = route0(shardingRule, tableRule,                 getShardingValuesFromShardingConditions(shardingRule, shardingRule.getDatabaseShardingStrategy(tableRule).getShardingColumns(), each),                getShardingValuesFromShardingConditions(shardingRule, shardingRule.getTableShardingStrategy(tableRule).getShardingColumns(), each));        result.addAll(dataNodes);        originalDataNodes.add(dataNodes);    }    return result;}

大致说明:

  • 这个方法用于对单张逻辑表进行SQL路由,这里又会按照 ShardingCondition维度对SQL路由进行拆分: 单张逻辑表+单个ShardingCondition 进行SQL路由解析;
  • 前面分析过ShardingCondition,这是一个非常重要的概念:
    • WHERE (o.order_id = 10 and o.user_id=20) or o.order_id = 11:前面分析过,这里会被解析成两个 ShardingCondition,即:o.order_id = 10 and o.user_id=20o.order_id = 11这两个;
    • WHERE (o.order_id = 10 or o.order_id = 20) and o.user_id = 11:这个也会被解析成两个 ShardingCondition,分别是o.order_id = 10 and o.user_id = 11o.order_id = 20 and o.user_id= 11
    • ShardingCondition内部是一组 and条件ShardingCondition间是 or关系
    • 理解了ShardingCondition,就可以明白为啥可以根据 ShardingCondition维度进行拆分,对单张逻辑表+ 单个ShardingCondition 进行SQL路由解析,然后遍历所有的 ShardingCondition,就可以对逻辑表完成全部路由解析。

下面继续跟踪下如何对单张逻辑表+ 单个ShardingCondition 进行SQL路由解析:


                                                                                                        /** * 分库分表 * * routeDataSources()方法进行分库,返回分库列表 * routeTables()方法在分库的基础上进行分表,返回DataNode集合 * DataNode主要包含两个元素dataSourceName和tableName,每个DataNode代表具体数据库上的一张物理表 * * route0():解析出一个ShardingCondition对应的DataNode集合,每个DataNode对应的是一张具体的物理表 *      1、先进行routeDataSources()方法进行分库,返回分库列表,比如返回:ds0、ds1 *      2、然后遍历每个分片库,调用routeTables()进行同库下表分片 *      3、最后返回DataNode集合,每个DataNode对应一张具体的物理表,比如:{"dataSourceName":"ds0", "tableName":"t_order0"} * * @param shardingRule * 
                                                                                                            @param tableRule * @param databaseShardingValues * 
                                                                                                                @param tableShardingValues * @return */
                                                                                                                    private Collection<DataNode> route0
                                                                                                                        (final ShardingRule shardingRule, final TableRule tableRule, 
                                                                                                                            final List<RouteValue> databaseShardingValues, final List<RouteValue> tableShardingValues) {    
                                                                                                                                //先使用库分片列相关的RouteValue进行库路由    Collection<String> routedDataSources = routeDataSources(shardingRule, tableRule, databaseShardingValues);    Collection<DataNode> result = 
                                                                                                                                    new LinkedList<>();    for (String each : routedDataSources) {
                                                                                                                                        //遍历库路由结果集        //然后对每个路由库,进行具体的表路由        result.addAll(routeTables(shardingRule, tableRule, each, tableShardingValues));    }    
                                                                                                                                            return result;}
                                                                                                                                                

大致说明:

  • 先会调用routeDataSources()进行库上路由;
  • 然后在每个路由库上调用routeTables()进行库下标路由;
  • routeDataSources()routeTables()路由逻辑都是借助于分片策略ShardingStrategy实现, ShardingStrategy又通过分片算法ShardingAlgorithm实现用户自定义的一些扩展,具体分片策略这里就不继续展开。

总结

自此,Sharding-jdbc中如何完成 SQL路由的流程基本都分析完成,SQL路由流程是非常关键的一步,决定了一个 SQL到底会在哪些库、哪些表上进行执行,只有理解了SQL路由,业务开发中进行数据库设计、 SQL设计等才能很好的规避一些问题,以及SQL如何更加高效执行等。

上面从源码角度分析了SQL路由核心流程,下面通过一个案例总结下,如下图:

大致说明:

  1. SQlwhere子句中提取出所有的ShardingCondition,这里提取出两个如上图;
  2. SQL路由解析从逻辑表和 ShardingCondition两个维度进行拆分;
  3. t_order和第一个 ShardingCondition进行SQL路由解析结果是 ds0_t_order_0;
  4. t_order和第二个 ShardingCondition进行SQL路由解析结果是 ds0_t_order_1ds1_t_order_1;
  5. 汇总逻辑表t_order路由结果: ds0_t_order_0ds0_t_order_1ds1_t_order_1这三个;
  6. 同理,接下里对逻辑表t_order_item进行路由解析,注意:虽然有两个 ShardingCondition,都是o.开头,即都是关于逻辑表 t_order的,t_order_item则不存在 ShardingCondition信息,所以这里解析出的路由结果是:全库全表;
  7. 最后,对逻辑表t_ordert_order_item进行笛卡尔积汇总,这里笛卡尔积不会出现跨库,这个千万注意,得出整条SQL最终的路由结果集。

长按二维码识别关注