Sharding-JDBC详解:掌握MySQL分库分表精髓
Sharding-JDBC详解:掌握MySQL分库分表精髓
在互联网大规模应用场景下,单一 MySQL 实例难以承载庞大的读写压力和海量数据。Sharding-JDBC(现归入 Apache ShardingSphere)作为一款轻量级的分库分表中间件,可以在应用层面透明地实现数据库分片(Sharding),既保留了 MySQL 本身的生态优势,又能轻松应对 TB 级甚至 PB 级数据规模。本文将从原理、配置、实战到最佳实践,配合代码示例、Mermaid 图解和详细说明,帮助你快速掌握 Sharding-JDBC 的核心精髓。
目录
- 什么是 Sharding-JDBC?
- Sharding-JDBC 核心原理
2.1. 架构与模块层次
2.2. 分片策略(Sharding Strategy)
2.3. 路由与执行流程 - 基础环境与依赖准备
- 配置示例:Spring Boot + Sharding-JDBC
4.1. YAML 配置示例(分库分表)
4.2. Java API 方式配置示例 - 分库分表策略详解
5.1. 常见分片键与算法
5.2. Transaction 分布式事务支持
5.3. 读写分离(Read/Write Splitting) - 数据分片路由与 SQL 拆分
6.1. 单表插入与更新如何路由
6.2. 跨分片 JOIN 和聚合
6.3. 分片键范围查询与隐藏成本 - 实战:项目代码示例与解释
7.1. 项目结构与依赖说明
7.2. 配置文件解读
7.3. DAO 层调用示例
7.4. 测试与验证效果 - Mermaid 图解:Sharding-JDBC 工作流程
- 进阶话题与最佳实践
9.1. 监控与诊断(Sharding-JDBC Extra)
9.2. 动态分片扩容
9.3. 数据倾斜与热点分片优化
9.4. 分片规则演进与方案迁移 - 小结
1. 什么是 Sharding-JDBC?
Sharding-JDBC 是Apache ShardingSphere 中的一个组件,作为应用层的分布式数据库中间件,主要功能包括:
- 分库分表:将数据水平拆分到多张表或多个库,提高单表/单库压力承载能力。
- 读写分离:将写操作路由到主库,读操作路由到从库,实现读写分离架构。
- 分布式事务:基于 XA、柔性事务等多种方案,保证跨分片事务一致性。
- 灵活配置:支持 YAML、Spring Boot 配置、Java API 等多种配置方式,零侵入化集成应用。
- 生态兼容:完全兼容 JDBC 协议,对上层应用透明,无需改动原有 SQL。
与其他代理型中间件(如 MyCat、Cobar)不同,Sharding-JDBC 直接作为依赖包嵌入应用,无额外部署,易开发、易调试,还能借助 JVM 监控工具做链路跟踪。
2. Sharding-JDBC 核心原理
2.1 架构与模块层次
Sharding-JDBC 的整体架构主要分为以下几层(下图以 Mermaid 形式示意):
flowchart LR
subgraph 应用层 Application
A[用户代码(DAO/Service)]
end
subgraph Sharding-JDBC (中间件依赖包)
B1[ShardingDataSource]
B2[Sharding-JDBC 核心模块]
B3[SQL解析 & 路由模块]
B4[分片策略配置模块]
B5[读写分离模块]
B6[分布式事务模块]
end
subgraph 存储层 Storage
C1[DB实例1 (库1)]
C2[DB实例2 (库2)]
C3[DB实例3 (库3)]
end
A --> |JDBC 调用| B1
B1 --> B2
B2 --> B3
B3 --> B4
B3 --> B5
B3 --> B6
B3 --> C1 & C2 & C3
ShardingDataSource
- 对外暴露一个 DataSource,应用直接使用该 DataSource 获取连接,无感知底层多数据库存在。
- 负责拦截并分发所有 JDBC 请求。
SQL 解析 & 路由模块
- 通过 SQLParser 将原始 SQL 解析成 AST(抽象语法树),识别出对应的分片表、分片键等信息。
- 根据配置的分片策略(Sharding Strategy)计算出目标数据节点(库 + 表),并生成路由后的 SQL 片段(如
INSERT INTO t_order_1
)。
分片策略配置模块
- 包含分库(DatabaseShardingStrategy)、分表(TableShardingStrategy)、**分表自增主键(KeyGenerator)**等配置、并可定制化算法。
- 内置常见算法:标准分片(Inline)、哈希取模、范围分片、复合分片等。
读写分离模块
- 支持主从复制架构,定义主库和从库的 DataSource 集合。
- 根据 SQL 类型(
SELECT
、INSERT/UPDATE/DELETE
)以及 Hint,可将读操作路由到从库,写操作路由到主库。
分布式事务模块
- 提供两种事务模式:XA事务(强一致性,但性能开销大)和 柔性事务(柔性事务框架,如 Seata)。
- 在多个数据源并行执行操作时,协调事务提交或回滚,保证数据一致性。
2.2 分片策略(Sharding Strategy)
常见分片策略有两种:
标准分片(Standard Sharding)
- 通过配置简单表达式(Inline)或者自定义分片算法,将分片键值映射到具体“库”与“表”。
例如,分片键
user_id
取模算法:- 数据库数量
dbCount = 2
,表数量tableCount = 4
(每个库 2 张表)。 dbIndex = user_id % dbCount
,tableIndex = user_id % tableCount
。- 最终路由到:
ds_${dbIndex}.t_user_${tableIndex}
。
- 数据库数量
复合分片(Complex Sharding)
- 当一个表需要根据多个字段进行分片时,可以使用复合分片策略(Complex Sharding)。
- 例如:按
user_id
取模分库,按order_id
取模分表。
2.3 路由与执行流程
下面用 Mermaid 时序图演示一次典型的 SQL 路由执行流程(以 INSERT
为例):
sequenceDiagram
participant App as 应用代码
participant ShardingDS as ShardingDataSource
participant SQLParser as SQLParser & Analyzer
participant Routing as 路由模块
participant DB1 as DB 实例1
participant DB2 as DB 实例2
App->>ShardingDS: connection.prepareStatement("INSERT INTO t_order(user_id, amount) VALUES (?, ?)")
ShardingDS->>SQLParser: 解析 SQL,提取 t_order 与分片键 user_id
SQLParser-->>Routing: 分片键 user_id = 103
Routing->>Routing: 计算 dbIndex = 103 % 2 = 1, tableIndex = 103 % 4 = 3
Routing-->>ShardingDS: 确定目标:ds_1.t_order_3
ShardingDS->>DB2: 执行 "INSERT INTO t_order_3 ..."
DB2-->>ShardingDS: 返回结果
ShardingDS-->>App: 返回执行结果
- SQLParser:负责将 SQL 文本解析成 AST,识别出分片表(
t_order
)和分片键(user_id
)。 - Routing:基于分片策略计算出目标数据节点。在本例中,
user_id
为 103,ds_1
第 2 个库,t_order_3
第 4 张表。 - 实际执行:ShardingDS 将拼装后的 SQL 发往目标数据库节点。
3. 基础环境与依赖准备
在开始编码之前,先确保本地或服务器环境安装以下组件:
- JDK 1.8+
- Maven或Gradle构建工具
- MySQL 多实例准备:至少两个 MySQL 实例或同机多端口模拟,数据库名可以为
ds_0
、ds_1
。 Apache ShardingSphere-JDBC 依赖:在
pom.xml
中引入如下核心依赖(以 5.x 版本为例):<dependencies> <!-- ShardingSphere-JDBC Spring Boot Starter --> <dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>shardingsphere-jdbc-spring-boot-starter</artifactId> <version>5.4.0</version> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <!-- Spring Boot Web(可选,根据项目需求) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Lombok(可选,用于简化 POJO) --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.28</version> <scope>provided</scope> </dependency> </dependencies>
数据库表结构示例:在
ds_0
、ds_1
中分别创建逻辑同名的分片表,例如:-- 在 ds_0 和 ds_1 中分别执行 CREATE TABLE t_order_0 ( order_id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, amount DECIMAL(10,2) NOT NULL, created_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE t_order_1 LIKE t_order_0; CREATE TABLE t_order_2 LIKE t_order_0; CREATE TABLE t_order_3 LIKE t_order_0;
这样一来,总共有四张分表:
t_order_0
、t_order_1
(位于ds_0
),t_order_2
、t_order_3
(位于ds_1
)。
4. 配置示例:Spring Boot + Sharding-JDBC
Sharding-JDBC 的配置方式常见有两种:YAML/Properties 方式(最流行、最简洁)和Java API 方式。下面分别示例。
4.1 YAML 配置示例(分库分表)
在 Spring Boot 项目中,编辑 application.yml
,内容示例如下:
spring:
shardingsphere:
datasource:
names: ds_0, ds_1
ds_0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/ds_0?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
username: root
password: root
ds_1:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3307/ds_1?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
username: root
password: root
rules:
sharding:
tables:
t_order:
actual-data-nodes: ds_${0..1}.t_order_${0..3}
database-strategy:
inline:
sharding-column: user_id
algorithm-expression: ds_${user_id % 2}
table-strategy:
inline:
sharding-column: user_id
algorithm-expression: t_order_${user_id % 4}
key-generator:
column: order_id
type: SNOWFLAKE
default-database-strategy:
none:
default-table-strategy:
none
说明:
datasource.names
- 定义两个 DataSource,
ds_0
和ds_1
,分别对应两个物理数据库。
- 定义两个 DataSource,
actual-data-nodes
ds_${0..1}.t_order_${0..3}
表示数据节点为:ds_0.t_order_0, ds_0.t_order_1, ds_0.t_order_2, ds_0.t_order_3
ds_1.t_order_0, ds_1.t_order_1, ds_1.t_order_2, ds_1.t_order_3
database-strategy.inline
- 分库策略:根据
user_id % 2
将数据路由到ds_0
或ds_1
。
- 分库策略:根据
table-strategy.inline
- 分表策略:根据
user_id % 4
路由到对应分表。
- 分表策略:根据
key-generator
- 自增主键策略,使用 Snowflake 算法生成分布式唯一
order_id
。
- 自增主键策略,使用 Snowflake 算法生成分布式唯一
Mermaid 图解:YAML 配置对应分片结构
flowchart LR subgraph ds_0 T00[t_order_0] T01[t_order_1] T02[t_order_2] T03[t_order_3] end subgraph ds_1 T10[t_order_0] T11[t_order_1] T12[t_order_2] T13[t_order_3] end %% 分库策略:user_id % 2 A[user_id % 2 = 0] --> T00 & T01 B[user_id % 2 = 1] --> T10 & T11 %% 分表策略:user_id % 4 subgraph ds_0 分表 A --> |user_id%4=0| T00 A --> |user_id%4=1| T01 A --> |user_id%4=2| T02 A --> |user_id%4=3| T03 end subgraph ds_1 分表 B --> |user_id%4=0| T10 B --> |user_id%4=1| T11 B --> |user_id%4=2| T12 B --> |user_id%4=3| T13 end
4.2 Java API 方式配置示例
如果不使用 YAML,而希望通过 Java 代码动态构建 DataSource
,可如下示例:
@Configuration
public class ShardingConfig {
@Bean
public DataSource shardingDataSource() throws SQLException {
// 1. 配置 ds_0
HikariDataSource ds0 = new HikariDataSource();
ds0.setJdbcUrl("jdbc:mysql://localhost:3306/ds_0?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC");
ds0.setUsername("root");
ds0.setPassword("root");
// 2. 配置 ds_1
HikariDataSource ds1 = new HikariDataSource();
ds1.setJdbcUrl("jdbc:mysql://localhost:3307/ds_1?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC");
ds1.setUsername("root");
ds1.setPassword("root");
// 3. 组装 DataSource Map
Map<String, DataSource> dataSourceMap = new HashMap<>();
dataSourceMap.put("ds_0", ds0);
dataSourceMap.put("ds_1", ds1);
// 4. 配置分片表规则
ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
TableRuleConfiguration orderTableRuleConfig = new TableRuleConfiguration();
orderTableRuleConfig.setLogicTable("t_order");
// ds_${0..1}.t_order_${0..3}
orderTableRuleConfig.setActualDataNodes("ds_${0..1}.t_order_${0..3}");
// 分库策略
orderTableRuleConfig.setDatabaseShardingStrategyConfig(new InlineShardingStrategyConfiguration(
"user_id", "ds_${user_id % 2}"
));
// 分表策略
orderTableRuleConfig.setTableShardingStrategyConfig(new InlineShardingStrategyConfiguration(
"user_id", "t_order_${user_id % 4}"
));
// 主键生成策略:Snowflake
orderTableRuleConfig.setKeyGenerateStrategyConfig(new KeyGenerateStrategyConfiguration(
"order_id", "SNOWFLAKE"
));
shardingRuleConfig.getTableRuleConfigs().add(orderTableRuleConfig);
// 5. 构造 ShardingDataSource
return ShardingDataSourceFactory.createDataSource(
dataSourceMap,
shardingRuleConfig,
new ConcurrentHashMap<>(), // shardingProperties 可留空
new Properties()
);
}
}
说明:
- 通过
TableRuleConfiguration
定义逻辑表的映射、分库分表策略、主键生成器。ShardingDataSourceFactory.createDataSource
根据dataSourceMap
和ShardingRuleConfiguration
构建 ShardingDataSource,并注册到 Spring 容器。
5. 分库分表策略详解
5.1 常见分片键与算法
选择合适的分片键至关重要,常见注意点如下:
- 尽量使用可以均匀分布(如 UUID、Snowflake、取模后分布较均匀的自增 ID 等)
- 避免热点分片:像日期、性别等值域过小、数据量集中度过高的字段,不适合作为分片键。
- 关联查询考量:如果业务场景需要频繁 JOIN 多张表,且能共享同一个分片键,可让它们沿用同样的分片键与算法,减少跨库 JOIN。
常见算法:
Inline(内联表达式)
- 最简单的方式,通过占位符
${}
计算表达式。 - 示例:
ds_${user_id % 2}
,t_order_${order_id % 4}
。
- 最简单的方式,通过占位符
哈希取模(Hash)
- 通过
HashShardingAlgorithm
自定义实现,返回对应库与表。 - 适合分布更均匀、分片数量不固定的场景。
- 通过
范围分片(Range)
- 通过
RangeShardingAlgorithm
,将分片键值域划分成若干范围,如日期区间。 - 适用于时间分片(如按天、按月分表)。
- 通过
复合分片(Complex)
在分库分表策略同时考虑多个列。例如:
complex: sharding-columns: user_id, order_id algorithm-expression: ds_${user_id % 2}.t_order_${order_id % 4}
5.2 Transaction 分布式事务支持
当业务涉及跨分片的 多表更新/插入 时,需要保障事务一致性。Sharding-JDBC 支持两种事务模式:
XA 事务(XA Transaction)
- 基于两段式提交协议(2PC),由数据库本身(如 MySQL)支持。
配置示例(YAML):
spring: shardingsphere: rules: sharding: default-database-strategy: none default-table-strategy: none default-data-source-name: ds_0 transaction: type: XA
- 优点:强一致性、事务隔离级别与单库事务一致。
- 缺点:性能开销较大,要求底层数据库支持 XA,且并发性能不如本地事务。
柔性事务(Base on ShardingSphere-Proxy / Saga / TCC)
- ShardingSphere 5.x 引入了柔性事务(基于 Seata 的 AT 模式或 Saga 模式)。
示例配置:
spring: shardingsphere: transaction: provider-type: SEATA_AT
- 将使用 Seata 注册中心与 TC Server 协调事务,提交速度略快于 XA。
- 需要额外部署 Seata Server 或使用 TCC/Saga 相关框架。
5.3 读写分离(Read/Write Splitting)
在分库分表之外,Sharding-JDBC 还能实现读写分离。其原理是将写操作(INSERT/UPDATE/DELETE)路由到主库,将读操作(SELECT)路由到从库。配置示例如下:
spring:
shardingsphere:
datasource:
names: primary, replica0, replica1
primary:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/primary_db
username: root
password: root
replica0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3307/replica_db_0
username: root
password: root
replica1:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3308/replica_db_1
username: root
password: root
rules:
readwrite-splitting:
data-sources:
ds_group_0:
primary-data-source-name: primary
replica-data-source-names:
- replica0
- replica1
load-balancer:
type: ROUND_ROBIN
- 通过
readwrite-splitting
规则,将逻辑ds_group_0
映射到主库primary
和从库replica0
、replica1
。 - 配置
load-balancer
(负载均衡策略),示例使用轮询(ROUND\_ROBIN)将读请求在两台从库间分发。 - 应用无需修改 SQL,即可自动将 SELECT 路由到从库,其他写操作路由到主库。
6. 数据分片路由与 SQL 拆分
Sharding-JDBC 在执行 SQL 时,会对原始语句进行拆分并路由到多个数据节点。下面详细探讨几种常见场景。
6.1 单表插入与更新如何路由
以 SQL:INSERT INTO t_order(user_id, amount) VALUES (103, 99.50);
为例:
- SQL 解析:识别出逻辑表
t_order
、分片键字段user_id
。 计算目标分片节点:
dsIndex = 103 % 2 = 1
→ 数据库ds_1
tableIndex = 103 % 4 = 3
→ 分表t_order_3
生成并执行实际 SQL:
INSERT INTO ds_1.t_order_3(user_id, amount) VALUES (103, 99.50);
分片后的 PreparedStatement
只会被发送到 ds_1
,其他节点无此业务执行。
6.2 跨分片 JOIN 和聚合
当业务执行以下 SQL 时,Sharding-JDBC 会尝试拆分并在本地做聚合:
SELECT u.user_id, u.name, o.order_id, o.amount
FROM t_user u
JOIN t_order o ON u.user_id = o.user_id
WHERE u.user_id BETWEEN 100 AND 200;
分片表:t_user
、t_order
也按照 user_id
做同样分片。对于上述 SQL:
user_id BETWEEN 100 AND 200
对应的dsIndex
可能为100%2=0
到200%2=0
→ 实际会包含ds_0
、ds_1
两个库(因为用户区间跨库)。Sharding-JDBC 会在两个数据节点各自执行对应 SQL:
-- 在 ds_0 上执行 SELECT u.user_id, u.name, o.order_id, o.amount FROM t_user_0 u JOIN t_order_0 o ON u.user_id=o.user_id WHERE u.user_id BETWEEN 100 AND 200; -- 在 ds_1 上执行 SELECT u.user_id, u.name, o.order_id, o.amount FROM t_user_0 u JOIN t_order_0 o ON u.user_id=o.user_id WHERE u.user_id BETWEEN 100 AND 200;
(假设表规则为
t_user_${user_id%2}
、t_order_${user_id%4}
,此处简化只示意分库层面分片。)- 内存合并:将两个节点返回的结果集合并(Merge),再返回给应用。
Mermaid 图解:跨库 JOIN 过程
flowchart TD subgraph 应用发起跨分片 JOIN A[SELECT ... FROM t_user JOIN t_order ... WHERE user_id BETWEEN 100 AND 200] end subgraph Sharding-JDBC 路由层 A --> B{确定分库节点} B -->|ds_0| C1[路由 ds_0: t_user_0 JOIN t_order_0 ...] B -->|ds_1| C2[路由 ds_1: t_user_1 JOIN t_order_1 ...] end subgraph 数据库层 C1 --> D1[ds_0 执行 SQL] C2 --> D2[ds_1 执行 SQL] D1 --> E1[返回结果A] D2 --> E2[返回结果B] end E1 --> F[结果合并 & 排序] E2 --> F F --> G[最终结果返回给应用]
注意:
- 跨分片 JOIN 会带来性能开销,因为需要将多个节点的数据拉到应用侧或中间层进行合并。
- 尽量设计分片键一致的同表 JOIN,或仅在单分片范围内 JOIN,避免全局广播查询。
6.3 分片键范围查询与隐藏成本
对于 SELECT * FROM t_order WHERE user_id > 5000;
这类不带具体等值分片键的范围查询,Sharding-JDBC 只能广播到所有分片节点执行,再合并结果。隐藏成本包括:
- 跨库网络开销:每个库都要执行同样 SQL,返回大批结果集。
- 内存合并消耗:Sharding-JDBC 将多个结果集聚合到内存,需要关注 OOM 风险。
优化建议:
- 尽量通过业务代码指定更精确的分片键(如
AND user_id BETWEEN 1000 AND 2000 AND user_id % 2 = 0
)。 - 使用**提示(Hint)**功能强制 SQL 只路由到特定分片。
- 定期归档老数据到归档库,减少主分片表数据量。
7. 实战:项目代码示例与解释
下面以一个简易 Spring Boot 项目为例,演示如何集成 Sharding-JDBC,构建订单服务,并验证分库分表效果。
7.1 项目结构与依赖说明
sharding-jdbc-demo/
├── pom.xml
└── src
├── main
│ ├── java
│ │ └── com.example.sharding
│ │ ├── ShardingJdbcDemoApplication.java
│ │ ├── config
│ │ │ └── ShardingConfig.java
│ │ ├── entity
│ │ │ └── Order.java
│ │ ├── mapper
│ │ │ └── OrderMapper.java
│ │ └── service
│ │ └── OrderService.java
│ └── resources
│ └── application.yml
└── test
└── java
└── com.example.sharding
└── ShardingTest.java
- ShardingJdbcDemoApplication:Spring Boot 启动类。
- config/ShardingConfig:Java API 方式配置 Sharding-JDBC。
- entity/Order:对应数据库分片表
t_order
的实体类。 - mapper/OrderMapper:MyBatis 或 Spring JDBC Template DAO。
- service/OrderService:业务服务层,提供插入、查询等方法。
- application.yml:Sharding-JDBC YAML 配置示例。
7.2 配置文件解读:application.yml
server:
port: 8080
spring:
shardingsphere:
datasource:
names: ds_0, ds_1
ds_0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/ds_0
username: root
password: root
ds_1:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3307/ds_1
username: root
password: root
rules:
sharding:
tables:
t_order:
actual-data-nodes: ds_${0..1}.t_order_${0..3}
database-strategy:
inline:
sharding-column: user_id
algorithm-expression: ds_${user_id % 2}
table-strategy:
inline:
sharding-column: user_id
algorithm-expression: t_order_${user_id % 4}
key-generator:
column: order_id
type: SNOWFLAKE
- 与前文示例一致,指定两个数据源与分片表规则。
t_order
分片表规则写明了actual-data-nodes
、分片策略和 Snowflake 主键生成器。
7.3 DAO 层调用示例:OrderMapper
假设使用 MyBatis,OrderMapper.java
如下:
package com.example.sharding.mapper;
import com.example.sharding.entity.Order;
import org.apache.ibatis.annotations.*;
import java.util.List;
@Mapper
public interface OrderMapper {
@Insert("INSERT INTO t_order(user_id, amount) VALUES (#{userId}, #{amount})")
@Options(useGeneratedKeys = true, keyProperty = "orderId")
int insertOrder(Order order);
@Select("SELECT order_id, user_id, amount, created_time FROM t_order WHERE user_id = #{userId}")
List<Order> selectByUserId(@Param("userId") Long userId);
@Select("SELECT order_id, user_id, amount, created_time FROM t_order WHERE order_id = #{orderId}")
Order selectByOrderId(@Param("orderId") Long orderId);
}
说明:
insertOrder
不需要关心分片,Sharding-JDBC 会自动将其路由到正确分表并填充主键orderId
。- 查询
selectByUserId
会根据分片策略,将 SQL 路由到相应的分表,返回单个分片中的结果集合。selectByOrderId
若orderId
作为分片键或暴露了分片信息,可更准确地路由到单表,否则会广播到所有分片,合并后返回。
7.4 Service 层示例:OrderService
package com.example.sharding.service;
import com.example.sharding.entity.Order;
import com.example.sharding.mapper.OrderMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class OrderService {
private final OrderMapper orderMapper;
public OrderService(OrderMapper orderMapper) {
this.orderMapper = orderMapper;
}
/**
* 创建订单
*/
@Transactional
public Long createOrder(Long userId, Double amount) {
Order order = new Order();
order.setUserId(userId);
order.setAmount(amount);
orderMapper.insertOrder(order);
return order.getOrderId();
}
/**
* 根据 user_id 查询该用户所有订单
*/
public List<Order> getOrdersByUser(Long userId) {
return orderMapper.selectByUserId(userId);
}
/**
* 根据 order_id 查询订单
*/
public Order getOrderById(Long orderId) {
return orderMapper.selectByOrderId(orderId);
}
}
@Transactional
保证跨分片的单个插入操作也在同一事务上下文中。- 获取订单列表(
getOrdersByUser
)会被 Sharding-JDBC 路由到当前userId
所在的分片。 - 若
getOrderById
方法中使用的orderId
可用来反推出userId
(例如存储了 userId 或在业务层先查询出userId
),则可避免广播查询。
7.5 测试与验证效果:ShardingTest
使用 JUnit 简要验证分库分表效果:
@SpringBootTest
public class ShardingTest {
@Autowired
private OrderService orderService;
@Test
public void testShardingInsertAndQuery() {
// 插入不同 userId 的订单
Long orderId1 = orderService.createOrder(1001L, 50.0);
Long orderId2 = orderService.createOrder(1002L, 75.0);
Long orderId3 = orderService.createOrder(1003L, 120.0);
System.out.println("orderId1 = " + orderId1);
System.out.println("orderId2 = " + orderId2);
System.out.println("orderId3 = " + orderId3);
// 查询 userId=1001 的订单(应路由到 ds_1.t_order_1)
List<Order> orders1001 = orderService.getOrdersByUser(1001L);
Assertions.assertFalse(orders1001.isEmpty());
// 查询 orderId1
Order o1 = orderService.getOrderById(orderId1);
Assertions.assertNotNull(o1);
System.out.println("Fetched Order: " + o1);
}
}
验证要点:
- 通过插入多条订单,先查看日志或调试断点,确认
INSERT
路由到不同分片表。- 调用
getOrdersByUser
时,Sharding-JDBC 会计算userId%2
与userId%4
,定位到正确分片。- 调用
getOrderById
(如果未设置分片键查询),会广播到所有分片,效率略低,应在业务层优化。
8. Mermaid 图解:Sharding-JDBC 工作流程
下面通过 Mermaid 时序图和流程图更加直观地展示 Sharding-JDBC 的工作过程。
8.1 单条插入请求全过程
sequenceDiagram
participant App as 应用代码
participant ShardingDS as ShardingDataSource
participant Parser as SQLParser
participant Routing as 路由模块
participant Execute as 执行模块
participant DB0 as ds_0
participant DB1 as ds_1
App->>ShardingDS: getConnection()
ShardingDS-->>App: Connection
App->>ShardingDS: prepareStatement("INSERT INTO t_order(user_id, amount) VALUES (101, 59.99)")
ShardingDS->>Parser: 解析 SQL -> 抽象语法树 (AST)
Parser-->>Routing: 提取 t_order, sharding_column=user_id=101
Routing->>Routing: 101 % 2 => 1;101 % 4 => 1
Routing-->>Execute: 路由到 ds_1.t_order_1
Execute->>DB1: 执行 "INSERT ds_1.t_order_1(user_id, amount) VALUES (101, 59.99)"
DB1-->>Execute: 返回执行结果(主键 auto-generated)
Execute-->>App: 返回执行结果
8.2 读写分离 SQL 路由
flowchart LR
subgraph 应用 SQL
A1[SELECT * FROM t_order WHERE order_id = 123]
A2[INSERT INTO t_order(…) VALUES (…) ]
end
subgraph Sharding-JDBC 路由
A1 --> B1{读 or 写?}
B1 -- 读 --> C1[路由到从库 (replica)]
B1 -- 写 --> C2[路由到主库 (primary)]
C1 --> DB_read
C2 --> DB_write
end
- Sharding-JDBC 根据 SQL 类型自动判断读写,将读操作发到从库,写操作发到主库。
9. 进阶话题与最佳实践
9.1 监控与诊断(Sharding-JDBC Extra)
- 利用 Sharding Analytics 运维工具,可实时查看各分片节点的 QPS、TPS、慢 SQL、热点表等信息。
- 性能插件:可以通过 Sharding-JDBC 的拦截器或 AOP 插件打印每条 SQL 的路由详情、执行耗时,辅助定位瓶颈。
- 对于关键 SQL,建议开启SQL 转换开关(SQLShow 或 SQLPrint)以记录实际路由后的真实 SQL,便于调试。
9.2 动态分片扩容
9.2.1 扩容思路
- 水平扩容数据库实例:新增一个或多个数据库,用于接收新数据分片。
- 更新分片规则:修改
actual-data-nodes
,将新增的数据库纳入分片节点范围。 - 迁移旧数据:通过脚本或工具,将历史数据从旧节点迁移到新节点,并调整分片键映射(如更新模运算参数)。
- 灰度切换 & 测试:逐步上线新版分片规则,观察系统情况,最后彻底切换、下线旧分片。
9.2.2 实现示例
假设需要在两个分库基础上新增 ds_2
,原分片公式 user_id % 3
,分表 user_id % 6
。配置变化示例如下:
spring:
shardingsphere:
datasource:
names: ds_0, ds_1, ds_2
ds_2:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3309/ds_2
username: root
password: root
rules:
sharding:
tables:
t_order:
actual-data-nodes: ds_${0..2}.t_order_${0..5}
database-strategy:
inline:
sharding-column: user_id
algorithm-expression: ds_${user_id % 3}
table-strategy:
inline:
sharding-column: user_id
algorithm-expression: t_order_${user_id % 6}
key-generator:
column: order_id
type: SNOWFLAKE
- 旧配置:
user_id % 2
→ 2 库,user_id % 4
→ 4 表。 - 新配置:
user_id % 3
→ 3 库,user_id % 6
→ 6 表。 - 在 平滑灰度 期间,需要双写到新旧分片(或仅写旧分片、暂缓读取),并逐步迁移历史数据。
9.3 数据倾斜与热点分片优化
- 诊断:通过监控 QPS、TPS、慢 SQL 等指标,发现某些分片负载明显高于其他。
- 避免:选取合适分片键,保证数据均匀分布;如使用哈希后缀替代直接自增。
- 手动干预:对于热点数据,可考虑手动分表、热点拆分(Hot partitioning)或者在应用层进行短暂缓存,降低分片压力。
9.4 分片规则演进与方案迁移
- 提前设计:最好预估未来数据规模,提前留出足够分片余量,避免频繁变更分片键算法。
- 弱化分片键依赖:在业务层不要过度依赖隐式分片逻辑,比如不要在业务代码大量写死
ds_${user_id % n}
,而应借助 Sharding-JDBC 来管理路由。 - 物理表名与逻辑表名解耦:不要在应用中直接使用物理分片表名;始终以逻辑表名(
t_order
)作为编程接口,让 Sharding-JDBC 透明转发。
10. 小结
本文围绕 “Sharding-JDBC详解:掌握MySQL分库分表精髓” 这一主题,从以下几个角度展开了详尽介绍:
Sharding-JDBC 的定位与核心原理
- 作为应用层轻量级分布式中间件,无需额外部署,兼容 JDBC 生态。
- 内部模块划分:DataSource 拦截、SQL 解析与路由、分片策略、读写分离、分布式事务等。
YAML 与 Java API 配置示例
- 详细展示了如何在 Spring Boot 中通过 YAML 或 Java 代码动态配置 DataSource、分片规则、Snowflake 主键生成器等。
- 通过 Mermaid 图解辅助说明分片表、分库策略如何映射到实际物理节点。
分片策略与路由执行流程
- 介绍了标准分片(Inline、Hash)、复合分片、范围分片等策略。
- 剖析了 SQLRouter 如何将原始 SQL 拆解、路由到目标数据节点,并在应用层进行结果合并。
常见问题与优化实践
- 提示跨分片 JOIN、范围查询带来的性能成本,建议尽量限定分片键查询范围。
- 探讨了分布式事务模式(XA、柔性事务)、读写分离、监控诊断、动态扩容、数据倾斜等进阶话题。
完整项目实战示例
- 提供一个可运行的 Spring Boot 示例,演示如何定义 DAO、Service、配置、单元测试,快速验证 Sharding-JDBC 分库分表功能。
- 通过 JUnit 测试展示插入、按
user_id
查询等常见业务场景。
未来演进与最佳实践
- 强调分片键选择对系统均衡性的重要性;
- 建议提前预留分片策略,减少后期迁移成本;
- 提供分片规则变更、数据迁移、灰度发布等常见方案思路。
掌握了 Sharding-JDBC 的核心精髓后,你将能够在不改动应用层业务代码的前提下,轻松实现 MySQL 的分库分表、读写分离与分布式事务,支撑大规模高并发场景。希望本文的代码示例、图解和详细说明能帮助你快速上手、并在实际项目中得心应手地应用 Sharding-JDBC 解决方案。
评论已关闭