Sharding-Jdbc 3.1.0遇到的問題與處理


讀前請注意

本文涉及的源碼修改並沒有經過嚴格的驗證或大量測試,謹慎參考.
另修改部分可在(github.com/jiangxiewei/jxw-sharding-jdbc)上查閱

RELEASE-1.0.0(我們生產跑老久了😂)

  • 基於sharding-jdbc 3.1.0版本進行修改.(非apache版本)
  • 提前了ConfigMapContext的初始化操作,使用戶的sharding-algorithm在使用時能正常拿到配置值.
  • 修復了sharding-jdbc一個異常無法拋出的bug(只能拋出NullPointerException,隱藏了真實異常).
  • 給基於hint(暗示)的路由方式中,setShardingValue進行的強制數據庫路由添加了SQL解析模塊(之前是跳過的).並給HintShardingAlgorithm添加了解析出來的表名. PS:此修改僅對基於hint的強制路由和 prepareStatement形式的查詢進行過簡單測試,其他情況使用請慎重.
  • 原版不支持包含UNION的SQL解析,此處特意放開這個限制,可能會引入BUG,可自行修改關閉.
  • 原版3.1.0的遺留問題: 分頁可能存在問題. github issue:https://github.com/apache/shardingsphere/issues/1722 目前認為可能是這個原因:https://blog.csdn.net/weixin_30721077/article/details/99647265 issue對應的修復是加上了單表時的特判.下版本(不存在)我將會嘗試對此問題的修復

提前configMapContext的注入便於shardingAlgorithm使用

  • 總的來說
    ShardingDataSource初始化時會讀取"sharding.jdbc.config.config-map"配置放入configMapContext類中.
    但是用戶自定義的shardingAlgorithm初始化先於configMapContext. 所以我給他提前了.
  • 具體來說
    下面這是讀取配置的部分
@ConfigurationProperties(prefix = "sharding.jdbc.config")
public class SpringBootConfigMapConfigurationProperties {
    private Map<String, Object> configMap = new LinkedHashMap<>();
}

下面是修改部分.

@EnableConfigurationProperties({
        SpringBootShardingRuleConfigurationProperties.class, SpringBootMasterSlaveRuleConfigurationProperties.class,
        SpringBootConfigMapConfigurationProperties.class, SpringBootPropertiesConfigurationProperties.class
})
public class SpringBootConfiguration implements EnvironmentAware {
	//此處為spring讀取的sharding.jdbc.config配置.
	private final SpringBootConfigMapConfigurationProperties configMapProperties;
	@Bean
    public DataSource dataSource() throws SQLException {
    	// ------------------以下為手動添加的部分------------------
        if (!configMapProperties.getConfigMap().isEmpty()) {
            ConfigMapContext.getInstance().getConfigMap().putAll(configMapProperties.getConfigMap());
        }
        // ------------------以上為手動添加的部分------------------
		//shardingProperties.getShardingRuleConfiguration()會對ShardingAlgorithm進行實例化,此時configMapContetx尚未初始化.
        return null == masterSlaveProperties.getMasterDataSourceName()
                ? ShardingDataSourceFactory
                .createDataSource(dataSourceMap, shardingProperties.getShardingRuleConfiguration(), configMapProperties.getConfigMap(), propMapProperties.getProps())
                : MasterSlaveDataSourceFactory.createDataSource(
                dataSourceMap, masterSlaveProperties.getMasterSlaveRuleConfiguration(), configMapProperties.getConfigMap(), propMapProperties.getProps());
    }
}
/**
 *下面是ShardingDataSource的構造函數
 */
public class ShardingDataSource extends AbstractDataSourceAdapter {
	public ShardingDataSource(final Map<String, DataSource> dataSourceMap, final ShardingRule shardingRule, final Map<String, Object> configMap, final Properties props) throws SQLException {
        super(dataSourceMap);
        checkDataSourceType(dataSourceMap);
        //原來是在此處進行configMapContext的初始化.
        if (!configMap.isEmpty()) {
            ConfigMapContext.getInstance().getConfigMap().putAll(configMap);
        }
        shardingContext = new ShardingContext(getDataSourceMap(), shardingRule, getDatabaseType(), props);
    }
}

解釋:

原本ConfigMapContext讀取"sharding.jdbc.config"的配置是在ShradingDatasource初始化的時候注入的.但是ShardingDatasource初始化的時候先進行了ShardingRule的初始化(此時進行了ShardingAlgorithm的實例化),故在ShardingAlgorithm中初始化獲取ConfigMapContext的時候還沒有獲取到配置文件中的配置.

莫名的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中使用表名了.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM