ShardingSphere 分库分表原理:SQL 解析与路由
深入剖析 Apache ShardingSphere 的 SQL 解析引擎与路由核心原理,从源码层面理解分库分表的技术实现
文章目录
- [ShardingSphere 分库分表原理:SQL 解析与路由](#ShardingSphere 分库分表原理:SQL 解析与路由)
-
- [一、ShardingSphere 分库分表架构概述](#一、ShardingSphere 分库分表架构概述)
-
- [1.1 核心架构](#1.1 核心架构)
- [1.2 执行流程全景图](#1.2 执行流程全景图)
- [二、SQL 解析引擎原理](#二、SQL 解析引擎原理)
-
- [2.1 基于 ANTLR 的解析器设计](#2.1 基于 ANTLR 的解析器设计)
- [2.2 抽象语法树(AST)的构建](#2.2 抽象语法树(AST)的构建)
- [2.3 SQLStatement 对象模型](#2.3 SQLStatement 对象模型)
- [2.4 SQL解析引擎对比](#2.4 SQL解析引擎对比)
- 三、路由引擎核心原理
-
- [3.1 路由流程详解](#3.1 路由流程详解)
- [3.2 分片策略与算法](#3.2 分片策略与算法)
- [3.3 路由结果优化](#3.3 路由结果优化)
- [3.4 路由类型对比](#3.4 路由类型对比)
- [四、SQL 改写引擎](#四、SQL 改写引擎)
-
- [4.1 改写的必要性](#4.1 改写的必要性)
- [4.2 分页改写原理](#4.2 分页改写原理)
- 五、执行引擎与结果归并
-
- [5.1 执行引擎架构](#5.1 执行引擎架构)
- [5.2 并行执行实现](#5.2 并行执行实现)
- [5.3 结果归并策略](#5.3 结果归并策略)
- 六、完整示例实战
-
- [6.1 配置分片规则](#6.1 配置分片规则)
- [6.2 SQL 执行流程演示](#6.2 SQL 执行流程演示)
- [6.3 性能优化建议](#6.3 性能优化建议)
- 七、总结
一、ShardingSphere 分库分表架构概述
Apache ShardingSphere 是一款分布式数据库中间件,通过SQL解析、路由、改写、执行和结果归并等核心流程,实现了对应用透明的分库分表能力。本文将深入分析其SQL解析与路由的核心原理。
1.1 核心架构
ShardingSphere 采用分层架构设计,主要包括以下层次:
应用层
ShardingSphere JDBC/Proxy
SQL解析层
路由层
改写层
执行层
归并层
ANTLR Parser
AST抽象语法树
SQLStatement
分片策略匹配
分片算法
路由结果
正确性改写
优化改写
多线程执行
连接池管理
流式归并
内存归并
1.2 执行流程全景图
结果归并 执行引擎 SQL改写器 路由引擎 SQL解析器 ShardingSphere 应用程序 结果归并 执行引擎 SQL改写器 路由引擎 SQL解析器 ShardingSphere 应用程序 发送SQL 解析SQL ANTLR生成AST 返回SQLStatement 匹配分片策略 执行分片算法 返回路由结果 SQL改写 返回改写后SQL 多线程执行 返回多个结果集 结果归并 返回最终结果
二、SQL 解析引擎原理
2.1 基于 ANTLR 的解析器设计
ShardingSphere 使用 ANTLR4 作为SQL解析器生成器,支持MySQL、PostgreSQL、Oracle、SQLServer等多种数据库方言。
核心源码分析 (ShardingSphere 5.5.3)
java
// SQLParserEntry.java (简化版)
public final class SQLParserEntry {
private final Cache<SQLParserCacheKey, SQLStatement> cache =
new ShardingSphereCache<>(1024);
/**
* 解析SQL为SQLStatement
* @param databaseType 数据库类型
* @param sql SQL语句
* @param useCache 是否使用缓存
* @return SQLStatement对象
*/
public SQLStatement parse(final DatabaseType databaseType,
final String sql,
final boolean useCache) {
// 1. 构建缓存Key
SQLParserCacheKey cacheKey = new SQLParserCacheKey(
databaseType, sql
);
// 2. 尝试从缓存获取
if (useCache) {
SQLStatement cached = cache.get(cacheKey);
if (cached != null) {
return cached;
}
}
// 3. 获取对应数据库的Parser
SQLParser sqlParser = SQLParserFactory.getInstance(
databaseType
);
// 4. 解析SQL生成AST
ParseTree parseTree = sqlParser.parse(sql);
// 5. 访问AST提取SQLStatement
SQLStatement result = sqlParser.visit(parseTree);
// 6. 存入缓存
if (useCache) {
cache.put(cacheKey, result);
}
return result;
}
}
2.2 抽象语法树(AST)的构建
ANTLR4解析器将SQL文本转换为ParseTree,然后通过Visitor模式遍历树结构提取关键信息。
ANTLR4 语法文件示例 (MySQL.g4 简化版)
antlr
// SELECT语句的语法规则
selectStatement
: querySpecification
| queryExpression
;
querySpecification
: SELECT selectSpec*
selectElements
(FROM fromClause)?
(WHERE whereClause)?
(GROUP BY groupByClause)?
(HAVING havingClause)?
(ORDER BY orderByClause)?
(limitClause)?
;
selectElements
: selectElement (',' selectElement)*
;
selectElement
: columnName // 列名
| functionName '(' columnName ')' // 函数
| '*' // 全部列
;
2.3 SQLStatement 对象模型
解析后的SQL被转换为Java对象模型,包含完整的SQL语义信息。
java
// SelectStatement.java (简化版)
public class SelectStatement implements SQLStatement {
// 包含的表信息
private final Collection<SimpleTableSegment> tables =
new LinkedList<>();
// 选择列信息
private final Collection<ProjectionSegment> projections =
new LinkedList<>();
// WHERE条件
private WhereSegment where;
// GROUP BY信息
private GroupBySegment groupBy;
// ORDER BY信息
private OrderBySegment orderBy;
// 分页信息
private LimitSegment limit;
// 判断是否包含分片键
public boolean containsShardingKey(
final ShardingColumn shardingColumn) {
// 在WHERE条件中查找
if (where != null &&
where.getConditions().contains(shardingColumn)) {
return true;
}
// 在JOIN条件中查找
for (SimpleTableSegment table : tables) {
if (table.getJoinConditions()
.contains(shardingColumn)) {
return true;
}
}
return false;
}
}
2.4 SQL解析引擎对比
| 特性 | ShardingSphere | MyCAT | Vitess |
|---|---|---|---|
| 解析器生成器 | ANTLR4 | 自定义Parser | 自定义Parser |
| 支持数据库 | MySQL/PG/Oracle/SQLServer | MySQL | MySQL |
| AST缓存 | 支持 | 不支持 | 支持 |
| 解析性能 | 高(多级缓存) | 中 | 高 |
| 扩展性 | SPI机制 | 代码侵入 | Plugin机制 |
三、路由引擎核心原理
3.1 路由流程详解
路由引擎负责根据解析上下文和分片策略,生成目标数据源和表的映射关系。
路由核心流程
是
否
Hint
SQLStatement
包含分片键?
标准路由
全库表路由
Hint路由
提取分片键值
应用分片算法
生成路由单元
遍历所有数据源
遍历所有表
从Hint提取值
优化路由结果
返回最终路由
3.2 分片策略与算法
ShardingSphere 内置分片算法对比
| 算法名称 | 类型 | 特点 | 适用场景 |
|---|---|---|---|
| StandardShardingAlgorithm | 标准分片 | 支持=、IN操作符 | 单分片键精确分片 |
| ComplexShardingAlgorithm | 复杂分片 | 支持多分片键组合 | 多键组合分片 |
| HintShardingAlgorithm | Hint分片 | 通过Hint指定路由 | 强制路由到指定节点 |
| AutoTableShardingAlgorithm | 自动分片 | 自动配置分表数量 | 快速分表场景 |
标准分片算法实现源码
java
// ModShardingAlgorithm.java - 取模分片算法
public class ModShardingAlgorithm
implements StandardShardingAlgorithm<Long> {
private Properties properties = new Properties();
/**
* 精确分片 - 处理 = 和 IN
* @param availableTargetNames 可用的数据源/表名
* @param shardingValue 分片键值
* @return 目标数据源/表名
*/
@Override
public String doSharding(
final Collection<String> availableTargetNames,
final PreciseShardingValue<Long> shardingValue) {
// 1. 获取分片总数
int shardingCount = availableTargetNames.size();
// 2. 获取分片键值
Long shardingKeyValue = shardingValue.getValue();
// 3. 计算取模
long modResult = shardingKeyValue % shardingCount;
// 4. 转换为非负数
long index = modResult >= 0 ? modResult :
modResult + shardingCount;
// 5. 转换为目标表名
String targetName = "table_" + index;
for (String available : availableTargetNames) {
if (available.endsWith(String.valueOf(index))) {
return available;
}
}
throw new UnsupportedOperationException(
"无法路由到目标: " + shardingValue
);
}
/**
* 范围分片 - 处理 BETWEEN、>、<、>=、<=
*/
@Override
public Collection<String> doSharding(
final Collection<String> availableTargetNames,
final RangeShardingValue<Long> shardingValue) {
Collection<String> result = new LinkedHashSet<>();
Range<Long> range = shardingValue.getValueRange();
int shardingCount = availableTargetNames.size();
// 遍历所有可用目标
for (String each : availableTargetNames) {
// 提取表后缀作为索引
int index = extractIndex(each);
// 判断是否在范围内
if (isInRange(index, range, shardingCount)) {
result.add(each);
}
}
return result;
}
private int extractIndex(String tableName) {
String suffix = tableName.substring(
tableName.lastIndexOf('_') + 1
);
return Integer.parseInt(suffix);
}
private boolean isInRange(
int index, Range<Long> range,
int shardingCount) {
// 简化版:检查索引是否可能匹配范围
return range.lowerEndpoint() % shardingCount <= index
&& index <= range.upperEndpoint() % shardingCount;
}
@Override
public String getType() {
return "MOD";
}
@Override
public void init(final Properties props) {
this.properties = props;
}
}
3.3 路由结果优化
路由单元去重与合并
java
// RouteUnit.java (简化版)
public final class RouteUnit {
// 数据源名称
private final String dataSourceName;
// 该数据源上的路由单元集合
private final Collection<RouteMapper> routeMappers;
/**
* 检查是否可以合并
*/
public boolean canMerge(final RouteUnit other) {
// 只有数据源相同才能合并
return this.dataSourceName.equals(
other.getDataSourceName()
);
}
/**
* 合并路由单元
*/
public RouteUnit merge(final RouteUnit other) {
Collection<RouteMapper> mergedMappers =
new LinkedList<>(routeMappers);
// 添加新的路由单元
for (RouteMapper mapper : other.getRouteMappers()) {
// 去重:已存在的不再添加
if (!mergedMappers.contains(mapper)) {
mergedMappers.add(mapper);
}
}
return new RouteUnit(dataSourceName, mergedMappers);
}
}
3.4 路由类型对比
| 路由类型 | 分片键 | 性能 | 复杂度 | 场景 |
|---|---|---|---|---|
| 标准路由 | 需要分片键 | ⭐⭐⭐⭐⭐ | 低 | 按用户ID、订单ID分片 |
| 全库表路由 | 无分片键 | ⭐ | 低 | 广播表、全表扫描 |
| 笛卡尔路由 | 多表关联 | ⭐⭐ | 高 | 未绑定表关系 |
| Hint路由 | Hint指定 | ⭐⭐⭐⭐ | 低 | 强制指定分片 |
四、SQL 改写引擎
4.1 改写的必要性
路由后的SQL无法直接在真实数据库执行,需要改写:
- 表名改写:逻辑表改为真实表名
- 列名补充:补充分片键用于结果归并
- 分页修正:修正OFFSET和LIMIT
- 排序改写:添加分片键到ORDER BY
表名改写源码
java
// TableTokenGenerateEngine.java
public final class TableTokenGenerateEngine {
/**
* 生成表名改写Token
* @param sqlSegments SQL片段集合
* @param routeUnits 路由单元
* @return 表名Token集合
*/
public Collection<TableToken> generateTableTokens(
final Collection<SQLSegment> sqlSegments,
final Collection<RouteUnit> routeUnits) {
Collection<TableToken> result = new LinkedList<>();
for (RouteUnit each : routeUnits) {
for (RouteMapper mapper : each.getRouteMappers()) {
// 遍历SQL片段
for (SQLSegment segment : sqlSegments) {
// 找到表名片段
if (segment instanceof TableSegment) {
TableSegment tableSegment =
(TableSegment) segment;
// 生成Token
TableToken token = new TableToken(
tableSegment.getStartIndex(),
tableSegment.getStopIndex(),
mapper.getActualTable()
);
result.add(token);
}
}
}
}
return result;
}
}
// SQLRewriteEngine.java
public final class SQLRewriteEngine {
/**
* 执行SQL改写
* @param sql 原始SQL
* @param tokens Token集合
* @return 改写后的SQL
*/
public String rewrite(
final String sql,
final Collection<SQLToken> tokens) {
StringBuilder result = new StringBuilder(sql);
// 按照Token位置倒序替换
List<SQLToken> sortedTokens = new ArrayList<>(tokens);
Collections.sort(sortedTokens,
Comparator.comparingInt(SQLToken::getStartIndex)
.reversed()
);
for (SQLToken each : sortedTokens) {
// 替换表名
if (each instanceof TableToken) {
TableToken tableToken = (TableToken) each;
result.replace(
tableToken.getStartIndex(),
tableToken.getStopIndex() + 1,
tableToken.getActualTableName()
);
}
}
return result.toString();
}
}
4.2 分页改写原理
当查询包含分页时,需要在每个分片查询更多数据,最终归并时再截取。
分页改写示例
java
// LimitClauseRewriteEngine.java
public final class LimitClauseRewriteEngine {
/**
* 改写分页参数
* @param originalLimit 原始LIMIT
* @param routeCount 路由数量
* @return 改写后的LIMIT
*/
public LimitClause rewrite(
final LimitClause originalLimit,
final int routeCount) {
if (routeCount == 1) {
return originalLimit;
}
// 原始参数
long originalOffset = originalLimit.getOffset();
long originalRowCount = originalLimit.getRowCount();
// 改写后参数
long rewrittenOffset = 0; // 每个分片都从0开始
long rewrittenRowCount = originalOffset + originalRowCount;
// 如果是最大值,表示不分页
if (rewrittenRowCount == Long.MAX_VALUE) {
return originalLimit;
}
return new LimitClause(rewrittenOffset, rewrittenRowCount);
}
}
分页改写对比表
| 场景 | 原始SQL | 改写后SQL (假设分2片) |
|---|---|---|
| 基础分页 | SELECT * FROM t_user LIMIT 10, 5 |
SELECT * FROM t_user_0 LIMIT 0, 15 SELECT * FROM t_user_1 LIMIT 0, 15 |
| 首页查询 | SELECT * FROM t_user LIMIT 0, 10 |
SELECT * FROM t_user_0 LIMIT 0, 10 SELECT * FROM t_user_1 LIMIT 0, 10 |
| 深度分页 | SELECT * FROM t_user LIMIT 100000, 10 |
SELECT * FROM t_user_0 LIMIT 0, 100010 SELECT * FROM t_user_1 LIMIT 0, 100010 |
五、执行引擎与结果归并
5.1 执行引擎架构
改写后SQL
ExecutionEngine
PreparedStatementExecutor
BatchPreparedStatementExecutor
StatementExecutor
连接池
DataSource_0
DataSource_1
DataSource_2
并行执行
ExecutorService
5.2 并行执行实现
java
// DriverExecutionExecuteEngine.java
public final class DriverExecutionExecuteEngine {
private final ExecutorService executorService;
/**
* 执行SQL并返回结果集
* @param routeUnits 路由单元
* @param sqlUnit SQL单元
* @return 执行结果集合
*/
public Collection<ExecuteResult> execute(
final Collection<RouteUnit> routeUnits,
final SQLUnit sqlUnit) {
// 1. 创建执行任务列表
List<ExecuteTask> tasks = new LinkedList<>();
for (RouteUnit each : routeUnits) {
// 为每个路由单元创建任务
ExecuteTask task = new ExecuteTask(
each.getDataSourceName(),
each.getSqlUnits().iterator().next(),
connectionManager
);
tasks.add(task);
}
// 2. 并行执行所有任务
List<Future<ExecuteResult>> futures =
new ArrayList<>(tasks.size());
for (ExecuteTask each : tasks) {
futures.add(executorService.submit(each));
}
// 3. 收集结果
Collection<ExecuteResult> result = new LinkedList<>();
for (Future<ExecuteResult> each : futures) {
try {
result.add(each.get());
} catch (InterruptedException |
ExecutionException ex) {
throw new ShardingException(ex);
}
}
return result;
}
}
5.3 结果归并策略
归并引擎对比
| 归并类型 | 内存占用 | 性能 | 支持操作 |
|---|---|---|---|
| 流式归并 | 低 | 高 | ORDER BY(包含分片键) |
| 内存归并 | 高 | 低 | ORDER BY(不含分片键) |
| 聚合归并 | 中 | 中 | SUM、COUNT、AVG等 |
| 分组归并 | 高 | 低 | GROUP BY |
流式归并实现
java
// StreamMergedResult.java
public final class StreamMergedResult
implements MergedResult {
private final Queue<ResultSetQueue> resultSets;
/**
* 构造流式归并结果
* @param resultSets 结果集队列
*/
public StreamMergedResult(
final Queue<ResultSetQueue> resultSets) {
this.resultSets = resultSets;
}
@Override
public boolean next() throws SQLException {
// 从队列中获取下一个结果
ResultSetQueue currentResultSet = resultSets.peek();
if (null == currentResultSet) {
return false;
}
// 移动游标
if (currentResultSet.next()) {
return true;
}
// 当前结果集耗尽,移除并尝试下一个
resultSets.poll();
return next();
}
@Override
public Object getValue(final int columnIndex,
final Class<?> type)
throws SQLException {
ResultSetQueue current = resultSets.peek();
if (null == current) {
return null;
}
return current.getValue(columnIndex, type);
}
}
内存归并实现
java
// MemoryMergedResult.java
public final class MemoryMergedResult
implements MergedResult {
private final List<MemoryResultSet> resultSets;
/**
* 内存归并:先加载所有数据到内存,再排序
* @param resultSets 所有结果集
* @param orderByItem 排序项
*/
public MemoryMergedResult(
final List<ResultSet> resultSets,
final OrderByItem orderByItem) throws SQLException {
// 1. 加载所有数据到内存
List<MemoryResultSet> loadedResults =
new ArrayList<>(resultSets.size());
for (ResultSet each : resultSets) {
MemoryResultSet memoryResultSet =
new MemoryResultSet(each);
memoryResultSet.loadAll(); // 全部加载到内存
loadedResults.add(memoryResultSet);
}
// 2. 排序
Collections.sort(loadedResults,
new ResultSetComparator(orderByItem));
this.resultSets = loadedResults;
}
@Override
public boolean next() throws SQLException {
for (MemoryResultSet each : resultSets) {
if (each.next()) {
return true;
}
}
return false;
}
}
六、完整示例实战
6.1 配置分片规则
yaml
# sharding.yaml
mode:
type: Standalone
repository:
type: JDBC
dataSources:
ds_0:
dataSourceClassName:
com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.jdbc.Driver
jdbcUrl: jdbc:mysql://localhost:3306/demo_ds_0
username: root
password: root
ds_1:
dataSourceClassName:
com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.jdbc.Driver
jdbcUrl: jdbc:mysql://localhost:3306/demo_ds_1
username: root
password: root
rules:
- !SHARDING
tables:
t_order:
actualDataNodes: ds_${0..1}.t_order_${0..1}
databaseStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: db_mod
tableStrategy:
standard:
shardingColumn: order_id
shardingAlgorithmName: table_mod
shardingAlgorithms:
db_mod:
type: MOD
props:
sharding-count: 2
table_mod:
type: MOD
props:
sharding-count: 2
props:
sql-show: true
6.2 SQL 执行流程演示
java
// 应用代码
public class OrderService {
@Resource
private JdbcTemplate jdbcTemplate;
/**
* 查询订单
*/
public List<Order> queryOrders(
final Long userId,
final Long orderId) {
// 原始SQL
String sql =
"SELECT order_id, user_id, status " +
"FROM t_order " +
"WHERE user_id = ? AND order_id = ?";
// ShardingSphere 执行流程:
//
// 1. SQL解析
// 输入: SELECT order_id, user_id, status
// FROM t_order
// WHERE user_id = ? AND order_id = ?
// 输出: SelectStatement {
// tables: [t_order],
// where: {
// user_id = 1001,
// order_id = 2001
// },
// parameters: [1001, 2001]
// }
//
// 2. 路由计算
// user_id = 1001 % 2 = 1 -> ds_1
// order_id = 2001 % 2 = 1 -> t_order_1
// 路由结果: ds_1.t_order_1
//
// 3. SQL改写
// 改写后: SELECT order_id, user_id, status
// FROM t_order_1
// WHERE user_id = 1001 AND order_id = 2001
//
// 4. 执行SQL
// 在 ds_1 上执行改写后的SQL
//
// 5. 结果归并
// 单个结果集,直接返回
return jdbcTemplate.query(
sql,
new Object[]{userId, orderId},
(rs, rowNum) -> Order.builder()
.orderId(rs.getLong("order_id"))
.userId(rs.getLong("user_id"))
.status(rs.getString("status"))
.build()
);
}
}
6.3 性能优化建议
| 优化项 | 说明 | 建议 |
|---|---|---|
| SQL解析缓存 | 启用可提升解析性能 | 默认开启,缓存大小1024 |
| 路由结果缓存 | 相同SQL复用路由结果 | 适合读多写少场景 |
| 并行执行线程池 | 控制并发度 | 设置为CPU核心数*2 |
| 连接池配置 | 避免连接泄露 | HikariCP默认配置即可 |
| 广播表 | 小表广播到所有节点 | 适用于字典表、配置表 |
七、总结
本文深入分析了ShardingSphere的SQL解析与路由核心原理:
-
SQL解析引擎:基于ANTLR4生成AST,通过Visitor模式提取SQL语义,支持多级缓存提升性能。
-
路由引擎:根据分片键值和分片策略计算目标数据源和表,支持标准路由、全库表路由、Hint路由等多种路由类型。
-
SQL改写:将逻辑SQL改写为可在真实数据库执行的SQL,包括表名改写、列名补充、分页修正等。
-
执行引擎:采用多线程并行执行,通过连接池管理数据库连接,提升执行效率。
-
结果归并:根据SQL类型选择流式归并或内存归并,支持排序、分组、聚合等多种归并策略。
ShardingSphere通过上述核心流程,实现了对应用透明的分库分表能力,为分布式数据库中间件提供了优秀的解决方案。
参考资料:
- Apache ShardingSphere 官方文档: https://shardingsphere.apache.org/
- ShardingSphere 源码 (5.5.3): https://github.com/apache/shardingsphere
- ANTLR4 官方文档: https://antlr.org/
相关标签:
ShardingSphere 分库分表 SQL解析 路由 分布式数据库 数据库中间件