1. 概述
本文分享分表分庫路由相關(guān)的實(shí)現(xiàn)辆飘。涉及內(nèi)容如下:
- SQL 路由器
- 路由引擎
- SQL 路由結(jié)果
SQL 路由大體流程如下:
第二個步驟其實(shí)是 SQL 的解析過程份殿,在上一篇<<SQL 解析>>已經(jīng)介紹了。所以具滴,嚴(yán)格來說,SQL 解析過程是在路由階段里的。
2. SQL 路由器
SQLRouter毙籽,SQL 路由器接口,共有兩種實(shí)現(xiàn):
- DatabaseHintSQLRouter:通過提示且僅路由至數(shù)據(jù)庫的SQL路由器
- ParsingSQLRouter:需要解析的SQL路由器
它們實(shí)現(xiàn) #parse() 進(jìn)行 SQL 解析毡庆, #route() 進(jìn)行 SQL 路由坑赡。
2.1 DatabaseHintSQLRouter
DatabaseHintSQLRouter,基于數(shù)據(jù)庫提示的路由引擎么抗。路由器工廠 SQLRouterFactory 創(chuàng)建路由器時毅否,判斷到使用數(shù)據(jù)庫提示( Hint ) 時,創(chuàng)建 DatabaseHintSQLRouter蝇刀。
// DatabaseHintRoutingEngine.java
public static SQLRouter createSQLRouter(final ShardingContext shardingContext) {
return HintManagerHolder.isDatabaseShardingOnly() ? new DatabaseHintSQLRouter(shardingContext) : new ParsingSQLRouter(shardingContext);
}
先來看下 HintManagerHolder螟加、HintManager 部分相關(guān)的代碼:
public final class HintManagerHolder {
public static final String DB_TABLE_NAME = "DB_TABLE_NAME";
public static final String DB_COLUMN_NAME = "DB_COLUMN_NAME";
private static final ThreadLocal<HintManager> HINT_MANAGER_HOLDER = new ThreadLocal<>();
/**
* 判斷是否當(dāng)前只分庫.
*
* @return database sharding only or not
*/
public static boolean isDatabaseShardingOnly() {
return null != HINT_MANAGER_HOLDER.get() && HINT_MANAGER_HOLDER.get().isDatabaseShardingOnly();
}
/**
* 清理線索分片管理器的本地線程持有者.
*/
public static void clear() {
HINT_MANAGER_HOLDER.remove();
}
/**
* Get hint manager in current thread.
*
* @return hint manager in current thread
*/
public static HintManager get() {
return HINT_MANAGER_HOLDER.get();
}
}
public final class HintManager implements AutoCloseable {
private final Map<ShardingKey, ShardingValue> databaseShardingValues = new HashMap<>();
@Getter
private boolean databaseShardingOnly;
/**
* 獲取線索分片管理器實(shí)例.
*
* @return {@code HintManager} instance
*/
public static HintManager getInstance() {
HintManager result = new HintManager();
HintManagerHolder.setHintManager(result);
return result;
}
/**
* 設(shè)置分庫分片值.
*
* <p>該方法適用于只分庫的場景</p>
*
* @param value sharding value
*/
public void setDatabaseShardingValue(final Comparable<?> value) {
databaseShardingOnly = true;
addDatabaseShardingValue(HintManagerHolder.DB_TABLE_NAME, HintManagerHolder.DB_COLUMN_NAME, value);
}
}
那么如果要使用 DatabaseHintSQLRouter,我們只需要 HintManager.getInstance().setDatabaseShardingValue(庫分片值) 即可吞琐。這里有兩點(diǎn)要注意下:
- HintManager#getInstance()捆探,每次獲取到的都是新的 HintManager,多次賦值需要小心站粟。
- HintManager#close()黍图,使用完需要去清理,避免下個請求讀到遺漏的線程變量奴烙。
Hint 方式主要使用場景:
- 分片字段不存在SQL助被、數(shù)據(jù)庫表結(jié)構(gòu)中,而存在于外部業(yè)務(wù)邏輯切诀。
- 強(qiáng)制在主庫進(jìn)行某些數(shù)據(jù)操作揩环。
2.2 ParsingSQLRouter
在我們平常的業(yè)務(wù)場景中,使用最多的是分片字段會在 SQL幅虑、數(shù)據(jù)庫表結(jié)構(gòu)中丰滑,其將采用 ParsingSQLRouter 進(jìn)行 SQL 的解析和路由。
ParsingSQLRouter 使用 SQLParsingEngine 解析SQL(上篇文章已經(jīng)介紹)翘单。
ParsingSQLRouter 在路由時吨枉,會根據(jù)表情況使用 SimpleRoutingEngine 或 CartesianRoutingEngine 進(jìn)行路由ParsingSQLRouter#route(parameters, sqlStatement)
:
private RoutingResult route(final List<Object> parameters, final SQLStatement sqlStatement) {
Collection<String> tableNames = sqlStatement.getTables().getTableNames();
RoutingEngine routingEngine;
if (sqlStatement instanceof DDLStatement) {
// DDL 表定義語言蹦渣,如 CREATE、ALTER 等操作
routingEngine = new DDLRoutingEngine(shardingRule, parameters, (DDLStatement) sqlStatement);
} else if (tableNames.isEmpty()) {
routingEngine = new DatabaseAllRoutingEngine(shardingRule.getDataSourceMap());
} else if (1 == tableNames.size() || shardingRule.isAllBindingTables(tableNames) || shardingRule.isAllInDefaultDataSource(tableNames)) {
// 簡單路由引擎
routingEngine = new SimpleRoutingEngine(shardingRule, parameters, tableNames.iterator().next(), sqlStatement);
} else {
// 混合路由引擎
routingEngine = new ComplexRoutingEngine(shardingRule, parameters, tableNames, sqlStatement);
}
return routingEngine.route();
}
- 當(dāng)是 DDL 語句時貌亭,采用
DDLRoutingEngine
進(jìn)行路由柬唯。 - 當(dāng)只有一個表名或者多表互為 BindingTable 關(guān)系時,就會采用
SimpleRoutingEngine
進(jìn)行路由圃庭。 - 其他情況(多庫多表情況)采用混合路由引擎
ComplexRoutingEngine
锄奢。
BindingTable 關(guān)系在 ShardingRule 的 tableRules 配置。配置該關(guān)系 TableRule 有如下需要遵守的規(guī)則:
- 分片策略與算法相同
- 數(shù)據(jù)源配置對象相同
- 真實(shí)表數(shù)量相同
2.3 SimpleRoutingEngine
SimpleRoutingEngine剧腻,簡單路由引擎。
// SimpleRoutingEngine.java
public RoutingResult route() {
// 1. 獲取分表規(guī)則
TableRule tableRule = shardingRule.getTableRule(logicTableName);
// 2. 獲取分庫值
List<ShardingValue> databaseShardingValues = getDatabaseShardingValues(tableRule);
// 3. 獲取分表值
List<ShardingValue> tableShardingValues = getTableShardingValues(tableRule);
// 4. 路由數(shù)據(jù)庫
Collection<String> routedDataSources = routeDataSources(tableRule, databaseShardingValues);
Collection<DataNode> routedDataNodes = new LinkedList<>();
for (String each : routedDataSources) {
// 5. 路由表
routedDataNodes.addAll(routeTables(tableRule, each, tableShardingValues));
}
// 6. 生成路由結(jié)果 RoutingResult
return generateRoutingResult(routedDataNodes);
}
第一步书在,根據(jù) SQL 的邏輯表 logicTableName 獲取分表規(guī)則(客戶端配置的分片規(guī)則)tableRule栏账。我們看 TableRule 的組成:
public final class TableRule {
// 邏輯表
private final String logicTable;
// 節(jié)點(diǎn)集合
private final List<DataNode> actualDataNodes;
// 數(shù)據(jù)庫分片策略
private final ShardingStrategy databaseShardingStrategy;
// 表分片策略
private final ShardingStrategy tableShardingStrategy;
// 自增主鍵字段
private final String generateKeyColumn;
// 自增器,默認(rèn)的自增器采用 snowflake
private final KeyGenerator keyGenerator;
private final String logicIndex;
}
第二步栈源,根據(jù) tableRule 獲取分庫值getDatabaseShardingValues(tableRule)
:
// SimpleRoutingEngine.java
private List<ShardingValue> getDatabaseShardingValues(final TableRule tableRule) {
ShardingStrategy strategy = shardingRule.getDatabaseShardingStrategy(tableRule);
return HintManagerHolder.isUseShardingHint() ? getDatabaseShardingValuesFromHint(strategy.getShardingColumns()) : getShardingValues(strategy.getShardingColumns());
}
private List<ShardingValue> getShardingValues(final Collection<String> shardingColumns) {
List<ShardingValue> result = new ArrayList<>(shardingColumns.size());
for (String each : shardingColumns) {
Optional<Condition> condition = sqlStatement.getConditions().find(new Column(each, logicTableName));
if (condition.isPresent()) {
result.add(condition.get().getShardingValue(parameters));
}
}
return result;
}
// ShardingRule.java
public ShardingStrategy getDatabaseShardingStrategy(final TableRule tableRule) {
return null == tableRule.getDatabaseShardingStrategy() ? defaultDatabaseShardingStrategy : tableRule.getDatabaseShardingStrategy();
}
該方法會從TableRule
獲取分庫策略挡爵,如果為空,則使用默認(rèn)的分庫策略(需要客戶端配置)甚垦,如果未配置默認(rèn)的分庫策略茶鹃,則使用NoneShardingStrategy
,標(biāo)明不使用任何分片策略艰亮。
關(guān)于 ShardingStrategy 的介紹闭翩,可以參考我的另一篇文章《分庫分表中間件 Sharding-JDBC》。
其中兩個比較重要的分片策略是 StandardShardingStrategy 和 ComplexShardingStrategy垃杖。前者針對是單個分片鍵男杈,后者針對的是多個分片鍵丈屹。
拿到 ShardingStrategy 之后调俘,繼續(xù)判斷,如果采用的時 Hint 分片方式旺垒,則從 Hint 中獲取分庫值彩库;如果非 Hint 方式,則根據(jù)分片鍵和邏輯表找到對應(yīng)的Condition
先蒋,我們看到了《SQL解析》分享的Condition
對象骇钦。之前我們提到過 Parser 半理解 SQL 的目的之一是:提煉分片上下文,此處即是該目的的體現(xiàn)竞漾。
接著眯搭,根據(jù)Condition
的 ShardingOperator 屬性窥翩,來創(chuàng)建不同的ShardingValue
:
// Condition.java
public ShardingValue getShardingValue(final List<Object> parameters) {
List<Comparable<?>> conditionValues = getValues(parameters);
switch (operator) {
case EQUAL:
case IN:
return new ListShardingValue<>(column.getTableName(), column.getName(), conditionValues);
case BETWEEN:
return new RangeShardingValue<>(column.getTableName(), column.getName(), Range.range(conditionValues.get(0), BoundType.CLOSED, conditionValues.get(1), BoundType.CLOSED));
default:
throw new UnsupportedOperationException(operator.getExpression());
}
}
分片操作為 = 或者 in 時,創(chuàng)建ListShardingValue
對象鳞仙。
分片操作為 Between 時寇蚊,創(chuàng)建RangeShardingValue
對象。
- ListShardingValue
public final class ListShardingValue<T extends Comparable<?>> implements ShardingValue {
// 邏輯表
private final String logicTableName;
// 分片字段
private final String columnName;
// 分片字段的值棍好,集合
private final Collection<T> values;
}
- RangeShardingValue
public final class RangeShardingValue<T extends Comparable<?>> implements ShardingValue {
private final String logicTableName;
private final String columnName;
// 分片字段的值仗岸,區(qū)間
private final Range<T> valueRange;
}
第三步,獲取表分片值借笙。跟第二步是類似的扒怖,這里就不再贅述了。
第四步业稼,根據(jù)數(shù)據(jù)庫分片值和分片規(guī)則盗痒,進(jìn)行數(shù)據(jù)庫路由操作routeDataSources
:
private Collection<String> routeDataSources(final TableRule tableRule, final List<ShardingValue> databaseShardingValues) {
// 獲取實(shí)際節(jié)點(diǎn)的數(shù)據(jù)庫集合
Collection<String> availableTargetDatabases = tableRule.getActualDatasourceNames();
if (databaseShardingValues.isEmpty()) {
return availableTargetDatabases;
}
// 根據(jù)數(shù)據(jù)庫分片值進(jìn)行分片操作
Collection<String> result = shardingRule.getDatabaseShardingStrategy(tableRule).doSharding(availableTargetDatabases, databaseShardingValues);
Preconditions.checkState(!result.isEmpty(), "no database route info");
return result;
}
主要的路由操作是在ShardingStrategy#doSharding
方法,這里我們以單個分片鍵為例低散,其實(shí)現(xiàn)類為StandardShardingStrategy
:
public final class StandardShardingStrategy implements ShardingStrategy {
// 分片字段
private final String shardingColumn;
// 精確分片算法
private final PreciseShardingAlgorithm preciseShardingAlgorithm;
// 區(qū)間分片算法
private final Optional<RangeShardingAlgorithm> rangeShardingAlgorithm;
public StandardShardingStrategy(final String shardingColumn, final PreciseShardingAlgorithm preciseShardingAlgorithm) {
this(shardingColumn, preciseShardingAlgorithm, null);
}
public StandardShardingStrategy(final String shardingColumn, final PreciseShardingAlgorithm preciseShardingAlgorithm, final RangeShardingAlgorithm rangeShardingAlgorithm) {
this.shardingColumn = shardingColumn;
this.preciseShardingAlgorithm = preciseShardingAlgorithm;
this.rangeShardingAlgorithm = Optional.fromNullable(rangeShardingAlgorithm);
}
@Override
public Collection<String> doSharding(final Collection<String> availableTargetNames, final Collection<ShardingValue> shardingValues) {
// 獲取分片值(就一個)
ShardingValue shardingValue = shardingValues.iterator().next();
// 分片操作
Collection<String> shardingResult = shardingValue instanceof ListShardingValue
? doSharding(availableTargetNames, (ListShardingValue) shardingValue) : doSharding(availableTargetNames, (RangeShardingValue) shardingValue);
Collection<String> result = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
result.addAll(shardingResult);
return result;
}
private Collection<String> doSharding(final Collection<String> availableTargetNames, final ListShardingValue<?> shardingValue) {
Collection<String> result = new LinkedList<>();
for (PreciseShardingValue<?> each : transferToPreciseShardingValues(shardingValue)) {
// 調(diào)用分片算法(精確/區(qū)間)的分片操作
result.add(preciseShardingAlgorithm.doSharding(availableTargetNames, each));
}
return result;
}
...
}
StandardShardingStrategy 分片的時候积糯,是調(diào)用其PreciseShardingAlgorithm
或者 RangeShardingAlgorithm
對象的 doSharding
方法進(jìn)行分片的。而我們客戶端配置分片算法的時候谦纱,就是實(shí)現(xiàn)了以上算法接口的看成。
第五步,遍歷數(shù)據(jù)庫路由結(jié)果跨嘉,對每一個數(shù)據(jù)庫進(jìn)行表的分片路由routeTables
:
// SimpleRoutingEngine.java
private Collection<DataNode> routeTables(final TableRule tableRule, final String routedDataSource, final List<ShardingValue> tableShardingValues) {
Collection<String> availableTargetTables = tableRule.getActualTableNames(routedDataSource);
Collection<String> routedTables = tableShardingValues.isEmpty() ? availableTargetTables
: shardingRule.getTableShardingStrategy(tableRule).doSharding(availableTargetTables, tableShardingValues);
Preconditions.checkState(!routedTables.isEmpty(), "no table route info");
Collection<DataNode> result = new LinkedList<>();
for (String each : routedTables) {
result.add(new DataNode(routedDataSource, each));
}
return result;
}
表的分片和數(shù)據(jù)庫的分片的邏輯是一樣的川慌,分片完成之后,將每個分片結(jié)果封裝在DataNode
對象中:
public class DataNode {
private static final String DELIMITER = ".";
// 數(shù)據(jù)庫
private final String dataSourceName;
// 表
private final String tableName;
}
第六步祠乃,將 DataNode 集合封裝成路由結(jié)果RoutingResult
:
private RoutingResult generateRoutingResult(final Collection<DataNode> routedDataNodes) {
RoutingResult result = new RoutingResult();
for (DataNode each : routedDataNodes) {
result.getTableUnits().getTableUnits().add(new TableUnit(each.getDataSourceName(), logicTableName, each.getTableName()));
}
return result;
}
RoutingResult
存放了TableUnits
梦重,其是TableUnit
的集合對象。該方法將分片出來的數(shù)據(jù)庫信息和表信息存入在TableUnit
中亮瓷,供后續(xù)改寫 SQL 用琴拧。
2.4 ComplexRoutingEngine
ComplexRoutingEngine,混合多庫多表路由引擎嘱支。
public final class ComplexRoutingEngine implements RoutingEngine {
private final ShardingRule shardingRule;
private final List<Object> parameters;
private final Collection<String> logicTables;
private final SQLStatement sqlStatement;
@Override
public RoutingResult route() {
Collection<RoutingResult> result = new ArrayList<>(logicTables.size());
Collection<String> bindingTableNames = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
// 遍歷邏輯表集合
for (String each : logicTables) {
Optional<TableRule> tableRule = shardingRule.tryFindTableRule(each);
if (tableRule.isPresent()) {
if (!bindingTableNames.contains(each)) {
// 有 BindingTable 關(guān)系的蚓胸,走普通路由引擎
result.add(new SimpleRoutingEngine(shardingRule, parameters, tableRule.get().getLogicTable(), sqlStatement).route());
}
Optional<BindingTableRule> bindingTableRule = shardingRule.findBindingTableRule(each);
if (bindingTableRule.isPresent()) {
bindingTableNames.addAll(Lists.transform(bindingTableRule.get().getTableRules(), new Function<TableRule, String>() {
@Override
public String apply(final TableRule input) {
return input.getLogicTable();
}
}));
}
}
}
log.trace("mixed tables sharding result: {}", result);
if (result.isEmpty()) {
throw new ShardingJdbcException("Cannot find table rule and default data source with logic tables: '%s'", logicTables);
}
if (1 == result.size()) {
return result.iterator().next();
}
// 如果有多個路由結(jié)果,則走笛卡爾積的路由引擎
return new CartesianRoutingEngine(result).route();
}
}
-
ComplexRoutingEngine 計算每個邏輯表的簡單路由分片除师,路由結(jié)果交給 CartesianRoutingEngine 繼續(xù)路由形成笛卡爾積結(jié)果沛膳。
由于在 ComplexRoutingEngine 路由前已經(jīng)判斷全部表互為 BindingTable 關(guān)系,因而不會出現(xiàn) result.size == 1汛聚,屬于防御性編程锹安。
部分表互為 BindingTable 關(guān)系時,ComplexRoutingEngine 不重復(fù)計算分片。
2.5 CartesianRoutingEngine
CartesianRoutingEngine叹哭,笛卡爾積的庫表路由忍宋。
public CartesianRoutingResult route() {
CartesianRoutingResult result = new CartesianRoutingResult();
// 根據(jù)路由結(jié)果獲取 dataSourceLogicTablesMap:key 為 數(shù)據(jù)庫名,value 為表名集合
for (Entry<String, Set<String>> entry : getDataSourceLogicTablesMap().entrySet()) {
// 獲得當(dāng)前數(shù)據(jù)源(庫)的實(shí)際表分組
List<Set<String>> actualTableGroups = getActualTableGroups(entry.getKey(), entry.getValue());
// 獲得當(dāng)前數(shù)據(jù)源(庫)的路由表單元分組
List<Set<TableUnit>> tableUnitGroups = toTableUnitGroups(entry.getKey(), actualTableGroups);
// 對路由表單元分組進(jìn)行笛卡爾積风罩,并合并到路由結(jié)果
result.merge(entry.getKey(), getCartesianTableReferences(Sets.cartesianProduct(tableUnitGroups)));
}
log.trace("cartesian tables sharding result: {}", result);
return result;
}
第一步讶踪,獲得同庫對應(yīng)的邏輯表集合,即 Entry<數(shù)據(jù)源(庫), Set<邏輯表>> entry泊交。
第二步乳讥,遍歷數(shù)據(jù)源(庫),獲得當(dāng)前數(shù)據(jù)源(庫)的路由表單元分組廓俭。
第三步云石,對路由表單元分組進(jìn)行笛卡爾積,并合并到路由結(jié)果研乒。
注意:同庫才可以進(jìn)行笛卡爾積汹忠。
3. 結(jié)語
由于篇幅關(guān)系,本文并未對笛卡爾積的路由展開說雹熬,感興趣的同學(xué)可以自行去了解宽菜。
經(jīng)過路由之后,得到的RoutingResult
對象竿报,會在下一篇文章 SQL 改寫中用到它铅乡,盡請關(guān)注~