learn-tech/专栏/ShardingSphere核心原理精讲-完/17路由引擎:如何理解分片路由核心类ShardingRouter的运作机制?.md
2024-10-16 06:37:41 +08:00

28 KiB
Raw Blame History

                        因收到Google相关通知网站将会择期关闭。相关通知内容
                        
                        
                        17  路由引擎:如何理解分片路由核心类 ShardingRouter 的运作机制?
                        前面我们花了几个课时对 ShardingSphere 中的 SQL 解析引擎做了介绍,我们明白 SQL 解析的作用就是根据输入的 SQL 语句生成一个 SQLStatement 对象。

从今天开始,我们将进入 ShardingSphere 的路由Routing引擎部分的源码解析。从流程上讲路由引擎是整个分片引擎执行流程中的第二步即基于 SQL 解析引擎所生成的 SQLStatement通过解析执行过程中所携带的上下文信息来获取匹配数据库和表的分片策略并生成路由结果。

分层:路由引擎整体架构

与介绍 SQL 解析引擎时一样,我们通过翻阅 ShardingSphere 源码,首先梳理了如下所示的包结构:

上述包图总结了与路由机制相关的各个核心类,我们可以看到整体呈一种对称结构,即根据是 PreparedStatement 还是普通 Statement 分成两个分支流程。

同时,我们也可以把这张图中的类按照其所属的包结构分成两个层次:位于底层的 sharding-core-route 和位于上层的 sharding-core-entry这也是 ShardingSphere 中所普遍采用的一种分包原则,即根据类的所属层级来组织包结构。关于 ShardingSphere 的分包原则我们在 [《12 | 从应用到原理:如何高效阅读 ShardingSphere 源码?》]中也已经进行了介绍,接下来我们具体分析这一原则在路由引擎中的应用。

1.sharding-core-route 工程

我们先来看图中的 ShardingRouter 类该类是整个路由流程的启动点。ShardingRouter 类直接依赖于解析引擎 SQLParseEngine 类完成 SQL 解析并获取 SQLStatement 对象,然后供 PreparedStatementRoutingEngine 和 StatementRoutingEngine 进行使用。注意到这几个类都位于 sharding-core-route 工程中,处于底层组件。

2.sharding-core-entry 工程

另一方面,上图中的 PreparedQueryShardingEngine 和 SimpleQueryShardingEngine 则位于 sharding-core-entry 工程中。从包的命名上看entry 相当于是访问的入口所以我们可以判断这个工程中所提供的类属于面向应用层组件处于更加上层的位置。PreparedQueryShardingEngine 和 SimpleQueryShardingEngine 的使用者分别是 ShardingPreparedStatement 和 ShardingStatement。这两个类再往上就是 ShardingConnection 以及 ShardingDataSource 这些直接面向应用层的类了。

路由核心类ShardingRouter

通过以上分析,我们对路由引擎的整体结构有了一个初步的认识。对于采用分层结构的执行流程而言,有两种解析思路,即自上而下或自下而上。今天,我们的思路是从底层出发逐层往上分析流程的链路,先来看路由引擎中最底层的对象 ShardingRouter变量定义如下

private final ShardingRule shardingRule; private final ShardingSphereMetaData metaData; private final SQLParseEngine parseEngine;

在 ShardingRouter 中,我们首先看到了熟悉的 SQL 解析引擎 SQLParseEngine 以及它的使用方法:

public SQLStatement parse(final String logicSQL, final boolean useCache) { return parseEngine.parse(logicSQL, useCache); }

上述代码非常简单,即通过 SQLParseEngine 对传入的 SQL 进行解析返回一个 SQLStatement 对象。这里将 SQL 命名为 logicSQL以便区别在分片和读写分离情况下的真实 SQL。

接下来我们来看一下 ShardingRule请注意这是一个基础类代表着分片的各种规则信息。ShardingRule 类位于 sharding-core-common 工程中,主要保存着与分片相关的各种规则信息,以及 ShardingKeyGenerator 等分布式主键的创建过程,各个变量定义以及对应的注释如下所示:

