sharding-jdbc系列之SQL改写(五)

 

前文回顾

在SQL路由那一节,我们分析了SQL的路由过程,最终会根据路由算法,计算出来这个SQL最终会经过几个数据源,几张表。

以查询为例:

select * from t_user 

总共两个库,每个库两张表,

routeDataSources : 得到两个数据源,dataSource0 , dataSource1

routeTables : 以上两个数据源分别得到t_user00 , t_user01 ,

上面得到的数据最终得到一个Map集合

dataSource0
    t_user00 
    t_user01 
dataSource1
    t_user00 
    t_user01

上面的例子可以看到,select * from t_user 这个SQL最终会经历两个数据源,每个数据源的两张表, sharding-jdbc拿到这个结果之后,会进行SQL改写,改写成能够在数据库中执行的SQL。

准备工作

放开SQL显示,将改写之后的SQL打印在控制台,方便查看

@Bean(name="dataSource")
    public DataSource shardingDataSource(ShardingRule shardingRule) throws SQLException {
        Properties props = new Properties();
          // 将show_sql的配置设置为true
        props.put(ShardingPropertiesConstant.SQL_SHOW.getKey(),"true");
        return ShardingDataSourceFactory.createDataSource(shardingRule,props);
    }

源码深入

源码入口:com.dangdang.ddframe.rdb.sharding.routing.router.ParsingSQLRouter

@Override
    public SQLRouteResult route(final String logicSQL, final List<Object> parameters, final SQLStatement sqlStatement) {
          // SQL路由结果
        SQLRouteResult result = new SQLRouteResult(sqlStatement);
        if (sqlStatement instanceof InsertStatement && null != ((InsertStatement) sqlStatement).getGeneratedKey()) {
            processGeneratedKey(parameters, (InsertStatement) sqlStatement, result);
        }
          // SQL路由
        RoutingResult routingResult = route(parameters, sqlStatement);
          // 建立SQL改写引擎
        SQLRewriteEngine rewriteEngine = new SQLRewriteEngine(shardingRule, logicSQL, sqlStatement);
          // 是否是单表路由,设计到的表数量为1 的时候,该值为true
        boolean isSingleRouting = routingResult.isSingleRouting();
          // 为查询的SQL,对分页做加强
        if (sqlStatement instanceof SelectStatement && null != ((SelectStatement) sqlStatement).getLimit()) {
            processLimit(parameters, (SelectStatement) sqlStatement, isSingleRouting);
        }
          // 改写SQL
        SQLBuilder sqlBuilder = rewriteEngine.rewrite(!isSingleRouting);
       // 针对笛卡尔结果集做的额外处理。 
        if (routingResult instanceof CartesianRoutingResult) {
              // 循环笛卡尔结果的涉及到的数据源
            for (CartesianDataSource cartesianDataSource : ((CartesianRoutingResult) routingResult).getRoutingDataSources()) {
                  // 循环数据源中的每个表执行单元 
                for (CartesianTableReference cartesianTableReference : cartesianDataSource.
                     getRoutingTableReferences()) {
                      // 调用SQL改写引擎生成SQL。
                    result.getExecutionUnits().add(new SQLExecutionUnit(
                      cartesianDataSource.getDataSource(), rewriteEngine.generateSQL(
                        cartesianTableReference, sqlBuilder)));
                }
            }
        } else {
              // 简单路由结果,会直接在tableUnites里面返回表的执行单元,此处直接循环
            for (TableUnit each : routingResult.getTableUnits().getTableUnits()) {
                  //  生成SQL
                result.getExecutionUnits().add(new SQLExecutionUnit(
                  each.getDataSourceName(), rewriteEngine.generateSQL(each, sqlBuilder)));
            }
        }
        if (showSQL) {
              // 打印结果。
            SQLLogger.logSQL(logicSQL, sqlStatement, result.getExecutionUnits(), parameters);
        }
        return result;
    }

简单路由改写

new SQLExecutionUnit(each.getDataSourceName(), rewriteEngine.generateSQL(each, sqlBuilder))

简单路由就是上面那一行代码,通过rewriteEngine.generateSQL生成SQL,构建一个SQLExecutionUnit执行单元。

public String generateSQL(final TableUnit tableUnit, final SQLBuilder sqlBuilder) {

        return sqlBuilder.toSQL(getTableTokens(tableUnit));
}

获取tableToken,同时生成SQL , getTableTokens为了获取当前表和绑定表的真实表

public String toSQL(final Map<String, String> tableTokens) {
        StringBuilder result = new StringBuilder();
        for (Object each : segments) {
            if (each instanceof TableToken && tableTokens.containsKey(((TableToken) each).tableName)) {
                result.append(tableTokens.get(((TableToken) each).tableName));
            } else {
                result.append(each);
            }
        }
        return result.toString();
    }

将逻辑表名替换为真实表,同时组装真实的SQL

举例说明

配置如下

@Bean
    public ShardingRule shardingRule(DataSourceRule dataSourceRule){
        //具体分库分表策略
        TableRule userTableRule = TableRule.builder("t_user")
                .actualTables(Arrays.asList( "t_user_00","t_user_01"))
                .tableShardingStrategy(new TableShardingStrategy("user_id", new ModuloTableShardingAlgorithm()))
                .dataSourceRule(dataSourceRule)
                .build();
        TableRule stuTableRule = TableRule.builder("t_stu")
                .actualTables(Arrays.asList("t_stu_0","t_stu_1"))
                .tableShardingStrategy(new TableShardingStrategy("id", new TestTableShardingAlgorithm()))
                .dataSourceRule(dataSourceRule)
                .build();

        //绑定表策略,在查询时会使用主表策略计算路由的数据源,因此需要约定绑定表策略的表的规则需要一致,可以一定程度提高效率
        List<BindingTableRule> bindingTableRules = new ArrayList<BindingTableRule>();
        bindingTableRules.add(new BindingTableRule(Arrays.asList(userTableRule,stuTableRule)));
        return ShardingRule.builder()
                .dataSourceRule(dataSourceRule)
                .tableRules(Arrays.asList(userTableRule,stuTableRule))
                .bindingTableRules(bindingTableRules)
                .databaseShardingStrategy(new DatabaseShardingStrategy("id", new ModuloDatabaseShardingAlgorithm()))
                .tableShardingStrategy(new TableShardingStrategy("user_id", new ModuloTableShardingAlgorithm()))
                .build();
    }

主要看上面的userTableRule 和 stuTableRule 这两个分表规则,二则互为绑定表。

SQL如下:

<select id="selectUser" resultType="com.sharding.entity.User">
        select u.* from t_user u
        left join t_stu st on u.id = st.id
        where u.id in
        <foreach collection="ids" item="id" open="(" separator="," close=")">
            #{id}
        </foreach>
        order by u.id  limit 0,10
</select>

查询t_user , 同时left join t_stu这张表

通过路由得到路由结果:

tableUnits 
  0 dataSourceName : "dataSource0"
    logicTableName : "t_stu"
    actualTableName: "t_stu_1"
  1 dataSourceName : "dataSource1"
    logicTableName : "t_stu"
    actualTableName: "t_stu_1"
  3 dataSourceName : "dataSource1"
    logicTableName : "t_stu"
    actualTableName: "t_stu_0"
  4 dataSourceName : "dataSource0"
    logicTableName : "t_stu"
    actualTableName: "t_stu_1"

由于t_stu和t_user 二者是绑定表关系,所以。 上面的SQL,sharding-jdbc只会路由一张表就好了, 得到t_stu的路由结果,接下来就是SQL改写了 , 总共得到4个执行单元,循环执行单元,改写SQL。

//循环执行单元,构建SQL
for (TableUnit each : routingResult.getTableUnits().getTableUnits()) {
                result.getExecutionUnits().add(new SQLExecutionUnit(each.getDataSourceName(), rewriteEngine.generateSQL(each, sqlBuilder)));
            }

调用rewriteEngine.generateSQL , 这个方法上面已经讲过,现在重点就是讲解他里面的getTableTokens方法

private Map<String, String> getTableTokens(final TableUnit tableUnit) {
        Map<String, String> tableTokens = new HashMap<>();
          // 获取table的逻辑表和真实表的映射
        tableTokens.put(tableUnit.getLogicTableName(), tableUnit.getActualTableName());
          // 根据当前的逻辑表名,获取当前表的绑定表信息。
        Optional<BindingTableRule> bindingTableRule = shardingRule.findBindingTableRule(tableUnit.getLogicTableName());
        if (bindingTableRule.isPresent()) {
              // 获取绑定表的token。
            tableTokens.putAll(getBindingTableTokens(tableUnit, bindingTableRule.get()));
        }
        return tableTokens;
}

步骤说明:

1.获取当前表的逻辑表和真实表的映射关系

2.根据当前表的逻辑表名,获取其绑定表信息

3.判断绑定表信息是否存在,不存在直接返回当前的

4.绑定表信息存在,获取绑定表信息里面的tableToken

参数说明:

tableUnit

dataSourceName : "dataSource0"
logicTableName : "t_stu"
actualTableName: "t_stu_1"

获取绑定表的信息。

/**
* tableUnit 简单路由的表信息
* bindingTableRule  当前表的绑定表信息
*/
private Map<String, String> getBindingTableTokens(final TableUnit tableUnit, final BindingTableRule bindingTableRule) {
        Map<String, String> result = new HashMap<>();
          // 循环当前SQL中所有涉及到的table,这里的tablename都属于逻辑表。在SQL解析中解析出来的。
        for (String eachTable : sqlStatement.getTables().getTableNames()) {
              // 不属于当前表的, 并且在绑定表信息中的。
            if (!eachTable.equalsIgnoreCase(tableUnit.getLogicTableName()) && bindingTableRule.hasLogicTable(eachTable)) {
                  // 将逻辑表为key, 同时根据tableUnit的dataSource(数据源) , actualTableName(真实表名) , 和当前得到的绑定表的逻辑表名,获取当前
                  // 逻辑表的真是表名称
                result.put(eachTable, bindingTableRule.getBindingActualTable(tableUnit.getDataSourceName(), eachTable,   tableUnit.getActualTableName()));
            }
        }
        return result;
}

getBindingActualTable ,

/**
     * 
     * 
     * @param dataSource 当前的数据源名称
     * @param logicTable  当前表的逻辑表名
     * @param otherActualTable 由上文可知,此处放的是简单路由里面的表的真实表名称
     * @return actual table name
     */
    public String getBindingActualTable(final String dataSource, final String logicTable, final String otherActualTable) {
        int index = -1;
        for (TableRule each : tableRules) {
            if (each.isDynamic()) {
                throw new UnsupportedOperationException("Dynamic table cannot support Binding table.");
            }
            // 根据当前数据源,otherActualTable 获取otherActualTable在表里面的位置,获取到index 坐标
            index = each.findActualTableIndex(dataSource, otherActualTable);
            if (-1 != index) {
                break;
            }
        }
        // 
        Preconditions.checkState(-1 != index, String.format("Actual table [%s].[%s] is not in table config", dataSource, otherActualTable));
          // 根据otherActualTable获取到的坐标,得到logicTable的真实表名称
        for (TableRule each : tableRules) {
            if (each.getLogicTable().equalsIgnoreCase(logicTable)) {
                return each.getActualTables().get(index).getTableName();
            }
        }
        throw new IllegalStateException(String.format("Cannot find binding actual table, data source: %s, logic table: %s, other actual table: %s", dataSource, logicTable, otherActualTable));
    }

步骤说明:

1.根据otherActualTable 获取otherActualTable在表里面的位置,获取到index 坐标

2.根据这个坐标,找到logicTable对应的真实表信息。

总结:

每个TableRule中都维护了数据源-真实表的一个集合, 所以,每个数据源-真实表在ArrayList中都有一个位置,初始化的代码如下

private List<DataNode> generateDataNodes(final List<String> actualTables, final DataSourceRule dataSourceRule, final Collection<String> actualDataSourceNames) {
        Collection<String> dataSourceNames = getDataSourceNames(dataSourceRule, actualDataSourceNames);
          // 数据节点集合
        List<DataNode> result = new ArrayList<>(actualTables.size() * (dataSourceNames.isEmpty() ? 1 : dataSourceNames.size()));
        for (String actualTable : actualTables) {
              // 判断真实表中,是否存在 “.”号,如果存在这个,说明可能是“datasource.table” 这种方式,所以不需要额外设置数据源
            if (DataNode.isValidDataNode(actualTable)) {
                result.add(new DataNode(actualTable));
            } else {
                  // 循环数据源
                for (String dataSourceName : dataSourceNames) {
                      //添加数据节点。
                    result.add(new DataNode(dataSourceName, actualTable));
                }
            }
        }
        return result;
    }

因此,在SQL改写的过程中,如果SQL中有设计其他的表,并且这个表跟它自身互为绑定表,那么 会通过当前表在ArrayList的坐标,找到另外的表的真实表。

绑定表这种机制,一般用于两张表的路由规则一致 , 并且设置的时候顺序也要一致

举例1:

//具体分库分表策略
        TableRule userTableRule = TableRule.builder("t_user")
                .actualTables(Arrays.asList( "t_user_00","t_user_01"))
                .tableShardingStrategy(new TableShardingStrategy("user_id", new ModuloTableShardingAlgorithm()))
                .dataSourceRule(dataSourceRule)
                .build();
        TableRule stuTableRule = TableRule.builder("t_stu")
                .actualTables(Arrays.asList("t_stu_0","t_stu_1"))
                .tableShardingStrategy(new TableShardingStrategy("id", new TestTableShardingAlgorithm()))
                .dataSourceRule(dataSourceRule)
                .build();

Arrays.asList( "t_user_00","t_user_01") 这个ArrayList里面的元素的顺序要对应

t_stu_0   对应  t_user00
t_stu_1   对应  t_user01

举例2:

//具体分库分表策略
        TableRule userTableRule = TableRule.builder("t_user")
                .actualTables(Arrays.asList( "t_user_01","t_user_00"))
                .tableShardingStrategy(new TableShardingStrategy("user_id", new ModuloTableShardingAlgorithm()))
                .dataSourceRule(dataSourceRule)
                .build();
        TableRule stuTableRule = TableRule.builder("t_stu")
                .actualTables(Arrays.asList("t_stu_0","t_stu_1"))
                .tableShardingStrategy(new TableShardingStrategy("id", new TestTableShardingAlgorithm()))
                .dataSourceRule(dataSourceRule)
                .build();

对应关系如下

t_stu_0   对应  t_user01
t_stu_1   对应  t_user00

打印的SQL如下:

2018-07-31 16:23:26.535  INFO 12776 --- [nio-7001-exec-1] Sharding-JDBC-SQL                        : Actual SQL: dataSource0 ::: select u.* , u.id AS ORDER_BY_DERIVED_0 from t_user_00 u
        left join t_stu_0 st on u.id = st.id
        where u.id in
         (  ? ) 
        order by u.id  limit 0,10 ::: [1]
2018-07-31 16:23:26.535  INFO 12776 --- [nio-7001-exec-1] Sharding-JDBC-SQL                        : Actual SQL: dataSource0 ::: select u.* , u.id AS ORDER_BY_DERIVED_0 from t_user_01 u
        left join t_stu_1 st on u.id = st.id
        where u.id in (  ? ) 
        order by u.id  limit 0,10 ::: [1]
2018-07-31 16:23:26.535  INFO 12776 --- [nio-7001-exec-1] Sharding-JDBC-SQL                        : Actual SQL: dataSource1 ::: select u.* , u.id AS ORDER_BY_DERIVED_0 from t_user_00 u
        left join t_stu_0 st on u.id = st.id
        where u.id in
         (  ? ) 
        order by u.id  limit 0,10 ::: [1]
2018-07-31 16:23:26.535  INFO 12776 --- [nio-7001-exec-1] Sharding-JDBC-SQL                        : Actual SQL: dataSource1 ::: select u.* , u.id AS ORDER_BY_DERIVED_0 from t_user_01 u
        left join t_stu_1 st on u.id = st.id
        where u.id in
         ( ?) 
        order by u.id  limit 0,10 ::: [1]

此处仅贴出了两个执行SQL, 查询的SQL,limit分页默认是按照正常的每页,从每张表里面按照这个分页数量查询出来,然后通过结果合并,最终再去前面的10条,这个后面的结果归并的时候再讲。

SQL改写分为两部分,一部分是将分表的逻辑表名称替换为真实表名称。

另一部分是根据SQL解析结果替换一些在分片环境中不正确的功能。这里具两个例子:

  1. 第1个例子是avg计算。在分片的环境中,以avg1 +avg2+avg3/3计算平均值并不正确,需要改写为(sum1+sum2+sum3)/(count1+count2+ count3)。这就需要将包含avg的SQL改写为sum和count,然后再结果归并时重新计算平均值。
  2. 第2个例子是分页。假设每10条数据为一页,取第2页数据。在分片环境下获取limit 10, 10,归并之后再根据排序条件取出前10条数据是不正确的结果。正确的做法是将分条件改写为limit 0, 20,取出所有前2页数据,再结合排序条件算出正确的数据。可以看到越是靠后的Limit分页效率就会越低,也越浪费内存。有很多方法可避免使用limit进行分页,比如构建记录行记录数和行偏移量的二级索引,或使用上次分页数据结尾ID作为下次查询条件的分页方式。

猜你喜欢

转载自blog.csdn.net/u012394095/article/details/81476290