Sharding-JDBC源码解析(一)整体流程-优快云博客
Sharding-JDBC源码解析(二)SQL解析-优快云博客
Sharding-JDBC源码解析(三)SQL路由-优快云博客
Sharding-JDBC源码解析(四)SQL改写-优快云博客
Sharding-JDBC源码解析(五)SQL执行-优快云博客
Sharding-JDBC源码解析(六)结果归并-优快云博客
目录
一、整体概述
在ShardingShpere中,路由分为两种,即携带分片键的分片路由以及不携带分片键的广播路由,本编笔记仅针对分片路由进行解析。
二、详细流程
下列的代码部分删减掉非核心的部分,部分直接以伪代码或文字的方式描述,需要详细的信息可以自行参照方法入口去阅读。
1.路由整体流程
首先路由的方法入口位于KernelProcessor#route方法,KernelProcessor中最终委托给PartialSQLRouteExecutor#route去完成路由。路由使用的是装饰器模式,也就是一条路由器链,将所有可用的数据节点作为输入,结合配置的规则和sql,经过这个路由器链的计算,最终会输出该条sql对应的数据节点列表,也就是所谓的路由单元。而具体一条sql需要经过哪些路由器,PartialSQLRouteExecutor会根据配置去加载对应的规则,比如根据配置该条sql既涉及到了分片又涉及到了主从,那么路由器链就由分片路由器 + 主从路由器组成,路由器链的顺序会依据每个路由器自身的优先级。
PartialSQLRouteExecutor#route方法如下
public RouteContext route(final LogicSQL logicSQL, final ShardingSphereMetaData metaData) {
RouteContext result = new RouteContext();
// routers即为路由器链
// 假设当前路由器链由分片路由器 + 主从路由器组成
// 且存在一条SQL select * from tb ,现在仅做了分库操作,逻辑数据库ds 对应的物理库为 ds0, ds1
// 则经过分片路由器解析后得出2个路由单元ds0.tb, ds1.tb
// 又因为当前的主从配置如下,也就是读操作指向从库
// ds0 : master -> ds0 slave -> ds0-slave
// ds1 : master -> ds1 slave -> ds1-slave
// 那么再经过主从路由器解析后,从ds0.tb, ds1.tb变成ds0-slave.tb, ds1-slave.tb
for (Map.Entry<ShardingSphereRule, SQLRouter> entry : routers.entrySet()) {
if (result.getRouteUnits().isEmpty()) {
result = entry.getValue().createRouteContext(logicSQL, metaData, entry.getKey(), props);
} else {
entry.getValue().decorateRouteContext(result, logicSQL, metaData, entry.getKey(), props);
}
}
// 没有匹配到任何规则且当前的分片规则配置下,只存在一个物理数据源,会默认生成一个路由单元,指向唯一的数据源,也就是没有配置分表分库的情况。
// 代码略...
return result;
}
2.分片路由器
在分片路由器中,首先会去解析sql中的分片条件,也就是说解析出一条sql中哪些条件可以作为分片的依据。然后根据sql的类型(dcl、dql、ddl、dal等)和查询的类型(标准查询、联合查询、复查查询等)加载对应的分片引擎,最终去解析需要发起请求的数据节点(即路由单元)
同样的此处举个例子,存在如下sql : select * from tb where id in (1,2,3,4) and age in (2,4,6,8),分表策略为标准分片(standard),分片键为id字段,分片算法的表达式为 tb${id % 2} ,那么此时解析出来的分片条件就是 tb.id in (2,4,6,8),且结合sql类型和查询类型此处会选择标准分片引擎,最终计算出需要请求的数据节点为为ds.tb1和db.tb0。
上述代码位于ShardingSQLRouter#createRouteContext
public RouteContext createRouteContext(final LogicSQL logicSQL, final ShardingSphereMetaData metaData, final ShardingRule rule, final ConfigurationProperties props) {
// 解析分片条件。
ShardingConditions shardingConditions = createShardingConditions(logicSQL, metaData, rule);
// 如果包含子查询,则合并这些分片条件。
if (sqlStatement instanceof DMLStatement && shardingConditions.isNeedMerge()) {
shardingConditions.merge();
}
// 选择分片引擎进行路由。
RouteContext result = ShardingRouteEngineFactory.newInstance(rule, metaData, logicSQL.getSqlStatementContext(), shardingConditions, props).route(rule);
return result;
}
2-1.解析分片条件
- 读操作条件解析
代码入口在WhereClauseShardingConditionEngine#createShardingConditions,主要作用就是从给定的where条件中解析出分片条件(即可以参与分片计算的条件)。public List<ShardingCondition> createShardingConditions(final SQLStatementContext<?> sqlStatementContext, final List<Object> parameters) { List<ShardingCondition> result = new ArrayList<>(); // 获取where中的条件 for (WhereSegment each : getWhereSegments(sqlStatementContext)) { // 从给定的where条件中解析出分片条件(即可以参与分片计算的条件) // 例如存在sql select * from tb where id in (1,2,3) and age in (3,4,5) ,且表tb的分片键配置为id,则此处最终解析出的分片条件为 tb.id in (1,2,3)。 result.addAll(createShardingConditions(sqlStatementContext, each.getExpr(), parameters)); } return result; }
- 写操作条件解析
代码入口在InsertClauseShardingConditionEngine#createShardingConditions,insert语句的条件解析分为两种,此处假设id为分片键,那么insert语句分为两种情况,一种指定了id,另外一种是使用数据库自增id, 指定id的情况下,通过解析sql中的id值生成分片条件, 没有指定id的情况下,会使用用户的自增策略生成id,再通过id去生成分片条件。public List<ShardingCondition> createShardingConditions(final InsertStatementContext sqlStatementContext, final List<Object> parameters) { // 指定id的情况下,即可以直接使用id生成分片条件 List<ShardingCondition> result = null == sqlStatementContext.getInsertSelectContext() ? createShardingConditionsWithInsertValues(sqlStatementContext, parameters) : createShardingConditionsWithInsertSelect(sqlStatementContext, parameters); // 没有指定id的情况下,会使用用户的自增策略生成id,再通过id去生成分片条件 appendGeneratedKeyConditions(sqlStatementContext, result); return result; }
2-2.选择路由引擎
-
标准路由引擎
在sql为DQL类型(即查询sql),且查询类型为标准查询时(即单表分片查询,或者连表但是涉及的表都为绑定表),会使用标准路由引擎进行路由。标准路由引擎会根据用户配置的分片策略(hint、standard、complex)结合分片条件解析出数据节点。
代码入口位于ShardingStandardRoutingEngine#route
public RouteContext route(final ShardingRule shardingRule) { RouteContext result = new RouteContext(); // 结合具体使用的分片策略解析出数据节点(数据分片的最小单元,由数据源名称和真实表组成。 例:ds0.tb0) Collection<DataNode> dataNodes = getDataNodes(shardingRule, shardingRule.getTableRule(logicTableName)); // dataNodes结构转换成路由单元 代码略.. return result; } private Collection<DataNode> getDataNodes(final ShardingRule shardingRule, final TableRule tableRule) { // 获取分库、分库策略 ShardingStrategy databaseShardingStrategy = createShardingStrategy(...); ShardingStrategy tableShardingStrategy = createShardingStrategy(...); // 根据配置的分片策略去执行 hint / standard / complex // 最终调用的方法都是route0方法 } private Collection<DataNode> route0(final TableRule tableRule, final ShardingStrategy databaseShardingStrategy, final List<ShardingConditionValue> databaseShardingValues, final ShardingStrategy tableShardingStrategy, final List<ShardingConditionValue> tableShardingValues) { // 先对数据源路由 Collection<String> routedDataSources = routeDataSources(tableRule, databaseShardingStrategy, databaseShardingValues); // 再遍历每个数据源 对表进行路由 // 也就是在routeDataSources方法中进行分库, 在routeTables方法中进行分表 Collection<DataNode> result = new LinkedList<>(); for (String each : routedDataSources) { result.addAll(routeTables(tableRule, each, tableShardingStrategy, tableShardingValues)); } return result; } private Collection<String> routeDataSources(final TableRule tableRule, final ShardingStrategy databaseShardingStrategy, final List<ShardingConditionValue> databaseShardingValues) { // 根据传入的分库策略执行分片 Collection<String> result = new LinkedHashSet<>(databaseShardingStrategy.doSharding(tableRule.getActualDatasourceNames(), databaseShardingValues, properties)); return result; } private Collection<DataNode> routeTables(final TableRule tableRule, final String routedDataSource, final ShardingStrategy tableShardingStrategy, final List<ShardingConditionValue> tableShardingValues) { Collection<String> availableTargetTables = tableRule.getActualTableNames(routedDataSource); // 如果不含分片条件 则返回所有的可用表 否则执行对应的分片策略 Collection<String> routedTables = new LinkedHashSet<>(tableShardingValues.isEmpty() ? availableTargetTables : tableShardingStrategy.doSharding(availableTargetTables, tableShardingValues, properties)); // 转换成数据节点 代码略.. return result; }
标准分片策略
标准分片策略是官方最为推荐的分片方式,支持的>=, https://shardingsphere.apache.org/document/5.0.0/cn/user-manual/shardingsphere-jdbc/configuration/built-in-algorithm/sharding/#%E6%A0%87%E5%87%86%E5%88%86%E7%89%87%E7%AE%97%E6%B3%95
标准分片策略的分片逻辑入口位于StandardShardingStrategy#doSharding
public Collection<String> doSharding(final Collection<String> availableTargetNames, final Collection<ShardingConditionValue> shardingConditionValues, final ConfigurationProperties props) { // 因为标准分片又分为行表达式分片算法和时间范围分片算法,此处仅解析行表达式分片算法 // 执行时也分为两种情况。当sql为精准查询时(例如 =/in这些条件),会将分片条件带入行表达式计算出对应的数据节点。 // 而如果sql为范围查询比如使用的是 >、<、BETWEEN AND等这些条件。那么会直接进行全路由,,默认情况下行表达式算法不开启范围查询 // 可以通过allow-range-query-with-inline-sharding设定。 }
复杂分片策略
可以简单理解为多分片键的标准分片策略。代码入口位于ComplexInlineShardingAlgorithm#doSharding
Hint分片策略
hint策略即在sql查询之前,用户通过HintManager在线程上下文中设置分片值,而后在HintInlineShardingAlgorithm#doSharding方法结合配置用的hint算法计算出最终的分片值public void query(){ HintManager.clear(); HintManager.getInstance().setDatabaseShardingValue("ds0"); // 执行sql查询.. }
-
复杂路由引擎
如果使用了连表查询,且表之间不全为绑定表的关系时,会使用复杂路由引擎。该引擎的主要流程即对每个逻辑表应用标准路由引擎,记录每张表的路由结果,而后使用笛卡尔积路由生成最终的结果。
代码位于ShardingComplexRoutingEngine#route
public RouteContext route(final ShardingRule shardingRule) { for (String each : logicTables) { Optional<TableRule> tableRule = shardingRule.findTableRule(each); if (tableRule.isPresent()) { // 使用标准路由引擎路由 if (!bindingTableNames.contains(each)) { routeContexts.add(new ShardingStandardRoutingEngine(tableRule.get().getLogicTable(), shardingConditions, props).route(shardingRule)); } shardingRule.findBindingTableRule(each).ifPresent(bindingTableRule -> bindingTableNames.addAll(bindingTableRule.getTableRules().keySet())); } } // 省略部分分支... // 对多个路由结果进行笛卡尔积 RouteContext routeContext = new ShardingCartesianRoutingEngine(routeContexts).route(shardingRule); result.getOriginalDataNodes().addAll(routeContext.getOriginalDataNodes()); result.getRouteUnits().addAll(routeContext.getRouteUnits()); return result; }
-
单表路由引擎
对于那些没有配置规则的表,会使用SingleTableRouteEngine去路由,在这个引擎中,查询只会走固定某个数据源,至于数据源要如何确定,则是按照数据源配置的顺序去选择。举个例子,比如表user无任何规则,且数据源配置了ds0,ds1,则优先使用ds0,如果ds0不存在表user,则依次后推。
方法位于SingleTableRouteEngine#route
-
广播路由引擎
当某个表配置为广播表(广播表的概念即每个数据源中都有该表,适用于一些体量较小的表,比如字典表)。那么该表的写请求会由广播路由引擎路由到所有数据源。
ShardingDatabaseBroadcastRoutingEngine#route
-
单播路由引擎
广播表的读请求由单播路由引擎来进行路由,由于所有数据源上都有该表,所以该引擎会将读请求随机路由到某个节点。
ShardingUnicastRoutingEngine#route
3.主从路由器
TODO..