//分片规则配置类,封装各种配置项信息 private final ShardingRuleConfiguration ruleConfiguration; //DataSource 名称列表 private final ShardingDataSourceNames shardingDataSourceNames; //针对表的规则列表 private final Collection tableRules; //针对绑定表的规则列表 private final Collection bindingTableRules; //广播表名称列表 private final Collection broadcastTables; //默认的数据库分片策略 private final ShardingStrategy defaultDatabaseShardingStrategy; //默认的数据表分片策略 private final ShardingStrategy defaultTableShardingStrategy; //默认的分片键生成器 private final ShardingKeyGenerator defaultShardingKeyGenerator; //针对读写分离的规则列表 private final Collection masterSlaveRules; //加密规则 private final EncryptRule encryptRule;

ShardingRule 的内容非常丰富但其定位更多是提供规则信息而不属于核心流程因此我们先不对其做详细展开。作为基础规则类ShardingRule 会贯穿整个分片流程,在后续讲解过程中我们会穿插对它的介绍,这里先对上述变量的名称和含义有简单认识即可。

我们回到 ShardingRouter 类,发现其核心方法只有一个,即 route 方法。这个方法的逻辑比较复杂,我们梳理它的执行步骤,如下图所示:

ShardingRouter 是路由引擎的核心类,在接下来的内容中,我们将对上图中的 6 个步骤分别一 一 详细展开,帮忙你理解一个路由引擎的设计思想和实现机制。

1.分片合理性验证

我们首先来看 ShardingRouter 的第一个步骤,即验证分片信息的合理性,验证方式如下所示:

//使用ShardingStatementValidator对Statement进行验证 Optional shardingStatementValidator = ShardingStatementValidatorFactory.newInstance(sqlStatement); if (shardingStatementValidator.isPresent()) { shardingStatementValidator.get().validate(shardingRule, sqlStatement, parameters); }

这段代码使用 ShardingStatementValidator 对输入的 SQLStatement 进行验证,可以看到这里用到了典型的工厂模式,工厂类 ShardingStatementValidatorFactory 如下所示:

public final class ShardingStatementValidatorFactory {

public static Optional<ShardingStatementValidator> newInstance(final SQLStatement sqlStatement) { 
    if (sqlStatement instanceof InsertStatement) { 
        return Optional.<ShardingStatementValidator>of(new ShardingInsertStatementValidator()); 
    } 
    if (sqlStatement instanceof UpdateStatement) { 
        return Optional.<ShardingStatementValidator>of(new ShardingUpdateStatementValidator()); 
    } 
    return Optional.absent(); 
} 

}

注意到 ShardingStatementValidator 要验证的只有 InsertStatement 和 UpdateStatement 这两个 SQLStatement。那么如何进行验证呢我们来看一下 ShardingStatementValidator 的定义,如下所示:

public interface ShardingStatementValidator {

//验证分片操作是否支持 
void validate(ShardingRule shardingRule, T sqlStatement, List<Object> parameters); 

}

对于验证过程而言,核心思想在于根据 SQLStatement 中的 Segment 与 ShardingRule 中的规则来判断它们之间是否有需要特殊处理的判断逻辑。我们以 ShardingInsertStatementValidator 为例来看验证过程,它的 validate 方法如下所示:

public final class ShardingInsertStatementValidator implements ShardingStatementValidator {

@Override 
public void validate(final ShardingRule shardingRule, final InsertStatement sqlStatement, final List<Object> parameters) { 
    Optional<OnDuplicateKeyColumnsSegment> onDuplicateKeyColumnsSegment = sqlStatement.findSQLSegment(OnDuplicateKeyColumnsSegment.class); 

    //如果是"ON DUPLICATE KEY UPDATE"语句且如果当前操作的是分片Column时验证不通过 
    if (onDuplicateKeyColumnsSegment.isPresent() && isUpdateShardingKey(shardingRule, onDuplicateKeyColumnsSegment.get(), sqlStatement.getTable().getTableName())) { 
        throw new ShardingException("INSERT INTO .... ON DUPLICATE KEY UPDATE can not support update for sharding column."); 
    } 
}
… 

}

可以看到这里的判断逻辑与“ON DUPLICATE KEY UPDATE”这一 Mysql 特有的语法相关,该语法允许我们通过 Update 的方式插入有重复主键的数据行(实际上这个语法也不是常规语法,本身也不大应该被使用)。

