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