1. 项目概述
在现代企业级应用开发中,数据量的快速增长常常会带来数据库性能瓶颈。作为一名长期奋战在一线的Java开发者,我经历过太多因为数据量激增导致的系统性能问题。今天要分享的是如何通过SpringBoot结合多数据源和分库分表技术来解决这些痛点。
在实际项目中,我们通常会遇到以下几种典型场景:
- 单表数据量超过千万级,查询性能急剧下降
- 业务需要同时访问多个独立的数据库实例
- 读写操作压力不均衡,需要分离读写流量
- 需要按特定维度(如时间、用户ID等)分散数据存储
针对这些问题,我将详细介绍两种主流解决方案:Dynamic-Datasource组件和ShardingSphere框架,以及它们的组合使用方式。这些方案都是我在多个生产环境中验证过的,包含大量实战中积累的经验和避坑指南。
2. Dynamic-Datasource 多数据源方案
2.1 核心组件介绍
dynamic-datasource-spring-boot-starter是MyBatis-Plus团队开发的一个轻量级多数据源组件。相比传统的AbstractRoutingDataSource实现方式,它提供了更简洁的注解驱动模式。
提示:在引入该组件前,请确保你的SpringBoot版本在2.x以上,JDK版本在1.8+
Maven依赖配置如下:
xml复制<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>4.3.1</version>
</dependency>
2.2 配置详解
与传统单数据源配置相比,dynamic-datasource的配置结构有显著差异。以下是生产环境中常用的配置模板:
yaml复制spring:
datasource:
dynamic:
primary: master # 默认数据源
strict: false # 是否严格匹配数据源
datasource:
master:
url: jdbc:mysql://master-host:3306/core_db
username: admin
password: encrypted_password
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.zaxxer.hikari.HikariDataSource
hikari:
maximum-pool-size: 20
connection-timeout: 30000
slave1:
url: jdbc:mysql://slave1-host:3306/core_db
username: read_only_user
password: encrypted_password
driver-class-name: com.mysql.cj.jdbc.Driver
关键配置项说明:
primary:指定默认数据源,当方法未标注@DS注解时使用strict:设为true时,未找到指定数据源会抛出异常;false则回退到默认数据源- 每个子数据源可以独立配置连接池参数
2.3 核心使用方式
组件提供了简洁的@DS注解来实现数据源切换:
java复制@Service
public class OrderService {
@DS("master")
public void createOrder(Order order) {
// 写入操作使用主库
orderMapper.insert(order);
}
@DS("slave1")
public Order getOrderById(Long id) {
// 查询操作使用从库
return orderMapper.selectById(id);
}
}
注意事项:
- @DS注解可以标注在类或方法上,方法注解优先级高于类注解
- 事务方法中切换数据源需要特别注意,建议在Service层最外层方法上统一标注
- 动态数据源与MyBatis一级缓存存在兼容性问题,建议在配置中关闭本地缓存
2.4 高级特性
2.4.1 读写分离支持
组件内置了简单的读写分离负载均衡策略。配置示例:
yaml复制spring:
datasource:
dynamic:
primary: master
datasource:
master:
url: jdbc:mysql://master-host:3306/db
slave_1:
url: jdbc:mysql://slave1-host:3306/db
slave_2:
url: jdbc:mysql://slave2-host:3306/db
strategy:
load-balance: # 负载均衡策略配置
slaves: slave_1,slave_2 # 从库分组
round-robin: # 轮询策略
使用时只需在查询方法上标注@DS("slave"),组件会自动在slave_1和slave_2之间轮询。
2.4.2 多租户支持
通过组合@DS注解和租户上下文,可以实现多租户数据隔离:
java复制@DS("#{tenantContext.getTenantDs()}")
public List<Order> getTenantOrders() {
// 根据租户上下文动态选择数据源
return orderMapper.selectList(null);
}
3. ShardingSphere 分库分表方案
3.1 框架选型考量
ShardingSphere已成为Java生态中最成熟的分库分表解决方案。相比其他方案,它的优势在于:
- 对业务代码零侵入
- 支持几乎所有的SQL语法
- 提供分布式事务支持
- 活跃的社区和持续更新
当前生产环境推荐使用5.x版本,其Maven依赖为:
xml复制<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
<version>5.2.1</version>
</dependency>
3.2 分片策略设计
3.2.1 水平分表配置
假设我们需要对订单表按order_id进行分表(分为t_order_0和t_order_1):
yaml复制spring:
shardingsphere:
datasource:
names: ds
ds:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/order_db
username: root
password: password
rules:
sharding:
tables:
t_order:
actual-data-nodes: ds.t_order_$->{0..1}
table-strategy:
standard:
sharding-column: order_id
precise-algorithm-name: t-order-inline
sharding-algorithms:
t-order-inline:
type: INLINE
props:
algorithm-expression: t_order_$->{order_id % 2}
3.2.2 分库分表复合策略
对于数据量更大的场景,可以同时实施分库和分表。例如按用户ID分库,再按订单ID分表:
yaml复制rules:
sharding:
tables:
t_order:
actual-data-nodes: ds$->{0..1}.t_order_$->{0..1}
database-strategy:
standard:
sharding-column: user_id
precise-algorithm-name: db-inline
table-strategy:
standard:
sharding-column: order_id
precise-algorithm-name: table-inline
sharding-algorithms:
db-inline:
type: INLINE
props:
algorithm-expression: ds$->{user_id % 2}
table-inline:
type: INLINE
props:
algorithm-expression: t_order_$->{order_id % 2}
3.3 分布式主键生成
ShardingSphere提供了多种分布式ID生成策略。生产环境推荐使用Snowflake算法:
yaml复制spring:
shardingsphere:
rules:
sharding:
key-generators:
snowflake:
type: SNOWFLAKE
props:
worker-id: 123
tables:
t_order:
key-generate-strategy:
column: order_id
key-generator-name: snowflake
实战经验:在Docker环境中部署时,务必确保worker-id的唯一性,否则可能导致ID冲突
3.4 自定义分片算法
当内置算法不满足需求时,可以自定义分片逻辑。例如实现按月分表:
java复制public class MonthShardingAlgorithm implements StandardShardingAlgorithm<Date> {
private DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMM");
@Override
public String doSharding(Collection<String> availableTargetNames,
PreciseShardingValue<Date> shardingValue) {
// 将日期转换为表后缀,如t_order_202301
LocalDate date = shardingValue.getValue().toInstant()
.atZone(ZoneId.systemDefault()).toLocalDate();
String suffix = date.format(formatter);
return "t_order_" + suffix;
}
// 实现其他必要方法...
}
注册自定义算法:
yaml复制sharding-algorithms:
month-sharding:
type: CLASS_BASED
props:
strategy: STANDARD
algorithm-class-name: com.example.MonthShardingAlgorithm
4. 混合架构:Dynamic + ShardingSphere
4.1 应用场景分析
在复杂的业务系统中,我们常常需要同时处理:
- 需要分片的海量数据表
- 不需要分片的配置表
- 需要读写分离的业务表
- 独立的外部数据源
这种混合场景下,单独使用ShardingSphere或Dynamic-Datasource都无法满足需求。通过将两者结合,可以实现更灵活的数据访问策略。
4.2 驱动级集成方案
4.2.1 架构设计
核心思想是将ShardingSphere作为Dynamic-Datasource管理的一个特殊数据源:
code复制应用层 → Dynamic-Datasource → [普通MySQL数据源]
→ [ShardingSphere数据源(内部管理多个物理库)]
4.2.2 具体配置
yaml复制spring:
datasource:
dynamic:
primary: common-db
datasource:
common-db:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://common-host:3306/common_db
username: user
password: pass
sharding-db:
driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver
url: jdbc:shardingsphere:absolutepath:/etc/sharding-config.yaml
其中,/etc/sharding-config.yaml是ShardingSphere的独立配置文件:
yaml复制dataSources:
ds0:
dataSourceClassName: com.zaxxer.hikari.HikariDataSource
jdbcUrl: jdbc:mysql://db-host-0:3306/db0
username: dbuser
password: dbpass
ds1:
# 第二个数据源配置...
rules:
- !SHARDING
tables:
t_order:
actualDataNodes: ds$->{0..1}.t_order_$->{0..1}
# 分片规则...
4.2.3 代码中使用
java复制@Service
public class HybridService {
// 使用普通数据源
@DS("common-db")
public Config getConfig(String key) {
return configMapper.selectById(key);
}
// 使用分片数据源
@DS("sharding-db")
public void createOrder(Order order) {
orderMapper.insert(order);
}
}
4.3 性能优化建议
-
连接池配置:
- 普通数据源:根据QPS设置合理的连接数
- 分片数据源:总连接数=物理库数量×每个库的连接数
-
事务管理:
- 跨多个物理库的操作需要使用分布式事务
- 推荐使用Seata集成方案
-
监控指标:
- 对每个数据源单独监控连接池状态
- 跟踪分片SQL的实际执行情况
5. 生产环境注意事项
5.1 常见问题排查
-
数据源切换失效:
- 检查@DS注解是否被Spring AOP代理
- 确认方法调用是否来自类内部(this.方法()调用会导致AOP失效)
-
分片路由异常:
- 检查分片键是否为NULL
- 确认分片算法与实际数据分布匹配
-
事务一致性:
- 避免在事务中跨数据源操作
- 对分布式事务做好补偿机制
5.2 数据迁移方案
当现有单表需要改造为分片表时,推荐步骤:
-
双写阶段:
- 同时写入原表和分片表
- 通过定时任务同步历史数据
-
校验阶段:
- 对比新旧数据一致性
- 修复不一致记录
-
切换阶段:
- 将读流量逐步切到分片表
- 最终停用原表写入
5.3 监控指标
关键监控项包括:
- 每个数据源的连接池使用率
- 分片SQL的执行延迟
- 分布式事务的成功率
- 数据节点之间的延迟
在Kubernetes环境中,建议通过Prometheus收集这些指标,并设置合理的告警阈值。
6. 技术选型建议
经过多个项目的实践验证,我总结出以下选型原则:
-
单纯读写分离场景:
- 数据量小:使用Dynamic-Datasource内置的负载均衡
- 数据量大:配合数据库中间件(如MySQL Router)
-
分库分表场景:
- 简单分片规则:直接使用ShardingSphere
- 复杂业务场景:采用Dynamic+ShardingSphere混合方案
-
多租户系统:
- 租户数量少:每个租户独立数据源
- 租户数量多:共享数据源+分片策略区分租户
在实际项目中,我们采用混合方案成功支撑了日订单量超过百万的电商系统。其中用户基础信息使用独立数据源,订单数据按用户ID分库分表,商品数据则按类目分片。这种架构既保证了核心业务的高性能,又保持了足够的灵活性应对业务变化。