支付宝红包
京东盲盒抽奖
幸运转盘
秒杀
自营热卖
支付宝红包

使用Sharding-jdbc 3.1.0遇到的问题

梦醒梦碎 1年前   阅读数 437 0

注意

本文涉及的源码修改并没有经过严格的验证或大量测试,谨慎参考.

一. 莫名的NullPointerException

总的来说

执行报错,抛空指针异常,发现真实异常被finally代码块中的异常覆盖了.然后改代码放出异常以便排查真正的报错原因.

具体来说

追踪下去发现了ShardingPreparedStatement中sqlRoute()方法抛了异常,routeResult就会为NULL,结果在finally的时候又抛了一次异常覆盖掉了前一次的异常,导致看到的都是nullPointerException
病源:

@Override
public boolean execute() throws SQLException {
    try {
        clearPrevious();
        //抛出异常
        sqlRoute();
        initPreparedStatementExecutor();
        return preparedStatementExecutor.execute();
    } finally {
        //若sqlRoute()存在异常,则routeResult的值为Null.便会再抛一次NullpointerException
        refreshTableMetaData(connection.getShardingContext(), routeResult.getSqlStatement());
        clearBatch();
    }
}

private void sqlRoute() {
    routeResult = routingEngine.route(new ArrayList<>(getParameters()));
}

临时处理方案: 由于我们只有查询(query)语句,故直接在这里加上一个非空判断.

if (routeResult != null) {
    refreshTableMetaData(connection.getShardingContext(), routeResult.getSqlStatement());
}

refreshTableMetaData()方法只有在如下几种场景时才会有作用,其中不包含我的查询场景.故直接跳过,具体作用没看

protected final void refreshTableMetaData(final ShardingContext shardingContext, final SQLStatement sqlStatement) {
    if (sqlStatement instanceof CreateTableStatement) {
        refreshTableMetaData(shardingContext, (CreateTableStatement) sqlStatement);
    } else if (sqlStatement instanceof AlterTableStatement) {
        refreshTableMetaData(shardingContext, (AlterTableStatement) sqlStatement);
    } else if (sqlStatement instanceof DropTableStatement) {
        refreshTableMetaData(shardingContext, (DropTableStatement) sqlStatement);
    }
}

二. 基于暗示(Hint)的路由,表级别的分片策略不生效,只走默认分片策略.

总的来说

仔细看官方文档.忘了就多看看.
addDatabaseShardingValue不会跳过SQL解析引擎,setDatabaseShardingValue则会并且从此无视为单表配置好的分片策略,只会走默认策略

具体来说

一开始没有仔细看官方文档,导致我一直不明白为什么给表配置了shardingAlgorithm却走不进去.
下面是官方文档内容:


添加分片键值

  • 使用hintManager.addDatabaseShardingValue来添加数据源分片键值。
  • 使用hintManager.addTableShardingValue来添加表分片键值。

分库不分表情况下,强制路由至某一个分库时,可使用hintManager.setDatabaseShardingValue方式添加分片。通过此方式添加分片键值后,将跳过SQL解析和改写阶段,从而提高整体执行效率。


若是想分片逻辑精确控制到表单位得使用HintManager.getInstance().addDatabaseShardingValue()而不是setDatabaseShardingValue(),而我没仔细看,就踩了进去.

若使用了setDatabaseShardingValue,
将会使用DatabaseHintSQLRouter路由器并跳过sql解析直接指定目标库(兼容性最好,可以无视sharding-jdbc解析引擎不支持的sql语法报错,比如CTE这种解析引擎无法识别的语法再或者UNION这种解析引擎主动过滤的语法(后者可以强制开启,见三))

public final class PreparedStatementRoutingEngine {

    public PreparedStatementRoutingEngine(final String logicSQL, final ShardingRule shardingRule, final ShardingMetaData shardingMetaData, final DatabaseType databaseType, final boolean showSQL) {
        this.logicSQL = logicSQL;
        //创建路由实例, 当使用了HintManager的setDatabaseShardingValue时,此处会生成DatabaseHintSQLRouter并跳过sql解析.
        //普通情况下,使用ParsingSQLRouter路由并解析sql只有解析了sql才知道表名,才能从配置中找到表对应的逻辑.
        shardingRouter = ShardingRouterFactory.newInstance(shardingRule, shardingMetaData, databaseType, showSQL);
        masterSlaveRouter = new ShardingMasterSlaveRouter(shardingRule.getMasterSlaveRules());
    }
    //路由入口,由shardingRouter.route进行路由. shardingRouter的生成逻辑见上
    public SQLRouteResult route(final List<Object> parameters) {
        if (null == sqlStatement) {
            sqlStatement = shardingRouter.parse(logicSQL, true);
        }
        return masterSlaveRouter.route(shardingRouter.route(logicSQL, parameters, sqlStatement));
    }
}

之所以使用setDatabaseShardingValue会使用DatabaseHintSQLRouter是因为set中databaseShardingOnly = true了

public void setDatabaseShardingValue(final Comparable<?> value) {
    databaseShardingValues.clear();
    addDatabaseShardingValue(HintManagerHolder.DB_TABLE_NAME, value);
    databaseShardingOnly = true;
}

而ShardingRouterFactory创建实例的时候又会根据这个databaseShardingOnly字段是否为true来决定创建哪一个ShardingRouter.见下:

public static ShardingRouter newInstance(final ShardingRule shardingRule, final ShardingMetaData shardingMetaData, final DatabaseType databaseType, final boolean showSQL) {
    return HintManagerHolder.isDatabaseShardingOnly() ? new DatabaseHintSQLRouter(shardingRule, showSQL) : new ParsingSQLRouter(shardingRule, shardingMetaData, databaseType, showSQL);
}

三. sharding-jdbc不支持union语句,强制放行.

总的来说

sharding-jdbc不让union语句执行,如果是项目初期可以考虑规避这个语法(强改也可以),如果是后来加入的又不想改SQL那么就很有必要改代码强制放行了,但会引入新的疑惑:1.会不会对SQL解析造成影响,2.出现多表查询时的疑惑(见具体最后的推测).

具体来说

刚加入sharidng-jdbc时就报了不支持UNION的错误,从逻辑上推理可能性大概就两种:一种是其使用的SQL解析引擎不支持UNION语法.另一种就是出于一些业务上的思考使其拒绝接受UNION语法.而前者我认为高概率不太可能,所以在领导的建议下我决定去看后者.
最终一路追踪下去,可以看到UNION是硬编码写死的不支持,于是我强制去掉了DefaultKeyword.UNION

public abstract class SelectRestClauseParser implements SQLClauseParser {
    private final LexerEngine lexerEngine;
    /** * Parse select rest. */
    public final void parse() {
        Collection<Keyword> unsupportedRestKeywords = new LinkedList<>();
        //可以强制去掉UNION
        unsupportedRestKeywords.addAll(Arrays.asList(DefaultKeyword.UNION, DefaultKeyword.INTERSECT, DefaultKeyword.EXCEPT, DefaultKeyword.MINUS));
        unsupportedRestKeywords.addAll(Arrays.asList(getUnsupportedKeywordsRest()));
        lexerEngine.unsupportedIfEqual(unsupportedRestKeywords.toArray(new Keyword[unsupportedRestKeywords.size()]));
    }
    protected abstract Keyword[] getUnsupportedKeywordsRest();
}

我推测是sharding-jdbc不想(也不该)做多个库的结果集合并时的去重逻辑,故直接ban掉了这个语法.比如下面这个sql路由到了多个库,然后在多个库执行完返回并聚合的时候,将会有两个2333 :
select 2333 union select xxxx from xxxx

