前言
本文基于sharding-jdbc1.5.4 , mybatis1.3.0
代码入口
源码入口: com.dangdang.ddframe.rdb.sharding.jdbc.core.statement.ShardingPreparedStatement
该类实现了PreparedStatement接口 。
在mybatis执行SQL的时候,会调用PreparedStatement的execute() 方法
@Override
public boolean execute() throws SQLException {
try {
// SQL路由
Collection<PreparedStatementUnit> preparedStatementUnits = route();
// SQL执行
return new PreparedStatementExecutor(
getConnection().getShardingContext().getExecutorEngine(), routeResult.getSqlStatement().getType(), preparedStatementUnits, getParameters()).execute();
} finally {
clearBatch();
}
}
由上可知, route()方法是SQL路由的核心方法,接下来主要看这个方法,至于SQL执行,则放在下一篇文章写。
private Collection<PreparedStatementUnit> route() throws SQLException {
// SQL路由的结果集合
Collection<PreparedStatementUnit> result = new LinkedList<>();
// getParameters() 获取 SQL中的参数, 用过PreparedStatement的都知道
// 执行路由
routeResult = routingEngine.route(getParameters());
//.... 代码省略
return result;
}
路由代码的主要逻辑在这一句
routeResult = routingEngine.route(getParameters());
源码入口 : com.dangdang.ddframe.rdb.sharding.routing.PreparedStatementRoutingEngine
public SQLRouteResult route(final List<Object> parameters) {
// sqlStatement 等于空,则解析SQL
if (null == sqlStatement) {
// SQL解析
sqlStatement = sqlRouter.parse(logicSQL, parameters.size());
}
// SQL路由
return sqlRouter.route(logicSQL, parameters, sqlStatement);
}
LogicSQL : 我们需要执行的SQL , 就是写在mybatis xml中的SQL
关于SQL解析,可以看我上一篇文章。
sqlRouter.route(logicSQL, parameters, sqlStatement) , 该方法的实现如下
@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);
}
// 获取路由结果,该方法后面会继续往下讲
RoutingResult routingResult = route(parameters, sqlStatement);
// 构建SQL改写引擎
SQLRewriteEngine rewriteEngine = new SQLRewriteEngine(shardingRule, logicSQL, sqlStatement);
boolean isSingleRouting = routingResult.isSingleRouting();
// 对分页查询额外做处理
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()) {
result.getExecutionUnits().add(new SQLExecutionUnit(cartesianDataSource.getDataSource(), rewriteEngine.generateSQL(cartesianTableReference, sqlBuilder)));
}
}
} else {
// 其他结果处理
for (TableUnit each : routingResult.getTableUnits().getTableUnits()) {
result.getExecutionUnits().add(new SQLExecutionUnit(each.getDataSourceName(), rewriteEngine.generateSQL(each, sqlBuilder)));
}
}
// 是否显示SQL,显示则打印SQL
if (showSQL) {
SQLLogger.logSQL(logicSQL, sqlStatement, result.getExecutionUnits(), parameters);
}
// 返回结果
return result;
}
本文主要是讲SQL如何进行路由的,至于SQL改写,笛卡尔积结果处理, 后续会单独开文章进行
RoutingResult routingResult = route(parameters, sqlStatement);
实现如下
private RoutingResult route(final List<Object> parameters, final SQLStatement sqlStatement) {
// 获取本次SQL执行的过程中 涉及到的table
Collection<String> tableNames = sqlStatement.getTables().getTableNames();
RoutingEngine routingEngine;
// 表的数量等于1 ,或者是绑定表路由,则选用SimpleRoutingEngine ,
// 否则选用ComplexRoutingEngine
if (1 == tableNames.size() || shardingRule.isAllBindingTables(tableNames)) {
routingEngine = new SimpleRoutingEngine(shardingRule, parameters, tableNames.iterator().next(), sqlStatement);
} else {
// TODO config for cartesian set
routingEngine = new ComplexRoutingEngine(shardingRule, parameters, tableNames, sqlStatement);
}
return routingEngine.route();
}
SQL路由
本文的重点来了,这里有四种路由方式。
SimpleRoutingEngine
当SQL执行的过程中,仅涉及一张逻辑表的时候 或 绑定表路由,则使用这个路由引擎
@Override
public RoutingResult route() {
// 通过逻辑表名,找到分表规则 ,如果找不到则使用默认的数据源。
TableRule tableRule = shardingRule.getTableRule(logicTableName);
// 通过tableRule找到分库规则,默认以分表规则中配置的分库规则为准,如果找不到,则使用全局的分库规则
// 如果全局的分库规则也不存在,则会使用NoneDatabaseShardingAlgorithm 这个规则。
Collection<String> routedDataSources = routeDataSources(tableRule);
Map<String, Collection<String>> routedMap = new LinkedHashMap<>(routedDataSources.size());
for (String each : routedDataSources) {
// 数据源为键, 分表策略的结果为value
routedMap.put(each, routeTables(tableRule, each));
}
//
return generateRoutingResult(tableRule, routedMap);
}
shardingRule.getTableRule(logicTableName) ,通过逻辑表去找分表规则,如果找不到分表规则, 则判断是否存在默认的数据源,
如果默认的数据源也不存在,则报错
public TableRule getTableRule(final String logicTableName) {
// 通过逻辑表找分表规则
Optional<TableRule> tableRule = tryFindTableRule(logicTableName);
if (tableRule.isPresent()) {
// 找到了,返回
return tableRule.get();
}
// 没找到分表规则,判断默认的数据源是否存在,
if (dataSourceRule.getDefaultDataSource().isPresent()) {
return createTableRuleWithDefaultDataSource(logicTableName, dataSourceRule);
}
// 默认的数据源不存在,则报异常
throw new ShardingJdbcException("Cannot find table rule and default data source with logic table: '%s'", logicTableName);
}
routeDataSources和routeTables 这两个方法中,调用了分片策略,通过分片算法,得到最终的分片结果。
private Collection<String> routeDataSources(final TableRule tableRule) {
// 获取分库策略
DatabaseShardingStrategy strategy = shardingRule.getDatabaseShardingStrategy(tableRule);
// 判断是否需要强制路由,根据分片键,从SqlStatement中获取分片值
List<ShardingValue<?>> shardingValues = HintManagerHolder.isUseShardingHint() ? getDatabaseShardingValuesFromHint(strategy.getShardingColumns())
: getShardingValues(strategy.getShardingColumns());
// 静态分片算法。 内部调用了,我们自定义的分库策略的方法。
Collection<String> result = strategy.doStaticSharding(tableRule.getActualDatasourceNames(), shardingValues);
Preconditions.checkState(!result.isEmpty(), "no database route info");
return result;
}
private Collection<String> routeTables(final TableRule tableRule, final String routedDataSource) {
// 获取分表策略
TableShardingStrategy strategy = shardingRule.getTableShardingStrategy(tableRule);
// 判断是否需要强制路由,通过分片键,从SqlStatement中获取分片值
List<ShardingValue<?>> shardingValues = HintManagerHolder.isUseShardingHint() ? getTableShardingValuesFromHint(strategy.getShardingColumns())
: getShardingValues(strategy.getShardingColumns());
// tableRule.isDynamic() 判断是动态分片,还是静态分片,调用不同的分片算法,后面会单独开文章讲
Collection<String> result = tableRule.isDynamic() ? strategy.doDynamicSharding(shardingValues) : strategy.doStaticSharding(tableRule.getActualTableNames(routedDataSource), shardingValues);
Preconditions.checkState(!result.isEmpty(), "no table route info");
return result;
}
说明:
HintManagerHolder.isUseShardingHint()这个表示是否走强制路由,如果走强制路由的话,则直接取自定义的强制路由的字段进行分片
generateRoutingResult 将分库分表得到的数据源,组装成返回的result数据。
private RoutingResult generateRoutingResult(final TableRule tableRule, final Map<String, Collection<String>> routedMap) {
RoutingResult result = new RoutingResult();
for (Entry<String, Collection<String>> entry : routedMap.entrySet()) {
Collection<DataNode> dataNodes = tableRule.getActualDataNodes(entry.getKey(), entry.getValue());
for (DataNode each : dataNodes) {
result.getTableUnits().getTableUnits().add(new TableUnit(each.getDataSourceName(), logicTableName, each.getTableName()));
}
}
return result;
}
以查询为例:
select * from t_user
总共两个库,每个库两张表,
routeDataSources : 得到两个数据源,dataSource0 , dataSource1
routeTables : 以上两个数据源分别得到t_user00 , t_user01 ,
上面得到的数据最终得到一个Map集合
dataSource0
t_user00
t_user01
dataSource1
t_user00
t_user01
generateRoutingResult 就是把map数据结构转化成result , 转换成一个个的表执行单元,就是最终我们知道在那个库,哪张表执行SQL了。
{
"singleRouting": false,
"tableUnits": {
"dataSourceNames": ["dataSource0", "dataSource1"],
"tableUnits": [{
"actualTableName": "t_user00",
"logicTableName": "t_user",
"dataSourceName": "dataSource0"
}, {
"actualTableName": "t_user01",
"logicTableName": "t_user",
"dataSourceName": "dataSource0"
}, {
"actualTableName": "t_user00",
"logicTableName": "t_user",
"dataSourceName": "dataSource1"
}, {
"actualTableName": "t_user01",
"logicTableName": "t_user",
"dataSourceName": "dataSource1"
}]
}
}
ComplexRoutingEngine
复杂路由引擎,当执行的SQL里面包含多个表时,会用到这个路由引擎,通常用于表的关联查询。
@Override
public RoutingResult route() {
// 创建结果集合, 以实际表的数量为大小。
Collection<RoutingResult> result = new ArrayList<>(logicTables.size());
Collection<String> bindingTableNames = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
// 循环表集合
for (String each : logicTables) {
// 通过表名,查询是否存在分表规则。
Optional<TableRule> tableRule = shardingRule.tryFindTableRule(each);
if (tableRule.isPresent()) {
// 存在分表规则
if (!bindingTableNames.contains(each)) {
// 调用简单路由引擎,过去该表的路由结果。
result.add(new SimpleRoutingEngine(shardingRule, parameters, tableRule.get().getLogicTable(), sqlStatement).route());
}
// 获取绑定表分表规则
Optional<BindingTableRule> bindingTableRule = shardingRule.findBindingTableRule(each);
if (bindingTableRule.isPresent()) {
bindingTableNames.addAll(Lists.transform(bindingTableRule.get().getTableRules(), new Function<TableRule, String>() {
@Override
public String apply(final TableRule input) {
return input.getLogicTable();
}
}));
}
}
}
log.trace("mixed tables sharding result: {}", result);
if (result.isEmpty()) {
throw new ShardingJdbcException("Cannot find table rule and default data source with logic tables: '%s'", logicTables);
}
if (1 == result.size()) {
// 如果结果的大小为1 ,则表明仅涉及一张 逻辑表(即分表了的),直接返回结果
return result.iterator().next();
}
// 结果大于1 ,则需要继续路由,使用笛卡尔积路由引擎
return new CartesianRoutingEngine(result).route();
}
步骤说明:
1.循环SQL中涉及到的所有表
2.判断表中是否存在分表规则。
3.存在,则调用简单路由引擎获取路由结果,放入结果集合
4.判断结果大小是否为1 ,如果为1 ,则直接返回结果
5.如果大于1 ,则继续笛卡尔积路由引擎获取路由结果。
总结:
复杂路由器,主要的工作就是分析SQL中涉及到的表,到底有几张表是涉及分库分表策略的,如果涉及到多个表,那么需要调用
笛卡尔路由引擎去做,如果只有一个,直接调用简单路由器,返回结果。
CartesianRoutingEngine
笛卡尔路由引擎,当涉及多个分库分表的表时,需要用到引擎
笛卡尔积是什么?
简单的说就是两个集合相乘的结果。
集合A{a1,a2,a3} 集合B{b1,b2}
他们的 笛卡尔积 是 A*B ={(a1,b1),(a1,b2),(a2,b1),(a2,b2),(a3,b1),(a3,b2)}
主要是积算出,有多少中路由方式,在多表关联查询的时候。
@Override
public CartesianRoutingResult route() {
CartesianRoutingResult result = new CartesianRoutingResult();
// 获取多表查询中的数据源集合,并且循环。
for (Entry<String, Set<String>> entry : getDataSourceLogicTablesMap().entrySet()) {
// 获取单个数据源中真实表集合
List<Set<String>> actualTableGroups = getActualTableGroups(entry.getKey(), entry.getValue());
// 获取单个数据源中 最小表的执行单位。
List<Set<TableUnit>> tableUnitGroups = toTableUnitGroups(entry.getKey(), actualTableGroups);
// 结果合并, 调用Sets.cartesianProduct 进行笛卡尔积计算。
result.merge(entry.getKey(), getCartesianTableReferences(Sets.cartesianProduct(tableUnitGroups)));
}
log.trace("cartesian tables sharding result: {}", result);
// 返回结果。
return result;
}
路由结果:
说明:
由上图可见,第一个数据源就有四个执行步骤,加上第二个执行步骤,也就说最小执行单元有8个,一个多表查询语句,
最终拆成了8个执行单元,如果在实际生产环境,真实表的数量很多,那么通过笛卡尔积最终得到的执行单位,将会非常
影响性能,因此,笔者在此建议大家,在分库分表的场景下,尽可能避免使用多表联合查询,如果要用多表联合查询,那么
最好可以使用绑定表路由。
绑定表路由
本段摘自:https://blog.csdn.net/yanyan19880509/article/details/78108468
先来看看业务场景:订单记录t_order为一个大的订单,像购物车购买这种一下可以买多种商品的话,最终一般会进行拆单,拆成多条t_order_item记录,如果我们能够确保这两个表的路由规则是完全一样的话,实际上是可以避免完全的笛卡尔积的。
比如在每个数据源中,都有如下的物理表:
t_order_0,t_order_1,t_order_item_0,t_order_item_1
当一个订单id映射到 t_order_0的时候,业务能够确保其对应的t_order_item一定映射到t_order_item_0,这种情况下,t_order_0永远没有必要与t_order_item_1之类的物理表进行联合查询,BindingTable就是用于配置这种情况的。
首先要配置需要绑定在一起的表,如下代码所示:
ShardingRule shardingRule = ShardingRule.builder().dataSourceRule(dataSourceRule).tableRules(Arrays.asList(orderTableRule,orderItemTableRule))
.bindingTableRules(Collections.singletonList(new BindingTableRule(Arrays.asList(orderTableRule, orderItemTableRule))))
.databaseShardingStrategy(new DatabaseShardingStrategy("user_id", new ModuloDatabaseShardingAlgorithm()))
.tableShardingStrategy(new TableShardingStrategy("order_id", new ModuloTableShardingAlgorithm())).build();1234
我们新增了BindingTableRule这句代码,把两张表绑定在一起,接着重新来看下解析的流程:
-
通过sql解析发现有两张逻辑表名称:t_order 和 t_order_item。
-
路由
在真正路由之前,引擎会做一个判断,如果解析出来的所有逻辑表都在一个绑定规则中,那么取出一张逻辑表,然后走单表的路由,在这里,两张逻辑表绑定在一个规则中,也就是所谓的全绑定,这时候,引擎使用其中一张表走单表路由逻辑。此示例用t_order单表路由解析出如下结果:
ds_jdbc_0, t_order, t_order_1那么问题来了,怎么处理逻辑表t_order_item的物理映射呢?
-
路由重写
在路由完t_order之后,引擎会进入sql重写阶段,要把sql中的t_order和t_order_item替换为物理表名,前面我们知道,我们找到了t_order_1,在重写的时候,引擎会用t_order去查找绑定规则,当找到了之后,计算t_order_1在所有物理表[t_order_0,t_order_1]中的位置索引,然后根据索引在物理表中[t_order_item0,t_order_item1]找到t_order_item的物理名表,这样便可以替换所有的逻辑表了。
总结
SQL路由是根据分片规则配置,将SQL定位至真正的数据源。主要分为单表路由、Binding表路由和笛卡尔积路由。
单表路由最为简单,但路由结果不一定落入唯一库(表),因为支持根据between和in这样的操作符进行分片,所以最终结果仍然可能落入多个库(表)。
Binding表可理解为分库分表规则完全一致的主从表。举例说明:订单表和订单详情表都根据订单ID作为分片键,任意时刻分片逻辑均相同。这样的关联查询和单表查询难度和性能相当。
笛卡尔积查询最为复杂,因为无法根据Binding关系定位分片规则的一致性,所以非Binding表的关联查询需要拆解为笛卡尔积组合执行。查询性能较低,而且数据库连接数较高,需谨慎使用。