🚀 Spring Boot整合多数据源:分库业务的标准做法
文章目录
- [🚀 Spring Boot整合多数据源:分库业务的标准做法](#🚀 Spring Boot整合多数据源:分库业务的标准做法)
- [🔍 一、为什么需要多数据源支持?](#🔍 一、为什么需要多数据源支持?)
-
- [💡 典型业务场景](#💡 典型业务场景)
- [⚙️ 二、多数据源集成方案对比](#⚙️ 二、多数据源集成方案对比)
-
- [💡 主流方案分析](#💡 主流方案分析)
- [🔧 方案选型建议](#🔧 方案选型建议)
- [🔄 三、动态数据源切换原理](#🔄 三、动态数据源切换原理)
-
- [💡 核心架构](#💡 核心架构)
- [⚙️ 核心代码实现](#⚙️ 核心代码实现)
- [🚀 使用示例](#🚀 使用示例)
- [🧩 四、多数据源事务管理](#🧩 四、多数据源事务管理)
-
- [💣 单事务管理器问题](#💣 单事务管理器问题)
- [💡 解决方案一:独立事务管理器](#💡 解决方案一:独立事务管理器)
- [💡 解决方案二:分布式事务](#💡 解决方案二:分布式事务)
- [⚠️ 事务管理最佳实践](#⚠️ 事务管理最佳实践)
- [🚀 五、主从读写分离实战](#🚀 五、主从读写分离实战)
-
- [💡 架构设计](#💡 架构设计)
- [⚙️ 配置示例](#⚙️ 配置示例)
- [🔧 动态路由配置](#🔧 动态路由配置)
- [⚡️ 读写分离策略](#⚡️ 读写分离策略)
- [🧪 六、整合MyBatis-Plus多数据源](#🧪 六、整合MyBatis-Plus多数据源)
-
- [💡 官方推荐方案](#💡 官方推荐方案)
- [⚙️ 配置示例](#⚙️ 配置示例)
- [🚀 注解使用](#🚀 注解使用)
- [💎 七、最佳实践总结](#💎 七、最佳实践总结)
-
- [🏆 核心实施步骤](#🏆 核心实施步骤)
- [⚠️ 避坑指南](#⚠️ 避坑指南)
- [🛠 推荐工具栈](#🛠 推荐工具栈)
- [🌟 建议](#🌟 建议)
🔍 一、为什么需要多数据源支持?
在实际业务开发中,随着系统规模的扩大,一个单一的数据源很难满足以下典型场景:
- 分库分表:将用户表、订单表、日志表拆分到不同数据库,降低单库压力。
- 读写分离:主库用于写操作,从库用于读操作,提高查询性能。
- 多租户架构:为不同客户接入独立的数据源,保障数据隔离。
如果仍使用单数据源架构,将面临以下问题:
问题 | 表现 |
---|---|
单点瓶颈 | 数据库连接数受限,性能下降 |
数据隔离困难 | 各业务之间相互影响,风险扩大化 |
扩展困难 | 无法灵活配置租户或业务线的数据库策略 |
案例分享:在电商平台中,我们将用户、订单、日志分离到不同数据库集群,使QPS提升300%,故障恢复时间缩短70%
💡 典型业务场景
业务需求 分库分表 读写分离 多租户隔离 用户库 订单库 日志库 主库写 从库读 租户A库 租户B库
⚙️ 二、多数据源集成方案对比
💡 主流方案分析
方案 | 优点 | 缺点 | 场景适配 |
---|---|---|---|
@Primary + @Qualifier |
简单明了,配置直观 | 不支持动态切换 | 静态业务分离 |
AbstractRoutingDataSource |
支持动态路由,结合 ThreadLocal 使用 | 配置复杂,事务处理较麻烦 | 动态切换读/写、多租户 |
Dynamic Datasource 中间件 | 快速集成、支持注解 + AOP + 多事务 | 引入依赖,需理解封装逻辑 | 推荐使用,兼容性强 |
🔧 方案选型建议
简单场景 原生注解 动态切换 AbstractRoutingDataSource 企业级需求 dynamic-datasource
🔄 三、动态数据源切换原理
💡 核心架构
Client AOP ThreadLocal RoutingDS DB 调用@DS注解方法 设置数据源key 执行方法 获取当前key 返回key 路由到目标数据源 返回结果 清除key Client AOP ThreadLocal RoutingDS DB
⚙️ 核心代码实现
java
// 1. 继承AbstractRoutingDataSource
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceHolder.getDataSourceKey();
}
}
// 2. 数据源上下文持有器
public class DataSourceHolder {
private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();
public static void setDataSourceKey(String key) {
CONTEXT.set(key);
}
public static String getDataSourceKey() {
return CONTEXT.get();
}
public static void clear() {
CONTEXT.remove();
}
}
// 3. 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DS {
String value() default "master";
}
// 4. AOP切面
@Aspect
@Component
public class DSAspect {
@Around("@annotation(ds)")
public Object around(ProceedingJoinPoint pjp, DS ds) throws Throwable {
String oldKey = DataSourceHolder.getDataSourceKey();
DataSourceHolder.setDataSourceKey(ds.value());
try {
return pjp.proceed();
} finally {
DataSourceHolder.setDataSourceKey(oldKey);
}
}
}
🚀 使用示例
java
@Service
public class UserService {
// 使用主库
@DS("master")
public void createUser(User user) {
userMapper.insert(user);
}
// 使用从库
@DS("slave")
public User getUser(Long id) {
return userMapper.selectById(id);
}
}
🧩 四、多数据源事务管理
💣 单事务管理器问题
java
@Service
public class OrderService {
// 跨数据源操作将失效!
@Transactional
public void createOrder(Order order) {
orderMapper.insert(order); // 订单库
userMapper.updatePoints(order.getUserId()); // 用户库
}
}
💡 解决方案一:独立事务管理器
java
// 配置主库事务管理器
@Bean
public PlatformTransactionManager masterTxManager(DataSource masterDataSource) {
return new DataSourceTransactionManager(masterDataSource);
}
// 配置从库事务管理器
@Bean
public PlatformTransactionManager slaveTxManager(DataSource slaveDataSource) {
return new DataSourceTransactionManager(slaveDataSource);
}
// 使用指定事务管理器
@Service
public class UserService {
@Transactional(transactionManager = "masterTxManager")
public void updateUser(User user) {
// ...
}
}
💡 解决方案二:分布式事务
java
// 使用Seata分布式事务
@GlobalTransactional
public void crossDbOperation() {
serviceA.update();
serviceB.update();
}
⚠️ 事务管理最佳实践
1.避免跨库事务 :尽量在单个数据源内完成事务
2.补偿机制 :无法避免时实现最终一致性
3.超时控制 :设置合理的事务超时时间
4.监控告警:实现事务失败实时告警
🚀 五、主从读写分离实战
💡 架构设计
同步 同步 同步 应用 主库 从库1 从库2 从库3
⚙️ 配置示例
java
spring:
datasource:
master:
url: jdbc:mysql://master:3306/db
username: root
password: master_pwd
driver-class-name: com.mysql.cj.jdbc.Driver
slave1:
url: jdbc:mysql://slave1:3306/db
username: read_user
password: read_pwd
driver-class-name: com.mysql.cj.jdbc.Driver
slave2:
url: jdbc:mysql://slave2:3306/db
username: read_user
password: read_pwd
driver-class-name: com.mysql.cj.jdbc.Driver
🔧 动态路由配置
java
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.master")
public DataSource masterDataSource() {
return DruidDataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.slave1")
public DataSource slave1DataSource() {
return DruidDataSourceBuilder.create().build();
}
@Bean
public DataSource routingDataSource(
@Qualifier("masterDataSource") DataSource master,
@Qualifier("slave1DataSource") DataSource slave1) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", master);
targetDataSources.put("slave1", slave1);
DynamicDataSource ds = new DynamicDataSource();
ds.setDefaultTargetDataSource(master);
ds.setTargetDataSources(targetDataSources);
return ds;
}
}
⚡️ 读写分离策略
java
// 读操作切面
@Aspect
@Component
public class ReadOnlyAspect {
@Around("@annotation(org.springframework.transaction.annotation.Transactional)")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
TransactionDefinition td = ((MethodInvocationProceedingJoinPoint) pjp).getTransactionAttribute();
if (td != null && td.isReadOnly()) {
DataSourceHolder.setDataSourceKey("slave");
}
try {
return pjp.proceed();
} finally {
DataSourceHolder.clear();
}
}
}
🧪 六、整合MyBatis-Plus多数据源
💡 官方推荐方案
xml
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
⚙️ 配置示例
yaml
spring:
datasource:
dynamic:
primary: master
strict: true
datasource:
master:
url: jdbc:mysql://master:3306/db
username: root
password: master_pwd
slave:
url: jdbc:mysql://slave:3306/db
username: read_user
password: read_pwd
🚀 注解使用
java
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> {
@DS("slave") // 从库查询
public User getBySlave(Long id) {
return getById(id);
}
@DS("master") // 主库写入
public void saveToMaster(User user) {
save(user);
}
}
💎 七、最佳实践总结
🏆 核心实施步骤
1.数据源规划:按业务划分数据源边界
2.路由策略:设计合理的数据源切换规则
3.事务管理:明确事务边界与处理方案
4.性能优化:连接池配置与监控
5.故障隔离:避免跨数据源故障扩散
⚠️ 避坑指南
问题 | 解决方案 |
---|---|
主从延迟 | 关键业务强制读主库 |
跨库事务 | 使用Seata分布式事务 |
连接泄露 | 严格使用try-with-resources |
配置错误 | 多环境配置分离 |
监控缺失 | 集成Druid监控 |
🛠 推荐工具栈
1.数据源管理 :dynamic-datasource-spring-boot-starter
2.分布式事务 :Seata
3.连接池 :Druid
4.监控工具 :Prometheus + Grafana
5.分库分表:ShardingSphere
🌟 建议
在系统中,我们采用以下策略保障数据安全:
1.写操作 :强制主库+分布式事务
2.读操作 :从库负载均衡+失败降级主库
3.数据校验 :夜间对账任务
4.熔断机制:从库故障自动切换
最后结语:多数据源架构是应对业务增长的必经之路。合理的设计能显著提升系统性能与可用性,但需警惕过度设计带来的复杂度。记住:技术服务于业务,而非相反!