乐趣区

关于java:ShardingSphereJDBC分片路由引擎

前言

上文 ShardingSphere-JDBC 分片解析引擎中介绍了分片流程中的解析引擎,重点介绍了解析引擎的外围组件ANTLR;本文持续介绍分片流程中的路由引擎,路由引擎能够说是整个分片流程的外围模块,用户自定义的分片算法都在路由引擎中执行;

开启日志

为了更加清晰具体的查看路由日志,开启 SQL_SHOW 性能:

  • 引入 log4j 相干 jar,以及 log4j.xml 配置文件
  • 配置 SQL_SHOW 属性为开启:

    Properties prop = new Properties();
    prop.put(ConfigurationPropertyKey.SQL_SHOW.getKey(), true);

路由装璜器

路由引擎的外层包装了一层路由装璜器 RouteDecorator,为什么要这么设计是因为除了咱们失常走路由算法的路由引擎,ShardingSphere-JDBC 还提供了读写拆散性能,这其实在肯定水平上来讲也是路由,而且这两种路由形式是能够叠加的;所有这里提供了一层形象,实现类包含:

  • MasterSlaveRouteDecorator:读写拆散路由装璜器;
  • ShardingRouteDecorator:分片路由装璜器,外部蕴含了各种路由引擎;

装璜器能够叠加,所以提供了优先级性能OrderAware,同时每个装璜器都有对应的规定,大抵如下所示:

装璜器 -RouteDecorator 配置 -Configuration 规定 -BaseRule 优先级 -Order
MasterSlaveRouteDecorator MasterSlaveRuleConfiguration MasterSlaveRule 10
ShardingRouteDecorator ShardingRuleConfiguration ShardingRule 0

依据优先级能够晓得首先执行 ShardingRouteDecorator,有了路由后果再执行MasterSlaveRouteDecorator;局部启动类代码在DataNodeRouter 中如下所示:

private final Map<BaseRule, RouteDecorator> decorators = new LinkedHashMap<>();
private RouteContext executeRoute(final String sql, final List<Object> parameters, final boolean useCache) {RouteContext result = createRouteContext(sql, parameters, useCache);
      for (Entry<BaseRule, RouteDecorator> entry : decorators.entrySet()) {result = entry.getValue().decorate(result, metaData, entry.getKey(), properties);
      }
      return result;
}

decorators会依据用户的配置来决定是否会启动对应的装璜器,能够参考下面的表格;上面依照优先级别离介绍两种装璜器;

分片路由装璜器

通过解析引擎获取到了SQLStatement,想要做分片路由除了此参数还须要另外一个重要参数分片路由规定ShardingRule;有了这两个外围参数分片路由大抵能够分为以下几步:

  • 获取分片条件ShardingConditions
  • 获取具体分片引擎ShardingRouteEngine
  • 执行路由解决,获取路由后果

在具体介绍每一步之前,首先介绍以下几个外围参数RouteContextShardingRule

外围参数

重点看一下 RouteContextShardingRule 这两个外围参数;

RouteContext

路由上下文参数,次要蕴含如下几个参数:

public final class RouteContext {
    private final SQLStatementContext sqlStatementContext;
    private final List<Object> parameters;
    private final RouteResult routeResult;
}
  • sqlStatementContext:解析引擎获取的SQLStatement
  • parameters:PreparedStatement中设置的参数,如执行 insert 操作 setXxx 代替?
  • routeResult:路由之后用来寄存路由后果;

ShardingRule

分片规定,主要参数如下,这个其实和 ShardingRuleConfiguration 大同小异,只是从新做了一个包装;

public class ShardingRule implements BaseRule {
    private final ShardingRuleConfiguration ruleConfiguration;
    private final ShardingDataSourceNames shardingDataSourceNames;
    private final Collection<TableRule> tableRules;
    private final Collection<BindingTableRule> bindingTableRules;
    private final Collection<String> broadcastTables;
    private final ShardingStrategy defaultDatabaseShardingStrategy;
    private final ShardingStrategy defaultTableShardingStrategy;
    private final ShardingKeyGenerator defaultShardingKeyGenerator;
    private final Collection<MasterSlaveRule> masterSlaveRules;
    private final EncryptRule encryptRule;
  • ruleConfiguration:路由规定配置,能够了解为是 ShardingRule 的原始文件;
  • shardingDataSourceNames:分片的数据源名称;
  • tableRules:表路由规定,对应了用户配置的TableRuleConfiguration
  • bindingTableRules:绑定表配置,分⽚规定⼀致的主表和⼦表;
  • broadcastTables:播送表配置,所有的分⽚数据源中都存在的表;
  • defaultDatabaseShardingStrategy:默认库分片策略;
  • defaultTableShardingStrategy:默认表分片策略;
  • defaultShardingKeyGenerator:默认主键生成策略;
  • masterSlaveRules:主从规定配置,用来实现读写拆散的,可配置一个主表多个从表;
  • encryptRule:加密规定配置,提供了对某些敏感数据进行加密的性能;

获取分片条件

在获取具体路由引擎和执行路由操作之前,咱们须要获取分片的条件,常见的分片条件次要在 Insert 语句和 Where 语句前面;局部获取分片条件的源码如下:

    private ShardingConditions getShardingConditions(final List<Object> parameters, 
                                                     final SQLStatementContext sqlStatementContext, final SchemaMetaData schemaMetaData, final ShardingRule shardingRule) {if (sqlStatementContext.getSqlStatement() instanceof DMLStatement) {if (sqlStatementContext instanceof InsertStatementContext) {return new ShardingConditions(new InsertClauseShardingConditionEngine(shardingRule).createShardingConditions((InsertStatementContext) sqlStatementContext, parameters));
            }
            return new ShardingConditions(new WhereClauseShardingConditionEngine(shardingRule, schemaMetaData).createShardingConditions(sqlStatementContext, parameters));
        }
        return new ShardingConditions(Collections.emptyList());
    }

首先会判断是不是 DMLStatement 类型,也是最常见的 SQL 类型比方:增删改查;接下来判断是否是 Insert 语句,别离应用不同的分片条件生成引擎:

  • InsertClauseShardingConditionEngine:解决 Insert 语句分片条件;
  • WhereClauseShardingConditionEngine:解决非 Insert 语句 Where 语句的分片条件;

InsertClauseShardingConditionEngine

Insert 语句中蕴含分片条件次要有两个中央:

  • Insert 语句中指定的字段名称,当然是否是分片条件,还须要检测 ShardingRule 中的 tableRules 是否配置了相干字段作为分片键,看一条简略的 Insert 语句:

    insert into t_order (user_id,order_id) values (?,?)

这一步其实就是检测 user_id 和 order_id 是否在 ShardingRule 中配置成分片键;

  • 当然除了下面显示指定的字段,还有无需显示指定的主键,如果配置了主键生成策略,同样须要检测 ShardingRule 中的 tableRules 是否配置了相干字段作为分片键;

    tableRuleConfig.setKeyGeneratorConfig(new KeyGeneratorConfiguration("SNOWFLAKE", "id"));

生成的后果就是ShardingConditions,外部蕴含多个ShardingCondition

public final class ShardingConditions {private final List<ShardingCondition> conditions;}

public class ShardingCondition {private final List<RouteValue> routeValues = new LinkedList<>();
}

这里的 RouteValue 实现包含ListRouteValueRangeRouteValueAlwaysFalseRouteValue

  • ListRouteValue:能够了解分片键对应的是一个具体的值,能够是单个也能够多个;
  • RangeRouteValue:分片键对应的是一个区间值;
  • AlwaysFalseRouteValue:总是失败的路由值;

WhereClauseShardingConditionEngine

常见 Select 语句,Where 语句前面能够蕴含多个条件,每个条件同样须要检测 ShardingRule 中的 tableRules 是否配置了相干字段作为分片键;稍有不同的中央是,Where 条件须要做合并解决,比方:

String sql = "select user_id,order_id from t_order where order_id = 101 and order_id = 101";
String sql = "select user_id,order_id from t_order where order_id = 101 and order_id in(101)";

order_id 呈现多个值一样会进行合并解决,这里会合并成一个order_id = 101,如果这里两个值不一样比方:

String sql = "select user_id,order_id from t_order where order_id = 101 and order_id = 102";

会返回一个AlwaysFalseRouteValue,示意这个条件不可能成立;

获取路由引擎

ShardingSphere-JDBC依据不同的 SQLStatement 提供了 10 种路由引擎,上面别离介绍,首先看一下大抵的流程图;

流程图

流程图大抵如上所示,具体查看 ShardingRouteEngineFactory 即可;上面具体介绍每个路由引擎;

路由引擎

ShardingDatabaseBroadcastRoutingEngine

全库路由引擎:用于解决对数据库的操作,包含用于库设置的 SET 类型的数据库治理命令,以及 TCL 这样的事务管制语句。在这种状况下,会依据逻辑库的名字遍历所有合乎名字匹配的实在库,并在实在库中执行该命令;

1. 属于 DALStatement

数据库拜访层,常见的命令包含:set,reset,show databases;

show databases;

以上 sql 会全库路由,路由 sql 如下所示:

Actual SQL: ds0 ::: show databases;
Actual SQL: ds1 ::: show databases;
2. 逻辑表都属于播送表
insert into t_config (k,v) values (?,?)

t_config配置的是一张播送表,执行 insert 操作会将数据插入所有库中;当然前提须要配置播送表:

Collection<String> broadcastTables = new LinkedList<>();
broadcastTables.add("t_config");
shardingRuleConfig.setBroadcastTables(broadcastTables);

路由日志如下:

Actual SQL: ds0 ::: insert into t_config (k,v) values (?, ?) ::: [aa1, 1111]
Actual SQL: ds1 ::: insert into t_config (k,v) values (?, ?) ::: [aa1, 1111]
3. 属于 TCLStatement

事务管制语言,包含设置保留点,回滚等;

SET autocommit=0

路由日志如下:

Actual SQL: ds0 ::: SET autocommit=0;
Actual SQL: ds1 ::: SET autocommit=0;

ShardingTableBroadcastRoutingEngine

全库表路由用于解决对数据库中与其逻辑表相干的所有实在表的操作,次要包含不带分片键的 DQL 和 DML,以及 DDL 等;

1. 属于 DDLStatement

数据库定义语言,包含创立、批改、删除表等;

ALTER  TABLE t_order MODIFY  COLUMN user_id  BIGINT(50) NOT NULL;

日志如下所示:

Actual SQL: ds0 ::: ALTER  TABLE t_order0 MODIFY  COLUMN user_id  BIGINT(50) NOT NULL;
Actual SQL: ds0 ::: ALTER  TABLE t_order1 MODIFY  COLUMN user_id  BIGINT(50) NOT NULL;
Actual SQL: ds1 ::: ALTER  TABLE t_order0 MODIFY  COLUMN user_id  BIGINT(50) NOT NULL;
Actual SQL: ds1 ::: ALTER  TABLE t_order1 MODIFY  COLUMN user_id  BIGINT(50) NOT NULL;
2. 属于 DCLStatement

数据库管制语言,包含受权,角色管制等;grant,deny 等命令

grant select on ds.t_order to root@'%'

给用户 root 受权 select 权限,这里须要指定惟一的表名,不能应用 * 代替;

Actual SQL: ds0 ::: grant select on t_order0 to root@'%'
Actual SQL: ds0 ::: grant select on t_order1 to root@'%'
Actual SQL: ds1 ::: grant select on t_order0 to root@'%'
Actual SQL: ds1 ::: grant select on t_order1 to root@'%'

ShardingIgnoreRoutingEngine

阻断路由用于屏蔽 SQL 对数据库的操作;

1.DALStatement

阻断路由次要针对 DALStatement 上面的 use 命令;

use ds0

ShardingDefaultDatabaseRoutingEngine

默认数据库路由,须要配置默认数据源名称,蕴含以下几种状况:

1. 属于 DALStatement
show create table t_order1

留神点:这里表是实在表,不能配置TableRuleConfiguration,须要配置默认数据源名称;

shardingRuleConfig.setDefaultDataSourceName("ds0");

日志如下:

Actual SQL: ds0 ::: show create table t_order1
2. 逻辑表都属于默认数据源
select user_id,order_id from t_order0 where user_id = 102

留神点:这里表是实在表,不能配置TableRuleConfiguration,须要配置默认数据源名称,不能配置为播送表;

Actual SQL: ds0 ::: select user_id,order_id from t_order0 where user_id = 102
3. 属于 DMLStatement
select 2+2

DMLStatement 没有表名,并且须要指定默认数据源名称;

Actual SQL: ds0 ::: select 2+2

ShardingUnicastRoutingEngine

单播路由用于获取某一实在表信息的场景,它仅须要从任意库中的任意实在表中获取数据即可;

1. 属于 DALStatement
desc t_order

日志如下:

Actual SQL: ds1 ::: desc t_order0
2. 逻辑表都属于播送表

播送表分片数据源中都存在的表,个别是字典表:

select * from t_config

须要配置播送表:

Collection<String> broadcastTables = new LinkedList<>();
broadcastTables.add("t_config");
shardingRuleConfig.setBroadcastTables(broadcastTables);

日志如下:

Actual SQL: ds0 ::: select * from t_config
3. 属于 DMLStatement

属于 DMLStatement 同时分片条件为AlwaysFalseShardingCondition,或者没有指定表名,或者没有配置表规定;

select user_id,order_id from t_order where order_id = 101 and order_id = 102

where 前面指定的条件会导致分片条件为AlwaysFalseShardingCondition

Actual SQL: ds1 ::: select user_id,order_id from t_order0 where order_id = 101 and order_id = 102

ShardingDataSourceGroupBroadcastRoutingEngine

数据源组播送,从数据源组中随机抉择一个数据源;

1. 属于 DALStatement
SHOW STATUS

DALStatement子类中除了 use、set、reset、show databases;根本都会走此引擎;

Actual SQL: ds1 ::: SHOW STATUS

ShardingMasterInstanceBroadcastRoutingEngine

全实例路由用于 DCL 操作,受权语句针对的是数据库的实例。无论一个实例中蕴含多少个 Schema,每个数据库的实例只执行一次;

1. 属于 DCLStatement
CREATE USER customer@127.0.0.1 identified BY '123'

:这里的主实例会检测各实例之间,不能有雷同的hostname 和雷同的port,本地测试同一台 Mysql 不同库,配置 hostname 不统一即可,比方 localhost 和 127.0.0.1;

ShardingStandardRoutingEngine

规范路由是最常见的分片形式了,通过以上几种路由引擎的过滤,剩下的SQLStatement,就会走剩下的两个引擎了,咱们配置分库分表策略,惯例应用的增删改查都会应用此引擎;

1. 单表查问
select user_id,order_id from t_order where order_id = 101

以上应用惯例配置,两个数据源别离是 ds0,ds1;user_id 作为分库键,order_id 作为分表键;

Actual SQL: ds0 ::: select user_id,order_id from t_order1 where order_id = 101
Actual SQL: ds1 ::: select user_id,order_id from t_order1 where order_id = 101

101 通过分片算法定位到物理表t_order1,然而无奈定位数据库,所以别离到两个库执行;

2. 关联查问
select a.user_id,a.order_id from t_order a left join t_order_item b ON a.order_id=b.order_id where a.order_id = 101

如果是关联查问,则只有在两者配置了绑定关系,才会应用规范路由;

Collection<String> bindingTables = new LinkedList<>();
bindingTables.add("t_order,t_order_item");
shardingRuleConfig.setBindingTableGroups(bindingTables);

以上配置了两张表为绑定表,关联查问与单表查问复杂度和性能相当,不会进行笛卡尔路由;

Actual SQL: ds0 ::: select a.user_id,a.order_id from t_order1 a left join t_order1 b ON a.order_id=b.order_id where a.order_id = 101
Actual SQL: ds1 ::: select a.user_id,a.order_id from t_order1 a left join t_order1 b ON a.order_id=b.order_id where a.order_id = 101

ShardingComplexRoutingEngine

笛卡尔路由是最简单的状况,它无奈依据绑定表的关系定位分片规定,因而非绑定表之间的关联查问须要拆解为笛卡尔积组合执行;

1. 关联查问

以上 SQL 如果不配置绑定关系,那么会进行笛卡尔路由,路由日志如下:

Actual SQL: ds0 ::: select a.user_id,a.order_id from t_order1 a left join t_order_item0 b ON a.order_id=b.order_id where a.order_id = 101
Actual SQL: ds0 ::: select a.user_id,a.order_id from t_order1 a left join t_order_item1 b ON a.order_id=b.order_id where a.order_id = 101
Actual SQL: ds1 ::: select a.user_id,a.order_id from t_order1 a left join t_order_item0 b ON a.order_id=b.order_id where a.order_id = 101
Actual SQL: ds1 ::: select a.user_id,a.order_id from t_order1 a left join t_order_item1 b ON a.order_id=b.order_id where a.order_id = 101

执行路由解决

通过以上流程解决,曾经获取到了解决此 SQLStatement 对应的路由引擎,接下来只须要执行对应的路由引擎,获取路由后果即可;

public interface ShardingRouteEngine {RouteResult route(ShardingRule shardingRule);
}

传入分片规定 ShardingRule,返回路由后果RouteResult;上面以规范路由为例,来剖析是如何执行路由解决的;ShardingStandardRoutingEngine 的外围办法 getDataNodes 如下所示:

    private Collection<DataNode> getDataNodes(final ShardingRule shardingRule, final TableRule tableRule) {if (isRoutingByHint(shardingRule, tableRule)) {return routeByHint(shardingRule, tableRule);
        }
        if (isRoutingByShardingConditions(shardingRule, tableRule)) {return routeByShardingConditions(shardingRule, tableRule);
        }
        return routeByMixedConditions(shardingRule, tableRule);
    }

以上有三种路由形式:hint 形式路由、分片条件路由、混合条件路由;上面别离介绍;

Hint 形式路由

首先判断是否应用强制路由形式:

    private boolean isRoutingByHint(final ShardingRule shardingRule, final TableRule tableRule) {return shardingRule.getDatabaseShardingStrategy(tableRule) instanceof HintShardingStrategy && shardingRule.getTableShardingStrategy(tableRule) instanceof HintShardingStrategy;
    }

须要库和表路由策略都是 HintShardingStrategy,这个只须要在配置TableRuleConfiguration 别离配置数据库和表的策略都为 HintShardingStrategyConfiguration 即可;

 private Collection<DataNode> route0(final ShardingRule shardingRule, final TableRule tableRule, final List<RouteValue> databaseShardingValues, final List<RouteValue> tableShardingValues) {Collection<String> routedDataSources = routeDataSources(shardingRule, tableRule, databaseShardingValues);
        Collection<DataNode> result = new LinkedList<>();
        for (String each : routedDataSources) {result.addAll(routeTables(shardingRule, tableRule, each, tableShardingValues));
        }
        return result;
    }

接下来就是别离执行路由库和路由表,不论是路由库还是表都须要两个外围的参数:以后可用的指标库或表、以后的分片值;

  • 以后可用的指标库或表:这个就是初始化的库和表,比方指标库包含 ds0、ds1,指标表包含 t_order0、t_order1;
  • 以后的分片值:以后的 hint 形式就是通过 HintManager 配置的分片值;

其余两种形式其实路由形式也都相似,只是分片值获取的形式不一样;有了这两个值就会调用咱们本人定义的分库分表算法 ShardingAlgorithm,这样就返回了通过路由后的库表,将后果保留到DataNode 中:

public final class DataNode {
    private final String dataSourceName;
    private final String tableName;
}

一个实在的库对应一个实在的表;最初将 DataNode 封装到 RouteResult 即可;

分片条件路由

同样首先判断是否走分片条件路由:

    private boolean isRoutingByShardingConditions(final ShardingRule shardingRule, final TableRule tableRule) {return !(shardingRule.getDatabaseShardingStrategy(tableRule) instanceof HintShardingStrategy || shardingRule.getTableShardingStrategy(tableRule) instanceof HintShardingStrategy);
    }

库和表路由策略都不是 HintShardingStrategy 的状况下才会走分片条件路由;

    private Collection<DataNode> routeByShardingConditions(final ShardingRule shardingRule, final TableRule tableRule) {return shardingConditions.getConditions().isEmpty()
                ? route0(shardingRule, tableRule, Collections.emptyList(), Collections.emptyList()) : routeByShardingConditionsWithCondition(shardingRule, tableRule);
    }

而后会判断是否有 ShardingConditions,对于ShardingConditions 下面章节会专门介绍;如果没有 ShardingConditions 阐明没有条件就会走全库表路由,如果有的话会从 ShardingConditions 中取出库表分片值,上面的逻辑就和 Hint 形式一样了;

混合条件路由

混合模式就是库和表路由策略都不全都是HintShardingStrategy,要么表应用强制路由,要么库应用强制路由;

    private Collection<DataNode> routeByMixedConditions(final ShardingRule shardingRule, final TableRule tableRule) {return shardingConditions.getConditions().isEmpty() ? routeByMixedConditionsWithHint(shardingRule, tableRule) : routeByMixedConditionsWithCondition(shardingRule, tableRule);
    }

会判断是否有ShardingConditions,如果没有阐明库或者表路由有一个应用HintShardingStrategy,另外一个没有;否则就是 Hint 和 Condition 混合;这种状况就要看谁的优先级高了,很显著是 Hint 形式优先级高,可用看库分片值获取:

   private List<RouteValue> getDatabaseShardingValues(final ShardingRule shardingRule, final TableRule tableRule, final ShardingCondition shardingCondition) {ShardingStrategy dataBaseShardingStrategy = shardingRule.getDatabaseShardingStrategy(tableRule);
        return isGettingShardingValuesFromHint(dataBaseShardingStrategy)
                ? getDatabaseShardingValuesFromHint() : getShardingValuesFromShardingConditions(shardingRule, dataBaseShardingStrategy.getShardingColumns(), shardingCondition);
    }

如果能获取到 Hint 分片值,那就应用 Hint 值,否则就从 Condition 中获取;

读写拆散路由装璜器

通过下面分片路由装璜器的解决,依据优先级,如果配置了读写拆散会执行读写拆散装璜器MasterSlaveRouteDecorator;大抵流程如下所示:

读写拆散配置

List<String> slaveDataSourceNames0 = new ArrayList<String>();
slaveDataSourceNames0.add("ds01");
MasterSlaveRuleConfiguration masterSlaveRuleConfiguration0 = new MasterSlaveRuleConfiguration("ds0", "ds0",
                slaveDataSourceNames0);
shardingRuleConfig.getMasterSlaveRuleConfigs().add(masterSlaveRuleConfiguration0);
        
List<String> slaveDataSourceNames1 = new ArrayList<String>();
slaveDataSourceNames1.add("ds11");
MasterSlaveRuleConfiguration masterSlaveRuleConfiguration1 = new MasterSlaveRuleConfiguration("ds1", "ds1",
                slaveDataSourceNames1);
shardingRuleConfig.getMasterSlaveRuleConfigs().add(masterSlaveRuleConfiguration1);

首先必须配置了读写拆散策略 ds0 备库为 ds01,ds1 备库为 ds11;

库名称匹配

通过分片路由生成的 RouteUnit 中对应的库名称,和 MasterSlaveRule 中配置的名称能匹配;

读写路由

    public String route(final SQLStatement sqlStatement) {if (isMasterRoute(sqlStatement)) {MasterVisitedManager.setMasterVisited();
            return masterSlaveRule.getMasterDataSourceName();}
        return masterSlaveRule.getLoadBalanceAlgorithm().getDataSource(masterSlaveRule.getName(), masterSlaveRule.getMasterDataSourceName(), new ArrayList<>(masterSlaveRule.getSlaveDataSourceNames()));
    }

并不是配置了读写拆散都会进行路由解决,有些 SQLStatement 是必须走主表的:

private boolean isMasterRoute(final SQLStatement sqlStatement) {return containsLockSegment(sqlStatement) || !(sqlStatement instanceof SelectStatement) || MasterVisitedManager.isMasterVisited() || HintManager.isMasterRouteOnly();
    }
    
    private boolean containsLockSegment(final SQLStatement sqlStatement) {return sqlStatement instanceof SelectStatement && ((SelectStatement) sqlStatement).getLock().isPresent();
    }
  • SelectStatement中蕴含了锁
  • SelectStatement
  • 配置了MasterVisitedManager,外部应用 ThreadLocal 治理
  • 配置了HintManager,外部应用 ThreadLocal 治理

以上四种状况都是走主表,其余状况走备库,如果有多台备库,会进行负载平衡解决;

替换路由库

通过读写拆散路由解决之后,获取到库名称须要替换原理的库名称;

总结

本文次要介绍了 ShardingSphere-JDBC 的分片路由引擎,重点和难点在于不同类型的 SQLStatement 应用不同的路由引擎,路由引擎泛滥,本文重点举例介绍了每种路由引擎在何种条件下应用;通过路由引擎会获取到路由后果上面就是对 SQL 进行改写了,改写成实在的库表,这样数据库能力执行。

参考

https://shardingsphere.apache…

感激关注

能够关注微信公众号「回滚吧代码」,第一工夫浏览,文章继续更新;专一 Java 源码、架构、算法和面试。

退出移动版