zoukankan      html  css  js  c++  java
  • 【源码解析】Sharding-Jdbc的执行过程(一)

    一、ShardingContext

    在Sharding-Jdbc中,我们其实需要抓住一个核心类,也就是ShardingContext,分片上下文,里面定义了下面几个内容:

    @RequiredArgsConstructor
    @Getter
    public final class ShardingContext {
        //分片规则
        private final ShardingRule shardingRule;
        //数据库类型
        private final DatabaseType databaseType;
        //执行引擎
        private final ExecutorEngine executorEngine;
        //是否要在log文件中展示sql语句
        private final boolean showSQL;
    }
    

    里面的几个参数很简单易懂,但是里面包含的内容也是整个中间件的核心内容。

    二、preparedStatement

    这块对应于代码中的jdbc/core/statement,里面包括两部分,一部分是statement,一部分是prepareStatement。考虑到性能问题(prepareStatement可以进行缓存)和代码的优雅性(变量的设置),以及考虑到通常JDBC使用的都是prepareStatement,所以我们着重看下prepareStatement的两部分,即MasterSlavePreparedStatement和ShardingPreparedStatement。

    2.1 MasterSlavePreparedStatement

    2.1.1 获取connection

    支持读写分离。这块可以先看一些他的构造方法,构造方法中一般都有这样的方法:

    connection.getConnections(sqlStatement.getType())
    

    这块其实是根据sql的类型,来获取不同的连接

    • 如果是DDL类型,会获取所有的连接,包括master和slave,也就是说对于表结构的修改,会修改包括master和slave
    • 如果是DML类型,也就是写操作,获取的是master的连接
    • 最后,如果是DQL类型,也就是读操作,是根据读写分离的策略获取某个slave连接

    后面两种是有本地缓存的,可以避免每次都进行connect构建,提高效率。

    当然,这个sqlType是怎么来的?这就需要用到sql解析的模块了。

    SQLStatement sqlStatement = new SQLJudgeEngine(sql).judge();
    

    这块先提一下,后续再详细讲解。

    2.1.2 执行sql

    主要有三个方法,

    • executeQuery
    public ResultSet executeQuery() throws SQLException {
        Preconditions.checkArgument(1 == routedStatements.size(), "Cannot support executeQuery for DDL");
        return routedStatements.iterator().next().executeQuery();
    }
    

    首先说明下,这里的routedStatements是待执行sql的一个集合,这块首先确保只有一个查询语句。然后直接调用PreparedStatement.executeQuery()方法,比较直观。

    • executeUpdate
    public int executeUpdate() throws SQLException {
        int result = 0;
        for (PreparedStatement each : routedStatements) {
            result += each.executeUpdate();
        }
        return result;
    }
    

    这块逻辑与上面一致,不过不一样的是,可以传入多个update的语句,然后顺序执行,最后调用的是PreparedStatement.executeUpdate()方法。

    • execute
    public boolean execute() throws SQLException {
        boolean result = false;
        for (PreparedStatement each : routedStatements) {
            result = each.execute();
        }
        return result;
    }
    

    最后就是一些其他的方法的执行了。

    总的来说,读写分离这块的内容比较简单,首先sql语句不要怎么变化,再者数据库连接connection基本上都是确定的,所以不需要路由什么的,可以直接运行。

    2.2 ShardingPreparedStatement

    下面我们看下分库分表的情况下,来执行我们的sql,这块就稍微有些复杂了。

    2.2.1 构造方法

    首先看一下他的构造方法,构造方法中有一些很奇怪的常量,比如TYPE_FORWARD_ONLY、CONCUR_READ_ONLY等等,这块可以参考这篇博客。其实就是在读写过程中的指针的方向等内容。

    2.2.2 sql执行

    2.2.2.1 查询

    查询的逻辑大概如下:

    • sql路由
    • sql改写
    • sql执行
    • 结果合并

    下面看下代码:

    public ResultSet executeQuery() throws SQLException {
        ResultSet result;
        try {
            Collection<PreparedStatementUnit> preparedStatementUnits = route();
            List<ResultSet> resultSets = new PreparedStatementExecutor(
                    getConnection().getShardingContext().getExecutorEngine(), routeResult.getSqlStatement().getType(), preparedStatementUnits, getParameters()).executeQuery();
            result = new ShardingResultSet(resultSets, new MergeEngine(resultSets, (SelectStatement) routeResult.getSqlStatement()).merge());
        } finally {
            clearBatch();
        }
        currentResultSet = result;
        return result;
    }
    

    首先我们需要看几个类:

    • PreparedStatementUnit
      • SQLExecutionUnit:包括dataSource和sql
      • PreparedStatement

    这个类,会用于最终我们到具体的数据库上执行sql,调用route方法,其实就是路由到具体的服务器上面。

    private Collection<PreparedStatementUnit> route() throws SQLException {
        Collection<PreparedStatementUnit> result = new LinkedList<>();
        routeResult = routingEngine.route(getParameters());
        for (SQLExecutionUnit each : routeResult.getExecutionUnits()) {
            SQLType sqlType = routeResult.getSqlStatement().getType();
            Collection<PreparedStatement> preparedStatements;
            if (SQLType.DDL == sqlType) {
                preparedStatements = generatePreparedStatementForDDL(each);
            } else {
                preparedStatements = Collections.singletonList(generatePreparedStatement(each));
            }
            routedStatements.addAll(preparedStatements);
            for (PreparedStatement preparedStatement : preparedStatements) {
                replaySetParameter(preparedStatement);
                result.add(new PreparedStatementUnit(each, preparedStatement));
            }
        }
        return result;
    }
    

    这里面有个routeResult,其实就是根据入参路由到的数据库列表。这边有一个路由引擎,看下这边是怎么路由的:

    public SQLRouteResult route(final List<Object> parameters) {
        if (null == sqlStatement) {
            sqlStatement = sqlRouter.parse(logicSQL, parameters.size());
        }
        return sqlRouter.route(logicSQL, parameters, sqlStatement);
    }
    

    这边有几个参数,

    • logicSQL:表示逻辑上的sql,因为业务使用时,写的sql其实是逻辑上的sql,用到的数据库和表也是逻辑库和逻辑表
    • parameters:传入的参数
    • sqlStatement:最终的sql语句

    首先解析sql,然后路由。这边的解析还是老套路,但是如果解析出来的sql是insert,而且配置了自动生成key,那么会调用自动生成key的方法,生成key,放到对应的column下。解析这块后续再分析。下面我们看下路由:

    @Override
    public SQLRouteResult route(final String logicSQL, final List<Object> parameters, final SQLStatement sqlStatement) {
        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, databaseType, sqlStatement);
        boolean isSingleRouting = routingResult.isSingleRouting();
        if (sqlStatement instanceof SelectStatement && null != ((SelectStatement) sqlStatement).getLimit()) {
            processLimit(parameters, (SelectStatement) sqlStatement, isSingleRouting);
        }
        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)));
            }
        }
        if (showSQL) {
            SQLLogger.logSQL(logicSQL, sqlStatement, result.getExecutionUnits(), parameters);
        }
        return result;
    }
    

    先看路由:

    private RoutingResult route(final List<Object> parameters, final SQLStatement sqlStatement) {
        Collection<String> tableNames = sqlStatement.getTables().getTableNames();
        RoutingEngine routingEngine;
        //不涉及到具体的表,全路由
        if (tableNames.isEmpty()) {
            routingEngine = new DatabaseAllRoutingEngine(shardingRule.getDataSourceMap());
        } else if (1 == tableNames.size() || shardingRule.isAllBindingTables(tableNames) || shardingRule.isAllInDefaultDataSource(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();
    }
    

    这里涉及到一个bindingTable的概念,概念如下:指在任何场景下分片规则均一致的主表和子表。例:订单表和订单项表,均按照订单ID分片,则此两张表互为BindingTable关系。BindingTable关系的多表关联查询不会出现笛卡尔积关联,关联查询效率将大大提升。

    这里最终返回的结果RoutingResult类似如下:
    image

    这里面有几种路由类,后续再详细分析。

    获取到数据库表的路由信息后,就到了sql改写的过程。这里也有一个sql改写的引擎:++SQLRewriteEngine++。中间插入了一个判断,routingResult.isSingleRouting(),判断是否路由之后的DB只有一个。这里有个小的优化:如果是select语句,用到了limit,而且最终落到了单片上,那么sql语句是不会被重写的,通过rewrite方法也可以看出来,是和isSingleRouting取反的。

    public SQLBuilder rewrite(final boolean isRewriteLimit) {
        SQLBuilder result = new SQLBuilder();
        if (sqlTokens.isEmpty()) {
            result.appendLiterals(originalSQL);
            return result;
        }
        int count = 0;
        sortByBeginPosition();
        for (SQLToken each : sqlTokens) {
            if (0 == count) {
                result.appendLiterals(originalSQL.substring(0, each.getBeginPosition()));
            }
            if (each instanceof TableToken) {
                appendTableToken(result, (TableToken) each, count, sqlTokens);
            } else if (each instanceof ItemsToken) {
                appendItemsToken(result, (ItemsToken) each, count, sqlTokens);
            } else if (each instanceof RowCountToken) {
                appendLimitRowCount(result, (RowCountToken) each, count, sqlTokens, isRewriteLimit);
            } else if (each instanceof OffsetToken) {
                appendLimitOffsetToken(result, (OffsetToken) each, count, sqlTokens, isRewriteLimit);
            } else if (each instanceof OrderByToken) {
                appendOrderByToken(result, count, sqlTokens);
            }
            count++;
        }
        return result;
    }
    

    执行完成这步之后,sql语句重写基本完成。如果路由结果涉及到笛卡尔积,还需要对sql进行进一步的重写,因为涉及到关联表。下面涉及到真正转化sql这部分,使用的方法是:rewriteEngine.generateSQL,涉及到sql中的逻辑库表替换为实际的库表,形成SQLExecutionUnit,添加到待执行的sql列表中,最终得到SQLRouteResult。至此,sql路由完成。回到ShardingPreparedStatement的route方法。遍历routeResult,形成预执行的statement集合。

    再回到ShardingPreparedStatement的executeQuery()方法,route()得到了Collection,下面就到了真正要执行sql的步骤。

  • 相关阅读:
    Java开发之富文本编辑器TinyMCE
    将博客搬至CSDN
    利用Docker搭建java项目开发环境
    Linux上传和下载之Xshell
    JSP中利用JSTL标签对日期格式化
    MySQL数据库localhost的root用户登陆遭遇失败
    CentOS7下 简单安装和配置Elasticsearch Kibana Filebeat 快速搭建集群日志收集平台(版本6.x)
    CentOS下递归遍历文件夹下所有文件,查找指定字符
    谷歌浏览器插件不让离线安装怎么办?
    X-Forwarded-For 会少记录一次代理服务器的IP
  • 原文地址:https://www.cnblogs.com/f-zhao/p/7877470.html
Copyright © 2011-2022 走看看