引言
在现代企业级应用开发中,多数据源的需求日益普遍。无论是数据库读写分离、多租户架构,还是异构数据源集成,都需要我们掌握在 Spring Boot 中配置和管理多数据源的技术。本文将深入探讨 Spring Boot 数据源和事务的加载原理,并详细讲解多种实现多数据源的方案,特别是如何保证动态数据源切换的正确性。
第一部分:Spring Boot 数据源与事务加载原理
1.1 数据源自动配置机制
依赖触发 → 自动配置 → Bean创建 → 应用启动 核心组件分工:
- spring-boot-starter-*:依赖管理,召集相关组件
- spring-jdbc:提供核心 API 和编程模型
- spring-boot-autoconfigure:自动配置逻辑的实现者
数据源加载详细流程:
java
// 1. 触发条件:类路径存在 DataSource.class
@ConditionalOnClass({DataSource.class, EmbeddedDatabaseType.class})
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
// 2. 配置属性绑定
@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties {
private String url;
private String username;
private String password;
// ...
}
// 3. 数据源创建(默认HikariCP)
@ConditionalOnMissingBean(DataSource.class)
@ConditionalOnProperty(name = "spring.datasource.type",
havingValue = "com.zaxxer.hikari.HikariDataSource",
matchIfMissing = true)
static class Hikari {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.hikari")
HikariDataSource dataSource(DataSourceProperties properties) {
return properties.initializeDataSourceBuilder()
.type(HikariDataSource.class)
.build();
}
}
}
1.2 事务管理加载原理
事务管理的核心是 AOP 代理机制:
java
// 事务自动配置
@ConditionalOnClass(PlatformTransactionManager.class)
public class TransactionAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
// 事务顾问配置
@Configuration
@ConditionalOnBean(PlatformTransactionManager.class)
public class InfrastructureAdvisorAutoConfiguration {
@Bean
public BeanFactoryTransactionAttributeSourceAdvisor transactionAdvisor(
TransactionAttributeSource transactionAttributeSource,
TransactionInterceptor transactionInterceptor) {
// 创建事务切面顾问
return advisor;
}
@Bean
public TransactionInterceptor transactionInterceptor() {
// 创建事务拦截器
return interceptor;
}
}
事务执行流程:
- 为被 @Transactional 注解的 Bean 创建代理
- 方法调用时被 TransactionInterceptor 拦截
- 拦截器调用 PlatformTransactionManager 管理事务
- 执行目标业务方法
- 根据执行结果提交或回滚事务
第二部分:MyBatis 多数据源实现方案
2.1 手动配置多数据源(生产推荐)
项目结构规划:
text
src/main/java/
├── com/example/
│ ├── config/
│ │ ├── PrimaryDataSourceConfig.java
│ │ └── SecondaryDataSourceConfig.java
│ ├── entity/
│ │ ├── primary/
│ │ └── secondary/
│ ├── mapper/
│ │ ├── primary/
│ │ └── secondary/
│ └── service/
└── resources/
├── mapper/
│ ├── primary/
│ └── secondary/
└── application.yml
配置文件:
yaml
spring:
datasource:
primary:
jdbc-url: jdbc:mysql://localhost:3306/db1
username: user1
password: pass1
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 20
connection-timeout: 30000
secondary:
jdbc-url: jdbc:mysql://localhost:3306/db2
username: user2
password: pass2
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 15
connection-timeout: 30000
主数据源配置:
java
@Configuration
@MapperScan(
basePackages = "com.example.mapper.primary",
sqlSessionFactoryRef = "primarySqlSessionFactory"
)
public class PrimaryDataSourceConfig {
@Bean("primaryDataSource")
@ConfigurationProperties(prefix = "spring.datasource.primary")
@Primary
public DataSource primaryDataSource() {
return DataSourceBuilder.create().build();
}
@Bean("primarySqlSessionFactory")
@Primary
public SqlSessionFactory primarySqlSessionFactory(
@Qualifier("primaryDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
sessionFactory.setMapperLocations(
new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/primary/*.xml"));
return sessionFactory.getObject();
}
@Bean("primaryTransactionManager")
@Primary
public PlatformTransactionManager primaryTransactionManager(
@Qualifier("primaryDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
业务层使用:
java
@Service
public class BusinessService {
private final PrimaryUserMapper primaryUserMapper;
private final SecondaryOrderMapper secondaryOrderMapper;
public BusinessService(PrimaryUserMapper primaryUserMapper,
SecondaryOrderMapper secondaryOrderMapper) {
this.primaryUserMapper = primaryUserMapper;
this.secondaryOrderMapper = secondaryOrderMapper;
}
@Transactional(transactionManager = "primaryTransactionManager")
public void createUserWithPrimary(PrimaryUser user) {
primaryUserMapper.insert(user);
}
@Transactional(transactionManager = "secondaryTransactionManager")
public void createOrderWithSecondary(SecondaryOrder order) {
secondaryOrderMapper.insert(order);
}
}
2.2 动态数据源方案
动态数据源路由:
java
public class DynamicDataSource extends AbstractRoutingDataSource {
private static final ThreadLocal<String> DATA_SOURCE_KEY = new ThreadLocal<>();
public static void setDataSource(String dataSource) {
DATA_SOURCE_KEY.set(dataSource);
}
public static void clearDataSource() {
DATA_SOURCE_KEY.remove();
}
@Override
protected Object determineCurrentLookupKey() {
return DATA_SOURCE_KEY.get();
}
}
动态数据源配置:
java
@Configuration
@MapperScan(basePackages = "com.example.mapper")
public class DynamicDataSourceConfig {
@Bean
public DataSource dynamicDataSource(
@Qualifier("primaryDataSource") DataSource primaryDataSource,
@Qualifier("secondaryDataSource") DataSource secondaryDataSource) {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("primary", primaryDataSource);
targetDataSources.put("secondary", secondaryDataSource);
dynamicDataSource.setTargetDataSources(targetDataSources);
dynamicDataSource.setDefaultTargetDataSource(primaryDataSource);
return dynamicDataSource;
}
}
第三部分:保证数据源切换优先于事务开启
3.1 问题分析
在动态数据源场景中,数据源切换必须在事务开启之前执行,这是因为:
- 事务管理器在事务开始时获取数据库连接
- 连接获取依赖于当前设置的数据源
- 如果数据源切换在事务开始之后,将使用错误的数据源
3.2 解决方案
方案一:使用 @Order 注解(推荐)
java
@Aspect
@Component
@Order(0) // 最高优先级,确保在事务切面之前执行
public class DataSourceAspect {
private static final Logger logger = LoggerFactory.getLogger(DataSourceAspect.class);
@Around("@annotation(dataSource)")
public Object aroundDataSource(ProceedingJoinPoint point, DataSource dataSource) throws Throwable {
String dsName = dataSource.value();
String methodName = point.getSignature().getName();
boolean wasDataSourceSet = false;
try {
// 检查是否已有事务(警告提示)
if (TransactionSynchronizationManager.isActualTransactionActive()) {
logger.warn("检测到在事务开启后切换数据源,方法: {},数据源: {},这可能导致数据源切换失效!",
methodName, dsName);
}
// 设置数据源
DynamicDataSource.setDataSource(dsName);
wasDataSourceSet = true;
logger.debug("数据源已切换至: {},方法: {}", dsName, methodName);
// 执行目标方法
return point.proceed();
} finally {
// 清理数据源
if (wasDataSourceSet) {
DynamicDataSource.clearDataSource();
logger.debug("已清理数据源: {}", dsName);
}
}
}
}
方案二:自定义 Advisor(更精细控制)
java
@Component
public class DataSourceAdvisor extends AbstractPointcutAdvisor {
private final StaticMethodMatcherPointcut pointcut;
private final DataSourceInterceptor interceptor;
public DataSourceAdvisor(DataSourceInterceptor interceptor) {
this.interceptor = interceptor;
this.pointcut = new StaticMethodMatcherPointcut() {
@Override
public boolean matches(Method method, Class<?> targetClass) {
return method.isAnnotationPresent(DataSource.class) ||
targetClass.isAnnotationPresent(DataSource.class);
}
};
// 设置最高优先级
setOrder(Ordered.HIGHEST_PRECEDENCE);
}
@Override
public Pointcut getPointcut() {
return pointcut;
}
@Override
public Advice getAdvice() {
return interceptor;
}
}
方案三:事务同步回调(处理事务环境)
java
@Aspect
@Component
@Order(0)
public class DataSourceAspect {
@Around("@annotation(dataSource)")
public Object aroundSwitchDataSource(ProceedingJoinPoint point, DataSource dataSource) throws Throwable {
String dsName = dataSource.value();
// 如果当前没有事务,直接切换并执行
if (!TransactionSynchronizationManager.isActualTransactionActive()) {
return executeWithDataSource(point, dsName);
}
// 如果已经有事务,注册回调确保数据源清理
DynamicDataSource.setDataSource(dsName);
try {
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCompletion(int status) {
DynamicDataSource.clearDataSource();
}
}
);
return point.proceed();
} catch (Exception e) {
DynamicDataSource.clearDataSource();
throw e;
}
}
private Object executeWithDataSource(ProceedingJoinPoint point, String dsName) throws Throwable {
DynamicDataSource.setDataSource(dsName);
try {
return point.proceed();
} finally {
DynamicDataSource.clearDataSource();
}
}
}
3.3 最佳实践示例
数据源注解定义:
java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
String value() default "primary";
}
Service层使用规范:
java
@Service
public class UserService {
// 正确用法:注解在同一个方法上
@DataSource("primary")
@Transactional(transactionManager = "primaryTransactionManager")
public void createUserInPrimary(User user) {
// 业务逻辑
}
// 正确用法:注解在类上,影响所有方法
@DataSource("secondary")
@Transactional(transactionManager = "secondaryTransactionManager")
public void createUserInSecondary(User user) {
// 业务逻辑
}
// 错误用法:避免在内部方法调用时切换数据源
public void problematicMethod(User user) {
// 这里的数据源切换可能失效
switchToSecondary();
createUserInSecondary(user); // 事务可能已经使用默认数据源
}
@DataSource("secondary")
private void switchToSecondary() {
// 私有方法上的注解不会被代理拦截
}
}