四. addDatabaseShardingValue需要填写表名,我不想填.

总的来说

我想统一调用addDatabaseShardingValue()接口,不用再每个持久层调用处加上,但是这个API又需要写入表名.所以我就改了下代码不用填表名了.

具体来说

我用HintManager的addDatabaseShardingValue((String logicTable,Comparable<?> value) 来进行分库又不想填表名,因为我不是在每个持久层调用前手写加的addDatabaseShardingValue的,而是统一拦截了SQL的查询参数并add进去的(具体见下注),故此时没有获取到表名(可以获取SQL自己解析),而若想进入表的hint分片策略,则需要为此表add过shardingValue才行,故需要改造来适配我的需求.

具体点说: 用的mybatis interceptor拦截Executor接口,因为sharding-jdbc的路由在prepareStatement创建时便决定好了,没办法通过拦截Executor更下层的接口add了.

我就直接使用一个常量作为这个addDatabaseShardingValue的表名参数(logicTable),这里我使用的是HintManagerHolder.DB_TABLE_NAME.
如果不作任何修改,那么结果将是不会进入我配置的表分片逻辑class里,而是将SQL在表所配的所有数据节点里跑一次.

sharding.jdbc.config.sharding.tables.表名.database-strategy.hint.algorithm-class-name=分片逻辑class

为了让其适配这种使用方式,
追踪源码,一路找到了RoutingEngineFactory路由引擎工厂.
ParsingSQLRouter通过这个引擎工厂创造RouteEngine然后执行其Route()方法决定路由目标库.
我们为表配置了路由策略,工厂会为我们生产StandardRoutingEngine来为我们分析得出目标数据节点.

//若有为表配置路由策略且查的是单表,那么将会使用StandardRoutingEngine. 若是没有配置过的表,使用DefaultDatabaseRoutingEngine
RoutingResult routingResult = RoutingEngineFactory.newInstance(shardingRule, shardingMetaData.getDataSource(), sqlStatement, shardingConditions).route();

接下来看StandardRoutingEngine.route()方法:

public final class StandardRoutingEngine implements RoutingEngine {

    private final ShardingRule shardingRule;
    private final String logicTableName;

    @Override
    public RoutingResult route() {
        //重点是getDataNodes()方法
        return generateRoutingResult(getDataNodes(shardingRule.getTableRuleByLogicTableName(logicTableName)));
    }

    private Collection<DataNode> getDataNodes(final TableRule tableRule) {
        //若database-strategy和table-strategy都配置成了HintAlgorithm,则会走这个.
        if (isRoutingByHint(tableRule)) {
            return routeByHint(tableRule);
        }
        if (isRoutingByShardingConditions(tableRule)) {
            return routeByShardingConditions(tableRule);
        }
        //当我只配了其中一种的时候,走的是这个.上面的Condition目前不知道干啥的,没有进入过.
        return routeByMixedConditions(tableRule);
    }

    
    //无论routeByHint还是routeByMixedConditions最终都会进入这个routeDataSources方法决定路由库结果.
    private Collection<String> routeDataSources(final TableRule tableRule, final List<ShardingValue> databaseShardingValues) {
        Collection<String> availableTargetDatabases = tableRule.getActualDatasourceNames();
        //此处将会判断databaseShardingValue是否为空(即是否通过HintManager添加了分库值)
        if (databaseShardingValues.isEmpty()) {
            return availableTargetDatabases;
        }
        //此处获取我们的分库逻辑ShardingAlgorithm然后执行我们的doSharding方法.
        Collection<String> result = new LinkedHashSet<>(shardingRule.getDatabaseShardingStrategy(tableRule).doSharding(availableTargetDatabases, databaseShardingValues));
        Preconditions.checkState(!result.isEmpty(), "no database route info");
        return result;
    }

    //上述传入的databaseShardingValues就来自于此方法
    private List<ShardingValue> getDatabaseShardingValuesFromHint() {
        //HintManagerHolder中存的是一个Map,其存储了<表名-->对应的add进去的分库值>
        //可以将此处的logicTableName改为我们的HintManagerHolder.DB_TABLE_NAME适配上述的操作
        Optional<ShardingValue> shardingValueOptional = HintManagerHolder.getDatabaseShardingValue(logicTableName);
        return shardingValueOptional.isPresent() ? Collections.singletonList(shardingValueOptional.get()) : Collections.<ShardingValue>emptyList();
    }
}

最终都会进入routeDataSources再调用我们写好的分库逻辑dosharding进行分库.而此时提供给我们的List<ShardingValue> databaseShardingValues来自于
getDatabaseShardingValuesFromHint方法(若为空,将会返回所有可用库,即拿着我们的sql去所有库都跑一次).
因为我们往HintManager中add的值的key是HintManagerHolder.DB_TABLE_NAME,
所以此处可以将HintManagerHolder.getDatabaseShardingValue(logicTableName)中的logicTableName参数改成我们期望的HintManagerHolder.DB_TABLE_NAME来完成此次需求.

五. 强制路由(set)时不知道表名是啥.

总的来说

最后选择了set方式强制路由,跳过SQL解析了.但我想在分片逻辑时知道表名(用于垂直分库)且不用改动原来的那份逻辑.那么答案就只有一个了,动手改吧. 跳过SQL解析就给它加上SQL解析,接口没有表名传递就给加上.

其实也可以在持久层拦截处通过解析SQL方式获取到表名再作为分片键值传递给分片逻辑接口中.

具体来说

这个问题得先从我用了HintManager的setDatabaseShardingValue()之后开始讲起.
起初我是用addDatabaseShardingValue添加分片键值,但是这种方式会走sql解析,不是说sql解析不好,而是我们的项目中存在使用CTE语法的情况,而这个语法不被sharding-jdbc 3.1.0的SQL解析引擎所支持.于是我便改用了setDatabaseShardingValue方式强制路由(PS:所幸我们在sharding-jdbc侧分库不分表(PS:我们分表使用PG自带的分区表,且我们只有查询的场景.)).
通过此方式添加分片键值后,将跳过SQL解析和改写阶段(即不解析SQL内容,直接根据添加的分片键值判断目标库. 起初只有水平分库,又因为我是拦截mybatis参数的形式将分库字段扔进分库逻辑HintShardingAlgorithm接口中去(默认不带表名参数,所以这里改了原实现),故实现方便,直接在分库逻辑(即shardingAlgorithm接口)中判断分片键值然后给出水平库即可.
后来又考虑加上垂直分库,其中几张表需要放入其他垂直库中去,最快的方式便是在ShardingAlgorithm里面再加一级判断表名的垂直分库,故我改了原来提供的HintShardingAlgorithm的接口,往里面追加了一各tableName的参数.

public interface HintShardingAlgorithm extends ShardingAlgorithm {
    //起初的接口
    Collection<String> doSharding(Collection<String> availableTargetNames, ShardingValue shardingValue);
    //修改后的.
    Collection<String> doSharding(Collection<String> availableTargetNames, Collection<String> logicalTableNames, ShardingValue shardingValue);
}

接口改了,顺带也改了使用到接口的几个相关类.然后重头是这个logicalTableNames的参数从哪儿来. 既然非set方式会进行sql解析,我便也给set方式加上了sql解析,然后从其解析完的sqlStatement中获取表名.

//路由引擎,负责调用路由器进行SQL解析和路由并返回路由结果
public final class PreparedStatementRoutingEngine {
    //其他略... 

        //当未使用set时是ParsingSQLRouter,反之则是DatabaseHintSQLRouter,后者默认不走强制路由
        private final ShardingRouter shardingRouter;

    public SQLRouteResult route(final List<Object> parameters) {
        if (null == sqlStatement) {
                       // parse解析完后返回的sqlStatement中包含表名(如果进行了正确的SQL解析).
            sqlStatement = shardingRouter.parse(logicSQL, true);
        }
        return masterSlaveRouter.route(shardingRouter.route(logicSQL, parameters, sqlStatement));
    }
}
//会进行SQL解析的路由器
public final class ParsingSQLRouter implements ShardingRouter {
    //其他略....

    @Override
    public SQLStatement parse(final String logicSQL, final boolean useCache) {
        parsingHook.start(logicSQL);
        try {
            // 此处创建了SQL解析引擎并进行解析返回解析完成的SQLStatement
            SQLStatement result = new SQLParsingEngine(databaseType, logicSQL, shardingRule, shardingMetaData.getTable()).parse(useCache);
            parsingHook.finishSuccess();
            return result;
        } catch (final Exception ex) {
            parsingHook.finishFailure(ex);
            throw ex;
        }
    }
}
// 强制路由时使用的路由器
public final class DatabaseHintSQLRouter implements ShardingRouter {
        //其他略...... 

        //默认的解析逻辑,强制路由的路由器不会进行SQL解析,返回的sqlStatement中不包含表名.
    @Override
    public SQLStatement parse(final String logicSQL, final boolean useCache) {
        return new SQLJudgeEngine(logicSQL).judge();
    }
    //修改后的路由器解析方法,参照了ParsingSQLRouter解析SQL的方式.
    @Override
    public SQLStatement parse(final String logicSQL, final boolean useCache) {
        try {
            //与ParsingSQLRouter一样的逻辑
            return new SQLParsingEngine(databaseType, logicSQL, shardingRule, shardingMetaData.getTable()).parse(useCache);
        } catch (final Exception ex) {
            //留下了解析不了SQL就继续用原来的策略,为了应对解析不了的语法比如CTE.
            return new SQLJudgeEngine(logicSQL).judge();
        }
    }
}

都改完了,看起来都完善了之后又遇到了一个问题. 那便是实际跑起来,并没有拿到表名.
无论我怎么跑,sqlStatement中的table永远都是空的.
一路debug追踪SQL解析引擎发现了猫腻

String tableName = SQLUtil.getExactlyValue(literals);
if (Strings.isNullOrEmpty(tableName)) {
    return;
}
//1.是否单表(但是这个值从入口看发现是写死的false,原因不明)
//2.是否有为此表配置分库分表逻辑(我走统一的强制路由,不进行单表逻辑配置)
//3.是否广播表
//4.绑定表是否有分库逻辑.
//5.可用数据源(统一的分库逻辑,故此处是全部数据源)中是否包含默认数据源.
if (isSingleTableOnly || shardingRule.findTableRuleByLogicTable(tableName).isPresent()
        || shardingRule.isBroadcastTable(tableName) || shardingRule.findBindingTableRule(tableName).isPresent()
        || shardingRule.getShardingDataSourceNames().getDataSourceNames().contains(shardingRule.getShardingDataSourceNames().getDefaultDataSourceName())) {
    sqlStatement.addSQLToken(new TableToken(beginPosition, skippedSchemaNameLength, literals));
    sqlStatement.getTables().add(new Table(tableName, aliasExpressionParser.parseTableAlias(sqlStatement, true, tableName)));
} else {
    aliasExpressionParser.parseTableAlias();
}

追踪到这段代码的时候,表名实际上是解析获取成功了,但是下面的判断最终导致了tableName没有被添加进sqlStatement中去.
而这当中,我最快能实现的便是添加上如下配置

sharding.jdbc.config.sharding.default-data-source-name=默认数据源名

最后终于可以愉快的在HintShardingAlgorithm中使用表名了.

发布了9 篇原创文章 · 获赞 7 · 访问量 6141

注意:本文归作者所有,未经作者允许,不得转载

全部评论: 0

    我有话说: