讀前請注意
本文涉及的源碼修改並沒有經過嚴格的驗證或大量測試,謹慎參考.
另修改部分可在(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中使用表名了.