Spring Boot 多数据源与事务管理深度解析:从原理到实践

引言

在现代企业级应用开发中,多数据源的需求日益普遍。无论是数据库读写分离、多租户架构,还是异构数据源集成,都需要我们掌握在 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;
    }
}

事务执行流程:

  1. 为被 @Transactional 注解的 Bean 创建代理
  2. 方法调用时被 TransactionInterceptor 拦截
  3. 拦截器调用 PlatformTransactionManager 管理事务
  4. 执行目标业务方法
  5. 根据执行结果提交或回滚事务

第二部分: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() {
        // 私有方法上的注解不会被代理拦截
    }
}
相关推荐
Yiii_x2 小时前
基于多线程机制的技术应用与性能优化
java·经验分享·笔记
uup2 小时前
包装类的 “缓存陷阱”:Integer.valueOf (128) == 128 为何为 false?
java
小徐Chao努力2 小时前
Go语言核心知识点底层原理教程【Map的底层原理】
java·golang·哈希算法
后端小张2 小时前
【AI 学习】LangChain框架深度解析:从核心组件到企业级应用实战
java·人工智能·学习·langchain·tensorflow·gpt-3·ai编程
天天摸鱼的java工程师2 小时前
后端密码存储优化:BCrypt 与 Argon2 加密方案对比
java·后端
雨中飘荡的记忆2 小时前
Vavr:让Java拥抱函数式编程的利器
java
沈千秋.2 小时前
xss.pwnfunction.com闯关(1~6)
java·前端·xss
关于不上作者榜就原神启动那件事2 小时前
Spring Data Redis 使用详解
java·redis·spring
invicinble2 小时前
java集合类(二)--map
java·开发语言·python