ShardingInsertStatementValidator 先判断是否存在 OnDuplicateKeyColumn然后再判断这个 Column 是否是分片键,如果同时满足这两个条件,则直接抛出一个异常,不允许在分片 Column 上执行“INSERT INTO …. ON DUPLICATE KEY UPDATE”语法。

2.获取上下文

接下来我们来看 ShardingRouter 类中 route 方法的第二段代码,该段代码比较简单,用于获取运行时的 SQLStatement 上下文,如下所示:

//获取 SQLStatementContext SQLStatementContext sqlStatementContext = SQLStatementContextFactory.newInstance(metaData.getRelationMetas(), logicSQL, parameters, sqlStatement);

可以看到这里构建了上下文对象 SQLStatementContext同样用到了工厂模式工厂类 SQLStatementContextFactory 如下所示:

public final class SQLStatementContextFactory {

public static SQLStatementContext newInstance(final RelationMetas relationMetas, final String sql, final List<Object> parameters, final SQLStatement sqlStatement) { 
    if (sqlStatement instanceof SelectStatement) { 
        return new SelectSQLStatementContext(relationMetas, sql, parameters, (SelectStatement) sqlStatement); 
    } 
    if (sqlStatement instanceof InsertStatement) { 
        return new InsertSQLStatementContext(relationMetas, parameters, (InsertStatement) sqlStatement); 
    } 
    return new CommonSQLStatementContext(sqlStatement); 
} 

}

请注意 SQLStatementContext 只有三种:

SelectSQLStatementContext InsertSQLStatementContext CommonSQLStatementContext

它们都实现了 SQLStatementContext 接口,顾名思义,所谓的 SQLStatementContext 就是一种上下文对象,保存着与特定 SQLStatement 相关的上下文信息,用于为后续处理提供数据存储和传递的手段。

我们可以想象在 SQLStatementContext 中势必都持有 SQLStatement 对象以及与表结构信息相关的上下文 TablesContext。

对于 SelectSQLStatement通常也需要保存与查询相关的分组上下文 GroupByContext、排序上下文 OrderByContext 和分页上下文 PaginationContext而对于InsertSQLStatementContext 而言InsertValueContext 则包含了所有与插入操作相关的值对象。

3.自动生成主键

接下来的第三段代码与数据库主键相关,同样只有一句代码,如下所示:

//如果是 InsertStatement 则自动生成主键 Optional generatedKey = sqlStatement instanceof InsertStatement ? GeneratedKey.getGenerateKey(shardingRule, metaData.getTables(), parameters, (InsertStatement) sqlStatement) : Optional.absent();

这段代码的逻辑比较明确,即如果输入的 SQLStatement 是 InsertStatement则自动创建一个主键 GeneratedKey反之就不做处理。

在数据分片的场景下,创建一个分布式主键实际上并没有那么简单,所以在这段代码背后有很多设计的思想和实现的技巧值得我们进行深入分析,关于这个主题,我们已经在 [《14 | 分布式主键ShardingSphere 中有哪些分布式主键实现方式?》]中对分布式主键生成机制做了专题分享。

4.创建分片条件

我们来看 ShardingRouter 中 route 方法的第四个步骤,这个步骤的作用是创建分片条件,如下所示:

//创建分片条件 ShardingConditions shardingConditions = getShardingConditions(parameters, sqlStatementContext, generatedKey.orNull(), metaData.getRelationMetas()); boolean needMergeShardingValues = isNeedMergeShardingValues(sqlStatementContext); if (sqlStatementContext.getSqlStatement() instanceof DMLStatement && needMergeShardingValues) { checkSubqueryShardingValues(sqlStatementContext, shardingConditions); mergeShardingConditions(shardingConditions); }

在 ShardingSphere 中,分片条件对象 ShardingCondition 定义如下所示,包含了一组路由信息和节点信息,其中路由信息包含表名和列名,而节点信息包含数据源名和表名:

public class ShardingCondition { //路由信息 private final List routeValues = new LinkedList<>(); //节点信息 private final Collection dataNodes = new LinkedList<>(); }

那么如何获取分片条件呢?如下所示的 getShardingConditions 方法给出了具体的实现方式,可以看到这里根据输入的 SQL 类型,分别通过 InsertClauseShardingConditionEngine 和WhereClauseShardingConditionEngine 创建了 ShardingConditions

private ShardingConditions getShardingConditions(final List