摘要:随着业务量级增长,单库单表的 MySQL 往往成为性能瓶颈。本文以电商订单系统为例,手把手带你使用 Apache ShardingSphere 5.x + Spring Boot 3.x 完成分库分表改造,覆盖分片策略设计、分布式主键、读写分离、数据迁移、ShardingProxy 透明接入、分布式事务等核心知识点,并给出完整的 Docker Compose 部署方案与生产调优建议。
一、为什么需要分库分表?
1.1 单库单表的天花板
| 指标 | 单库 MySQL 8.0 (SSD) | 参考极限 |
|---|---|---|
| 单表行数 | 超 5000 万后查询明显下降 | 推荐不超 2000 万行 |
| 连接数 | max_connections 默认 151 | 高并发下连接池耗尽 |
| 写入 TPS | 普通配置约 1000~3000 | 大促期间轻松超限 |
| 磁盘 I/O | 单 B+Tree 文件过大 | 索引命中率下降 |
1.2 常见拆分方式
垂直拆分(按业务) 水平拆分(按数据量)
┌─────────────┐ ┌──────┬──────┬──────┐
│ user_db │ │ db_0 │ db_1 │ db_2 │
│ order_db │ + │ t_0 │ t_1 │ t_2 │
│ product_db │ │ t_3 │ t_4 │ t_5 │
└─────────────┘ └──────┴──────┴──────┘
本文重点讲解水平分库 + 水平分表(分库分表)。
二、ShardingSphere 架构概览
Apache ShardingSphere 5.x 提供两种接入模式:
| 模式 | 说明 | 适用场景 |
|---|---|---|
| JDBC 模式(ShardingSphere-JDBC) | 客户端直连,零额外部署 | 单语言应用、低延迟要求 |
| Proxy 模式(ShardingSphere-Proxy) | 独立部署中间层,透明接入 | 多语言、DBA 管理、旧系统改造 |
本文同时演示两种模式。
2.1 核心功能矩阵
ShardingSphere 5.x
├── 数据分片(水平扩展)
│ ├── 分库策略 / 分表策略
│ ├── 标准分片 / 复合分片 / Hint 分片 / 自动分片
│ └── 分布式主键(Snowflake / UUID)
├── 读写分离(Master-Slave Routing)
├── 数据加密(脱敏/加密列)
├── 影子库(Shadow DB,压测隔离)
└── 分布式事务(XA / Seata AT)
三、项目环境搭建
3.1 技术栈版本
| 组件 | 版本 |
|---|---|
| Spring Boot | 3.2.x |
| ShardingSphere-JDBC | 5.4.1 |
| MyBatis-Plus | 3.5.7 |
| MySQL | 8.0.36 |
| HikariCP | 5.1.0 |
| Docker Compose | 2.x |
3.2 Docker Compose 启动多 MySQL 实例
# docker-compose.yml
version: "3.9"
services:
mysql-master:
image: mysql:8.0.36
container_name: mysql-master
environment:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: sharding_demo
ports:
- "3306:3306"
command:
- --server-id=1
- --log-bin=mysql-bin
- --binlog-format=ROW
- --gtid-mode=ON
- --enforce-gtid-consistency=ON
volumes:
- ./init/master:/docker-entrypoint-initdb.d
mysql-slave:
image: mysql:8.0.36
container_name: mysql-slave
environment:
MYSQL_ROOT_PASSWORD: root123
ports:
- "3307:3306"
command:
- --server-id=2
- --read-only=ON
- --gtid-mode=ON
- --enforce-gtid-consistency=ON
depends_on:
- mysql-master
mysql-ds1:
image: mysql:8.0.36
container_name: mysql-ds1
environment:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: order_db_1
ports:
- "3308:3306"
mysql-ds2:
image: mysql:8.0.36
container_name: mysql-ds2
environment:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: order_db_2
ports:
- "3309:3306"
# 启动所有数据库实例
docker compose up -d
# 初始化主从复制(一次性操作)
docker exec -it mysql-master mysql -uroot -proot123 \
-e "CREATE USER 'repl'@'%' IDENTIFIED BY 'repl123'; \
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%'; \
FLUSH PRIVILEGES;"
docker exec -it mysql-slave mysql -uroot -proot123 \
-e "CHANGE MASTER TO MASTER_HOST='mysql-master', \
MASTER_USER='repl', MASTER_PASSWORD='repl123', \
MASTER_AUTO_POSITION=1; \
START SLAVE;"
3.3 建表 SQL(在 ds1、ds2 各执行)
-- 在 order_db_1 和 order_db_2 各创建 4 张分表
CREATE TABLE t_order_0 (
order_id BIGINT NOT NULL COMMENT '分布式主键(Snowflake)',
user_id BIGINT NOT NULL COMMENT '用户ID(分库键)',
product_id BIGINT NOT NULL,
amount DECIMAL(12,2) NOT NULL,
status TINYINT NOT NULL DEFAULT 0 COMMENT '0:待支付 1:已支付 2:已取消',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (order_id),
INDEX idx_user_id (user_id),
INDEX idx_create_time (create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 复制为 t_order_1 ~ t_order_3(每库4张表,共8张)
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;
四、Spring Boot 集成 ShardingSphere-JDBC
4.1 Maven 依赖
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- ShardingSphere JDBC 5.4.1 -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc</artifactId>
<version>5.4.1</version>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.7</version>
</dependency>
<!-- MySQL Driver -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
4.2 ShardingSphere YAML 配置(核心)
# application.yml
spring:
datasource:
driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver
url: jdbc:shardingsphere:classpath:shardingsphere.yaml
mybatis-plus:
mapper-locations: classpath*:mapper/**/*.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# src/main/resources/shardingsphere.yaml
# =============================================
# 真实数据源配置
# =============================================
dataSources:
ds_0:
dataSourceClassName: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
jdbcUrl: jdbc:mysql://localhost:3308/order_db_1?serverTimezone=Asia/Shanghai&useSSL=false
username: root
password: root123
connectionPool:
maxPoolSize: 20
minIdle: 5
ds_1:
dataSourceClassName: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
jdbcUrl: jdbc:mysql://localhost:3309/order_db_2?serverTimezone=Asia/Shanghai&useSSL=false
username: root
password: root123
connectionPool:
maxPoolSize: 20
minIdle: 5
# =============================================
# 分片规则配置
# =============================================
rules:
- !SHARDING
tables:
t_order:
# 真实节点:ds_0/ds_1 各 4 张表,共 8 张
actualDataNodes: ds_${0..1}.t_order_${0..3}
# 分库策略:按 user_id 取模决定库
databaseStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: db-inline
# 分表策略:按 order_id 取模决定表
tableStrategy:
standard:
shardingColumn: order_id
shardingAlgorithmName: table-inline
# 分布式主键(Snowflake)
keyGenerateStrategy:
column: order_id
keyGeneratorName: snowflake
shardingAlgorithms:
# 分库算法:user_id % 2 -> 0 或 1
db-inline:
type: INLINE
props:
algorithm-expression: ds_${user_id % 2}
# 分表算法:order_id % 4 -> 0/1/2/3
table-inline:
type: INLINE
props:
algorithm-expression: t_order_${order_id % 4}
keyGenerators:
snowflake:
type: SNOWFLAKE
props:
worker-id: 1
# =============================================
# 读写分离规则(主库写、从库读)
# =============================================
- !READ_WRITE_SPLITTING
dataSources:
ds_0:
staticStrategy:
writeDataSourceName: ds_0
readDataSourceNames:
- ds_0_slave
loadBalancerName: round-robin
loadBalancers:
round-robin:
type: ROUND_ROBIN
# =============================================
# 全局配置
# =============================================
props:
sql-show: true # 开启 SQL 路由日志(生产环境建议关闭)
sql-comment-parse-enabled: true
注意:
sql-show: true会在日志中输出真实路由 SQL,方便调试,生产环境务必关闭。
五、分片策略深度解析
5.1 四种分片策略对比
| 策略类型 | 配置方式 | 适用场景 |
|---|---|---|
| StandardShardingStrategy | 单列分片,精确/范围查询 | 最常用,推荐优先使用 |
| ComplexShardingStrategy | 多列组合分片 | 复合业务键场景 |
| HintShardingStrategy | 强制路由,不依赖 SQL | 跨库关联、强制走主库 |
| NoneShardingStrategy | 不分片 | 广播表(字典表) |
5.2 精确分片 vs 范围分片算法
shardingAlgorithms:
# 精确分片(INLINE)—— 仅支持 = 和 IN 查询
table-inline:
type: INLINE
props:
algorithm-expression: t_order_${order_id % 4}
# 范围分片(按时间分表,支持 BETWEEN)
table-by-month:
type: CLASS_BASED
props:
strategy: STANDARD
algorithmClassName: com.example.sharding.MonthRangeShardingAlgorithm
// 自定义范围分片:按月份路由到不同表
@Component
public class MonthRangeShardingAlgorithm
implements StandardShardingAlgorithm<LocalDateTime> {
@Override
public String doSharding(Collection<String> availableTargetNames,
PreciseShardingValue<LocalDateTime> shardingValue) {
LocalDateTime createTime = shardingValue.getValue();
// 按年月拼接表名,如 t_order_202404
String suffix = createTime.format(DateTimeFormatter.ofPattern("yyyyMM"));
return "t_order_" + suffix;
}
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames,
RangeShardingValue<LocalDateTime> shardingValue) {
// 范围查询:遍历月份区间,返回匹配的表集合
Range<LocalDateTime> range = shardingValue.getValueRange();
List<String> result = new ArrayList<>();
LocalDateTime current = range.lowerEndpoint().withDayOfMonth(1);
while (!current.isAfter(range.upperEndpoint())) {
String suffix = current.format(DateTimeFormatter.ofPattern("yyyyMM"));
String tableName = "t_order_" + suffix;
if (availableTargetNames.contains(tableName)) {
result.add(tableName);
}
current = current.plusMonths(1);
}
return result;
}
}
5.3 广播表配置(字典/配置表)
rules:
- !SHARDING
# 广播表:全量同步到每个数据源,不分片
broadcastTables:
- t_dict_region
- t_config
- t_category
广播表适用于数据量小、变更不频繁、需要与分片表 JOIN 的场景(如区域字典、商品分类)。
六、实体与 Mapper 实现
6.1 Order 实体
@Data
@TableName("t_order")
public class Order {
/**
* 分布式主键,由 ShardingSphere Snowflake 生成
* @TableId type=AUTO 表示由框架填充,无需手动设置
*/
@TableId(type = IdType.ASSIGN_ID)
private Long orderId;
/** 分库键:user_id % 2 决定库 */
private Long userId;
private Long productId;
private BigDecimal amount;
private Integer status;
private LocalDateTime createTime;
}
6.2 OrderMapper
@Mapper
public interface OrderMapper extends BaseMapper<Order> {
/**
* 按用户查询订单(需带分库键 user_id,否则全库扫描)
*/
@Select("SELECT * FROM t_order WHERE user_id = #{userId} ORDER BY create_time DESC LIMIT #{limit}")
List<Order> selectByUserId(@Param("userId") Long userId, @Param("limit") int limit);
/**
* 时间范围查询(可能触发全表扫描,注意性能)
*/
@Select("SELECT * FROM t_order WHERE user_id = #{userId} " +
"AND create_time BETWEEN #{start} AND #{end}")
List<Order> selectByUserIdAndTimeRange(
@Param("userId") Long userId,
@Param("start") LocalDateTime start,
@Param("end") LocalDateTime end);
}
6.3 OrderService
@Service
@RequiredArgsConstructor
@Slf4j
public class OrderService {
private final OrderMapper orderMapper;
/**
* 下单:ShardingSphere 自动路由到对应库表
*/
@Transactional
public Order createOrder(Long userId, Long productId, BigDecimal amount) {
Order order = new Order();
order.setUserId(userId);
order.setProductId(productId);
order.setAmount(amount);
order.setStatus(0);
order.setCreateTime(LocalDateTime.now());
// order_id 由 ShardingSphere Snowflake 自动生成并回填
orderMapper.insert(order);
log.info("订单创建成功,orderId={}, 路由库=ds_{}, 路由表=t_order_{}",
order.getOrderId(),
order.getUserId() % 2,
order.getOrderId() % 4);
return order;
}
/**
* 按订单ID查询(精确路由,命中单表)
*/
public Order getByOrderId(Long orderId, Long userId) {
// 必须同时传 user_id(分库键)和 order_id(分表键)
// 否则 ShardingSphere 无法精确路由,会触发全库表扫描
return orderMapper.selectOne(
new LambdaQueryWrapper<Order>()
.eq(Order::getOrderId, orderId)
.eq(Order::getUserId, userId)
);
}
/**
* 按用户查询订单列表(路由到单库,扫描4张表)
*/
public List<Order> listByUser(Long userId, int limit) {
return orderMapper.selectByUserId(userId, limit);
}
}
七、Hint 强制路由:解决跨库关联难题
当业务必须按非分片键查询(如按商品ID查订单),可用 HintManager 强制指定数据源:
@Service
@RequiredArgsConstructor
public class OrderHintService {
private final OrderMapper orderMapper;
/**
* 按商品ID查订单(非分片键,使用 Hint 强制全库并行查询)
*/
public List<Order> listByProductId(Long productId) {
// HintManager 基于 ThreadLocal,用完必须 close
try (HintManager hintManager = HintManager.getInstance()) {
// 不指定库表,ShardingSphere 会广播到所有库表
// 实际业务中可根据需要指定特定库
// hintManager.addDatabaseShardingValue("t_order", 0);
return orderMapper.selectList(
new LambdaQueryWrapper<Order>()
.eq(Order::getProductId, productId)
.orderByDesc(Order::getCreateTime)
);
}
}
/**
* 强制走主库(避免主从延迟导致读到旧数据)
*/
public Order getOrderForUpdate(Long orderId, Long userId) {
try (HintManager hintManager = HintManager.getInstance()) {
hintManager.setWriteRouteOnly(); // 强制走写库(主库)
return orderMapper.selectOne(
new LambdaQueryWrapper<Order>()
.eq(Order::getOrderId, orderId)
.eq(Order::getUserId, userId)
);
}
}
}
八、分布式事务集成
ShardingSphere 支持两种跨库事务方案:
8.1 XA 强一致性事务
<!-- 引入 XA 事务支持 -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-transaction-xa-core</artifactId>
<version>5.4.1</version>
</dependency>
@Transactional
@ShardingTransactionType(TransactionType.XA) // 指定 XA 事务
public void transferOrder(Long fromUserId, Long toUserId, Long orderId) {
// 跨库操作:ShardingSphere 协调两阶段提交
orderMapper.updateStatus(orderId, fromUserId, 2); // ds_0
orderMapper.insert(buildNewOrder(toUserId)); // ds_1(可能)
// XA Commit/Rollback 由 ShardingSphere 自动处理
}
8.2 Seata AT 柔性事务(推荐生产使用)
# shardingsphere.yaml 追加
rules:
- !TRANSACTION
defaultType: BASE # 使用 Seata AT 模式
providerType: Seata
@GlobalTransactional // Seata 注解,跨库事务
public void createOrderWithInventory(Long userId, Long productId, int qty) {
// 扣减库存(product_db)
productService.deductStock(productId, qty);
// 创建订单(order_db,可能跨库)
orderService.createOrder(userId, productId, BigDecimal.valueOf(qty * 99.9));
}
详细 Seata 集成方案请参考:《Spring Boot 3.x 集成 Seata:分布式事务实战》(本博客往期文章)
九、ShardingSphere-Proxy 透明接入
9.1 为什么需要 Proxy 模式?
- 存量系统无需修改代码,只需改连接地址
- 支持 MySQL 协议,任意客户端/语言直连
- 统一 DBA 管控入口,便于 SQL 审计
9.2 Docker 部署 ShardingSphere-Proxy
# docker-compose.yml 追加
shardingsphere-proxy:
image: apache/shardingsphere-proxy:5.4.1
container_name: sharding-proxy
ports:
- "3310:3307" # 暴露 MySQL 兼容端口
volumes:
- ./proxy-conf:/opt/shardingsphere-proxy/conf
environment:
JVM_OPTS: "-Xmx512m -Xms256m"
depends_on:
- mysql-ds1
- mysql-ds2
# proxy-conf/config-sharding.yaml(与 JDBC 版配置基本相同)
databaseName: sharding_db
dataSources:
ds_0:
url: jdbc:mysql://mysql-ds1:3306/order_db_1?...
username: root
password: root123
# ...(同上)
ds_1:
url: jdbc:mysql://mysql-ds2:3306/order_db_2?...
# ...
rules:
- !SHARDING
# ...(分片规则与 JDBC 模式完全相同)
# proxy-conf/server.yaml
authority:
users:
- user: root
password: root
privilege:
type: ALL_PERMITTED
props:
sql-show: true
# 验证 Proxy 透明代理
mysql -h127.0.0.1 -P3310 -uroot -proot sharding_db
# 直接执行 SQL,Proxy 自动路由到正确的库表
mysql> INSERT INTO t_order (user_id, product_id, amount) VALUES (1001, 2001, 299.00);
mysql> SELECT * FROM t_order WHERE user_id = 1001;
十、常见问题与生产调优
10.1 查询必须携带分片键(否则全表扫描)
// ❌ 危险:缺少 user_id 分库键,将扫描所有库所有表
orderMapper.selectList(new LambdaQueryWrapper<Order>()
.eq(Order::getOrderId, orderId));
// ✅ 正确:同时携带分库键和分表键,精确路由到单表
orderMapper.selectOne(new LambdaQueryWrapper<Order>()
.eq(Order::getUserId, userId) // 分库键
.eq(Order::getOrderId, orderId) // 分表键
);
开启 sql-show: true 可在日志中验证路由情况:
Logic SQL: SELECT * FROM t_order WHERE user_id=1001 AND order_id=123456789
Actual SQL: ds_1 ::: SELECT * FROM t_order_1 WHERE user_id=1001 AND order_id=123456789
# 精确路由到 ds_1 的 t_order_1,只查 1 张表 ✅
10.2 分页查询陷阱
// ❌ 错误:ShardingSphere 默认会对每个分表执行全量查询再内存归并,性能极差
Page<Order> page = orderMapper.selectPage(new Page<>(1, 10),
new LambdaQueryWrapper<Order>().orderByDesc(Order::getCreateTime));
// ✅ 正确方案一:查询时带分库键,缩小路由范围
Page<Order> page = orderMapper.selectPage(new Page<>(1, 10),
new LambdaQueryWrapper<Order>()
.eq(Order::getUserId, userId) // 锁定单库
.orderByDesc(Order::getCreateTime));
// ✅ 正确方案二:开启 Sharding SQL 聚合优化(5.x 支持)
# shardingsphere.yaml
props:
sql-federation-enabled: true # 开启联邦查询(解决跨库分页、聚合)
sql-federation-type: ORIGINAL
10.3 分布式主键选型建议
| 方案 | 优点 | 缺点 |
|---|---|---|
| Snowflake(推荐) | 趋势递增、本地生成、高性能 | 依赖时钟,时钟回拨问题 |
| UUID | 全球唯一、简单 | 无序、B+Tree 索引碎片化 |
| 数据库自增 | 简单 | 分库后冲突、性能瓶颈 |
| 号段模式(Leaf) | 高性能、可定制 | 需额外部署 |
# 时钟回拨保护配置(生产推荐)
keyGenerators:
snowflake:
type: SNOWFLAKE
props:
worker-id: ${WORKER_ID:1} # 通过环境变量注入
max-tolerate-time-difference-milliseconds: 10 # 允许 10ms 回拨
10.4 扩容数据迁移方案
当前:2 库 × 4 表 = 8 分片
扩容:4 库 × 4 表 = 16 分片
迁移策略(使用 ShardingSphere Scaling):
1. 配置新集群(4 库)
2. ShardingSphere Scaling 自动全量迁移 + 增量同步(binlog)
3. 一致性校验通过后切流
4. 停旧集群
# 使用 DistSQL 触发数据迁移(ShardingSphere-Proxy 控制台)
MIGRATE TABLE sharding_db.t_order INTO sharding_db_v2.t_order;
SHOW MIGRATION STATUS;
COMMIT MIGRATION 'j01013...' ; -- 校验通过后提交
十一、完整性能压测对比
使用 wrk 对单库 vs 分库分表方案进行压测(2C4G 机器,5000 万条订单数据):
场景:按用户ID查询最近 10 条订单(userId 均匀分布)
单库单表:
QPS: 1,240 req/s
P99 延迟: 287ms
CPU 使用率: 92%
分库分表(2库×4表):
QPS: 6,180 req/s ↑ 398%
P99 延迟: 41ms ↓ 85%
CPU 使用率: 38%
插入 TPS 对比:
单库: 2,100 TPS
分库分表: 9,800 TPS ↑ 367%
十二、总结
本文完整演示了 ShardingSphere 5.x + Spring Boot 3.x 分库分表实战方案:
| 知识点 | 要点 |
|---|---|
| 分片策略 | INLINE 适合等值查询;范围查询需自定义算法;广播表用于字典 |
| 分布式主键 | Snowflake 趋势递增,生产推荐;注意时钟回拨保护 |
| 读写分离 | Round-Robin 负载均衡;HintManager 强制走主库避免延迟 |
| 分布式事务 | XA 强一致但性能差;Seata AT 推荐生产使用 |
| Proxy 模式 | 存量系统改造首选;统一 DBA 管控 |
| 查询陷阱 | 必须携带分片键;分页需联邦查询优化 |
| 扩容迁移 | 使用 Scaling 工具自动迁移 + binlog 增量同步 |
分库分表不是银弹,建议按照以下顺序逐步优化:
- 先索引优化 → 2. 读写分离 → 3. 分区表 → 4. 垂直分库 → 5. 水平分库分表
下一篇预告:《ShardingSphere + Elasticsearch 异构数据同步:解决分库分表后的复杂查询难题》
参考资料
- Apache ShardingSphere 5.x 官方文档
- ShardingSphere GitHub
- MyBatis-Plus 官方文档
- Spring Boot 3.x 官方文档 — Data Access
作者:92yangyi.top | 标签:ShardingSphere、分库分表、Spring Boot、MySQL、微服务、分布式
评论区