sharding-jdbc系列之SQL路由(三)

前言

本文基于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这句代码,把两张表绑定在一起,接着重新来看下解析的流程:

  1. 通过sql解析发现有两张逻辑表名称:t_order 和 t_order_item。

  2. 路由
    在真正路由之前,引擎会做一个判断,如果解析出来的所有逻辑表都在一个绑定规则中,那么取出一张逻辑表,然后走单表的路由,在这里,两张逻辑表绑定在一个规则中,也就是所谓的全绑定,这时候,引擎使用其中一张表走单表路由逻辑。

    此示例用t_order单表路由解析出如下结果:
    ds_jdbc_0, t_order, t_order_1

    那么问题来了,怎么处理逻辑表t_order_item的物理映射呢?

  3. 路由重写
    在路由完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表的关联查询需要拆解为笛卡尔积组合执行。查询性能较低,而且数据库连接数较高,需谨慎使用。

sharedCode源码交流群,欢迎喜欢阅读源码的朋友加群,添加下面的微信, 备注”加群“ 。 

展开阅读全文

没有更多推荐了,返回首页