1. 为什么需要动态切换数据源?
在典型的SpringBoot应用中,我们通常配置单一数据源连接数据库。但随着业务复杂度提升,以下场景变得常见:
- 多租户系统需要根据用户身份切换不同数据库
- 读写分离架构中需要区分查询和写入操作
- 分库分表场景下需要路由到不同物理库
- 跨数据源事务需要同时操作多个数据库
传统做法是在代码中硬编码多个DataSource实例,手动获取连接。这种方式存在明显问题:
- 连接管理混乱:每个DAO层都需要处理数据源选择逻辑
- 事务难以控制:跨数据源操作缺乏统一事务管理
- 资源浪费:无法有效复用连接池
- 侵入性强:业务代码与数据源耦合度高
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心实现方案设计
2.1 技术选型分析
实现动态数据源主要有三种技术路线:
- AbstractRoutingDataSource:Spring提供的抽象类,通过determineCurrentLookupKey()方法动态决定数据源
- MyBatis插件:通过拦截SQL执行,在运行时切换连接
- AOP切面:在方法调用前后切换数据源
经过对比,AbstractRoutingDataSource方案具有明显优势:
- 原生Spring支持,与事务管理兼容性好
- 无需修改MyBatis/MyBatis-Plus原有逻辑
- 实现简单,只需重写一个关键方法
- 性能损耗极小(仅多一次Map查找)
2.2 架构设计要点
完整实现需要以下核心组件:
- 数据源容器:管理所有候选数据源的初始化与销毁
- 路由决策器:根据当前上下文返回数据源标识
- 上下文管理器:使用ThreadLocal保存当前数据源标识
- 事务增强:确保事务边界内的数据源一致性
关键类关系图:
code复制[客户端代码] -> [DataSource]
-> [AbstractRoutingDataSource]
-> [多个真实DataSource]
-> [ThreadLocal上下文]
3. 详细实现步骤
3.1 基础环境准备
首先确保项目中包含必要依赖:
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
3.2 动态数据源核心实现
创建动态数据源类:
java复制public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceKey();
}
}
实现线程安全的上下文管理器:
java复制public class DataSourceContextHolder {
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.3 多数据源配置
在application.yml中配置多个数据源:
yaml复制spring:
datasource:
master:
url: jdbc:mysql://localhost:3306/master
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
slave1:
url: jdbc:mysql://localhost:3306/slave1
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
Java配置类初始化:
java复制@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.slave1")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public DataSource dynamicDataSource(
@Qualifier("masterDataSource") DataSource master,
@Qualifier("slaveDataSource") DataSource slave) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", master);
targetDataSources.put("slave1", slave);
DynamicDataSource dataSource = new DynamicDataSource();
dataSource.setTargetDataSources(targetDataSources);
dataSource.setDefaultTargetDataSource(master);
return dataSource;
}
}
3.4 MyBatis-Plus集成配置
确保MyBatis使用我们的动态数据源:
java复制@Configuration
@MapperScan("com.example.mapper")
public class MybatisConfig {
@Bean
public SqlSessionFactory sqlSessionFactory(
DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
factory.setDataSource(dataSource);
return factory.getObject();
}
}
4. 使用示例与最佳实践
4.1 基本使用模式
在服务层切换数据源:
java复制@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public User getFromMaster(Long id) {
DataSourceContextHolder.setDataSourceKey("master");
try {
return userMapper.selectById(id);
} finally {
DataSourceContextHolder.clear();
}
}
public User getFromSlave(Long id) {
DataSourceContextHolder.setDataSourceKey("slave1");
try {
return userMapper.selectById(id);
} finally {
DataSourceContextHolder.clear();
}
}
}
4.2 注解驱动方案
更优雅的方式是自定义注解:
java复制@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
String value() default "master";
}
通过AOP实现自动切换:
java复制@Aspect
@Component
public class DataSourceAspect {
@Before("@annotation(dataSource)")
public void beforeSwitch(DataSource dataSource) {
DataSourceContextHolder.setDataSourceKey(dataSource.value());
}
@After("@annotation(dataSource)")
public void afterSwitch(DataSource dataSource) {
DataSourceContextHolder.clear();
}
}
使用示例:
java复制@DataSource("slave1")
public User getFromSlave(Long id) {
return userMapper.selectById(id); // 自动使用slave1数据源
}
5. 关键问题与解决方案
5.1 ThreadLocal内存泄漏
常见误区是忘记清理ThreadLocal,导致内存泄漏。必须确保:
- 在finally块中调用clear()
- 使用拦截器/AOP确保清理
- 对于Web应用,可添加Filter清理
改进的AOP实现:
java复制@Around("@annotation(dataSource)")
public Object around(ProceedingJoinPoint pjp, DataSource dataSource) throws Throwable {
try {
DataSourceContextHolder.setDataSourceKey(dataSource.value());
return pjp.proceed();
} finally {
DataSourceContextHolder.clear();
}
}
5.2 事务一致性处理
默认情况下,事务开启时会锁定数据源。解决方案:
- 声明式事务:在@Transactional方法开始前就确定数据源
java复制@Transactional
@DataSource("master")
public void updateUser(User user) {
// 事务内所有操作使用master数据源
}
- 编程式事务:使用TransactionTemplate
java复制public void complexOperation() {
DataSourceContextHolder.setDataSourceKey("master");
try {
transactionTemplate.execute(status -> {
// 事务操作
return null;
});
} finally {
DataSourceContextHolder.clear();
}
}
5.3 动态数据源监控
建议添加以下监控措施:
- 记录数据源切换日志
java复制public class LoggingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
String key = DataSourceContextHolder.getDataSourceKey();
log.debug("Current datasource: {}", key);
return key;
}
}
- 统计各数据源使用频率
- 连接池健康检查
6. 高级应用场景
6.1 基于分库分表的动态路由
实现分库算法:
java复制public class ShardingDataSourceRouter {
public static String determineDataSourceKey(Long userId) {
int dbIndex = userId % 2;
return "ds_" + dbIndex;
}
}
// 使用
String dsKey = ShardingDataSourceRouter.determineDataSourceKey(userId);
DataSourceContextHolder.setDataSourceKey(dsKey);
6.2 读写分离自动路由
通过SQL类型自动选择:
java复制@Aspect
@Component
public class ReadWriteDataSourceAspect {
@Before("execution(* com.example.mapper.*.*(..))")
public void beforeMethod(JoinPoint jp) {
String methodName = jp.getSignature().getName();
if (methodName.startsWith("get") ||
methodName.startsWith("select") ||
methodName.startsWith("find")) {
DataSourceContextHolder.setDataSourceKey("slave");
} else {
DataSourceContextHolder.setDataSourceKey("master");
}
}
@After("execution(* com.example.mapper.*.*(..))")
public void afterMethod() {
DataSourceContextHolder.clear();
}
}
6.3 多租户系统集成
结合租户上下文:
java复制public class TenantDataSourceRouter {
public static String getCurrentTenantDataSource() {
String tenantId = TenantContext.getCurrentTenant();
return "tenant_" + tenantId;
}
}
// 在Filter中设置
String tenantId = request.getHeader("X-Tenant-ID");
TenantContext.setCurrentTenant(tenantId);
DataSourceContextHolder.setDataSourceKey(
TenantDataSourceRouter.getCurrentTenantDataSource());
7. 性能优化建议
- 连接池配置:为每个数据源单独优化
yaml复制spring:
datasource:
master:
hikari:
maximum-pool-size: 20
minimum-idle: 5
slave1:
hikari:
maximum-pool-size: 30
minimum-idle: 10
- 预热连接池:应用启动时初始化连接
java复制@PostConstruct
public void init() {
DataSourceContextHolder.setDataSourceKey("master");
try {
masterDataSource.getConnection().close();
} catch (SQLException e) {
log.error("Master DS warmup failed", e);
}
// 同理预热其他数据源
}
-
缓存数据源实例:避免重复创建
-
监控与动态调整:根据负载自动增减连接
8. 常见问题排查
8.1 数据源未切换
检查步骤:
- 确认determineCurrentLookupKey()被调用
- 检查ThreadLocal值是否正确传递
- 验证AOP拦截点是否正确
- 查看事务传播行为
8.2 事务失效场景
典型情况:
- 自调用问题(同类方法调用)
- 异常处理不当(吞掉异常)
- 传播属性配置错误
解决方案:
java复制// 正确的事务注解示例
@Transactional(propagation = Propagation.REQUIRED,
rollbackFor = Exception.class)
@DataSource("master")
public void transactionalMethod() {
// ...
}
8.3 MyBatis缓存问题
多数据源环境下可能出现缓存混乱。建议:
- 禁用二级缓存
xml复制<settings>
<setting name="cacheEnabled" value="false"/>
</settings>
- 为不同数据源配置不同的缓存实例
- 在数据源切换时清除相关缓存
9. 生产环境注意事项
- Failover机制:当从库不可用时自动切主库
java复制public String getReadDataSource() {
if (slaveHealthChecker.isHealthy()) {
return "slave1";
}
return "master";
}
- 灰度发布策略:新数据源逐步切换
- 配置中心集成:动态调整数据源参数
- 压力测试:验证多数据源并发能力
10. 扩展思考
10.1 与SpringCloud整合
在分布式环境中,可以:
- 通过ConfigServer统一管理数据源配置
- 结合Ribbon实现数据源级别的负载均衡
- 使用Sentinel进行数据源熔断
10.2 多数据源事务方案
对于跨数据源事务,可考虑:
- JTA分布式事务(性能较差)
- 最终一致性模式(Saga模式)
- 本地消息表方案
10.3 无侵入式实现
通过字节码增强(如JavaAgent)实现完全透明的数据源路由,但对技术要求较高。
在实际项目中,我建议根据团队技术能力选择合适的实现复杂度。对于大多数应用,基于AbstractRoutingDataSource+ThreadLocal的方案已经能很好满足需求。关键是要建立规范的使用模式,避免团队成员滥用数据源切换导致难以维护的代码。
