Spring Boot 配置多数据源并手动配置事务
- 一、为什么多数据源需要手动配置?
- 二、配置多数据源
- 1. 数据源配置类 (DataSourceConfig)
- 2. 主数据库 MyBatis 配置类 (PrimaryDbMyBatisConfig)
- 3. 从数据库 MyBatis 配置类 (SecondaryDbMyBatisConfig)
- 4. application.yml 配置
- 5. 使用 @Transactional 指定事务管理器
 
- 三、问题说明:will not be managed by Spring
- 四、总结
在现代企业级应用中,往往会涉及到多个数据库的使用。例如,一个应用可能需要同时操作不同的业务数据库,或分离读写操作。
Spring Boot 提供了对多数据源的支持,但在某些情况下,自动配置可能无法满足复杂的需求。特别是在事务管理方面,当需要在多个数据源间进行操作时,默认的事务管理方式可能不适用,或者无法满足更高的灵活性需求。
因此,手动配置多个数据源并支持事务管理是一个常见的解决方案。
一、为什么多数据源需要手动配置?
-  多数据源隔离: 在微服务架构或者不同模块之间可能会使用不同的数据库,每个数据库有独立的数据源和事务管理。如果使用自动配置的方式,Spring Boot会自动配置一个数据源和一个事务管理器,这可能会导致不同数据源的事务冲突或共享问题。因此,需要手动配置多个数据源和多个事务管理器。 
-  跨数据源的事务管理: Spring 默认的事务管理仅适用于单一数据源,如果在一个方法中涉及到多个数据源,Spring并不会自动处理这些数据源之间的事务关系。这时,我们需要显式地配置事务管理器,以便手动控制事务的提交与回滚。
-  灵活的事务控制: 有时我们可能需要在不同的数据源之间手动控制事务,比如先操作主数据源,再操作从数据源,或者在同一个方法中对多个数据源进行操作。这时,手动配置事务管理器可以为我们提供更高的灵活性和控制能力。 
二、配置多数据源
1. 数据源配置类 (DataSourceConfig)
这个配置类主要负责配置 primary_db 和 secondary_db 两个数据源,并为它们设置基本的连接属性。
@Configuration
@EnableTransactionManagement
public class DataSourceConfig {
    /**
     * 配置主数据库(primary_db)数据源
     * 
     * 这个方法使用 `@ConfigurationProperties` 注解,从 `application.yml` 配置文件中读取 
     * `spring.datasource.primary_db` 下的配置,自动配置数据源的连接信息(如 URL、用户名、密码等)。
     * 
     * @return 配置的主数据库数据源对象
     */
    @Bean(name = "primaryDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.primary_db")
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().build(); // 创建并返回数据源对象
    }
    /**
     * 配置从数据库(secondary_db)数据源
     * 
     * 该方法使用 `@ConfigurationProperties` 注解,从 `application.yml` 配置文件中读取 
     * `spring.datasource.secondary_db` 下的配置,自动配置数据源的连接信息。
     * 
     * @return 配置的从数据库数据源对象
     */
    @Bean(name = "secondaryDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.secondary_db")
    public DataSource secondaryDataSource() {
        return DataSourceBuilder.create().build(); // 创建并返回数据源对象
    }
}
-  primaryDataSource()和secondaryDataSource()方法用于创建和配置两个数据源。通过@ConfigurationProperties注解,Spring 会自动读取配置文件中的数据源信息,并将其与DataSource配置绑定。DataSourceBuilder.create().build()是 Spring Boot 提供的便捷方式来创建数据源实例。
-  @EnableTransactionManagement启用了事务管理功能,允许你在需要的地方使用@Transactional注解来声明事务。
2. 主数据库 MyBatis 配置类 (PrimaryDbMyBatisConfig)
这个配置类负责配置与主数据库(primary_db)相关的 MyBatis 组件。它包含了 MyBatis 的 SqlSessionFactory 和事务管理器配置。
@Configuration
@EnableTransactionManagement
@MapperScan(basePackages = "com.cx.mail.mapper.primary_db", sqlSessionFactoryRef = "primaryDbSqlSessionFactory")
public class PrimaryDbMyBatisConfig {
    @Autowired
    @Qualifier("primaryDataSource")
    private DataSource primaryDataSource;
    /**
     * 配置主数据库的 MyBatis SqlSessionFactory
     * 
     * 创建并配置 MyBatis 的 `SqlSessionFactory`,它是 MyBatis 用来与数据库交互的核心组件。
     * 在这个配置中,我们指定了使用的 `primaryDataSource` 数据源。
     * 
     * @return 配置好的 SqlSessionFactory 对象
     * @throws Exception 可能抛出异常,例如创建 `SqlSessionFactory` 时出错
     */
    @Bean
    public SqlSessionFactory primaryDbSqlSessionFactory() throws Exception {
        MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
        factory.setDataSource(primaryDataSource); // 设置使用的主数据库数据源
        // 配置全局设置,主要是 MetaObjectHandler,用于自动填充字段
        GlobalConfig globalConfig = GlobalConfigUtils.defaults();
        globalConfig.setMetaObjectHandler(new EntityAutoFillHandler()); // 自动填充创建时间和更新时间
        factory.setGlobalConfig(globalConfig);
        // 配置 MyBatis Plus 插件
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 添加分页插件
        factory.setPlugins(interceptor);
        // 配置 MyBatis 基本设置
        MybatisConfiguration configuration = new MybatisConfiguration();
        configuration.setMapUnderscoreToCamelCase(true); // 将数据库字段下划线命名转换为 Java 类的驼峰命名法
        configuration.setLogImpl(StdOutImpl.class); // 配置 SQL 打印
        factory.setConfiguration(configuration);
        // 配置 MyBatis Mapper XML 文件的位置
        factory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/primary_db/*.xml"));
        return factory.getObject(); // 返回创建的 SqlSessionFactory
    }
    /**
     * 配置主数据库的事务管理器
     * 
     * 该方法创建一个 `DataSourceTransactionManager`,它是 Spring 提供的事务管理器,用来处理主数据库的事务。
     * 
     * @param dataSource 主数据库的数据源
     * @return 配置好的事务管理器
     */
    @Primary
    @Bean(name = "primaryDbTransactionManager")
    public DataSourceTransactionManager primaryDbTransactionManager(@Qualifier("primaryDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource); // 返回主数据库的事务管理器
    }
}
-  primaryDbSqlSessionFactory()方法配置了主数据库的SqlSessionFactory,并指定了与该数据库交互时的 MyBatis 配置。
-  primaryDbTransactionManager()方法创建了主数据库的事务管理器(DataSourceTransactionManager),使得我们可以通过@Transactional注解来管理与主数据库相关的事务。
3. 从数据库 MyBatis 配置类 (SecondaryDbMyBatisConfig)
这个配置类与主数据库配置类似,但是它专门配置与从数据库(secondary_db)相关的 MyBatis 组件。
@Configuration
@EnableTransactionManagement
@MapperScan(basePackages = "com.cx.mail.mapper.secondary_db", sqlSessionFactoryRef = "secondaryDbSqlSessionFactory")
public class SecondaryDbMyBatisConfig {
    @Autowired
    @Qualifier("secondaryDataSource")
    private DataSource secondaryDataSource;
    /**
     * 配置从数据库的 MyBatis SqlSessionFactory
     * 
     * 创建并配置从数据库的 `SqlSessionFactory`,它将用于从数据库的 MyBatis 操作。
     * 
     * @return 配置好的 SqlSessionFactory 对象
     * @throws Exception 可能抛出异常,例如创建 `SqlSessionFactory` 时出错
     */
    @Bean(name = "secondaryDbSqlSessionFactory")
    public SqlSessionFactory secondaryDbSqlSessionFactory() throws Exception {
        MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
        factory.setDataSource(secondaryDataSource); // 设置使用的从数据库数据源
        // 配置全局设置,主要是 MetaObjectHandler,用于自动填充字段
        GlobalConfig globalConfig = GlobalConfigUtils.defaults();
        globalConfig.setMetaObjectHandler(new EntityAutoFillHandler());
        factory.setGlobalConfig(globalConfig);
        // 配置 MyBatis Plus 插件
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 添加分页插件
        factory.setPlugins(interceptor);
        // 配置 MyBatis 基本设置
        MybatisConfiguration configuration = new MybatisConfiguration();
        configuration.setMapUnderscoreToCamelCase(true); // 设置驼峰命名法
        configuration.setLogImpl(StdOutImpl.class); // 配置 SQL 打印
        factory.setConfiguration(configuration);
        // 设置 Mapper XML 文件的位置
        factory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/secondary_db/*.xml"));
        return factory.getObject(); // 返回创建的 SqlSessionFactory
    }
    /**
     * 配置从数据库的事务管理器
     * 
     * 该方法创建一个 `DataSourceTransactionManager`,它是 Spring 提供的事务管理器,用来处理从数据库的事务。
     * 
     * @param dataSource 从数据库的数据源
     * @return 配置好的事务管理器
     */
    @Bean(name = "secondaryDbTransactionManager")
    public DataSourceTransactionManager secondaryDbTransactionManager(@Qualifier("secondaryDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource); // 返回从数据库的事务管理器
    }
}
- secondaryDbSqlSessionFactory()和- secondaryDbTransactionManager()方法与主数据库的配置类类似,但它们专门用于配置和管理从数据库的 MyBatis 和事务。
4. application.yml 配置
# 本地库
spring:
  # 数据源配置 mysql
  datasource:
    primary_db:
      url: jdbc:mysql://localhost:3306/primary_db
      username: root
      password: root
      driver-class-name: com.mysql.cj.jdbc.Driver
    secondary_db:
      url: jdbc:mysql://localhost:3306/secondary_db
      username: root
      password: root
      driver-class-name: com.mysql.cj.jdbc.Driver
- 配置了 primary_db和secondary_db两个数据源的连接信息(如 URL、用户名、密码等),这些配置将被数据源配置类读取。
5. 使用 @Transactional 指定事务管理器
在多个数据源配置好之后,需要手动管理事务,确保每个数据源的操作在不同的事务上下文中进行。
使用 @Transactional 指定事务管理器,应该加在需要使用该事务管理器进行事务管理的 服务层方法或类 上。通过 @Transactional 注解来指定使用哪个事务管理器。这样,Spring 会使用指定的事务管理器来管理该方法的事务。
@Service
@Transactional(transactionManager = "primaryDbTransactionManager")  // 显式指定事务管理器
public class AdminServiceImpl implements AdminService {
    @Autowired
    private FakeDomainMapper fakeDomainMapper;
    @Autowired
    private FakeUserService fakeUserService;
    /**
     * 新增域名
     */
    @Override
    public void addFakeDomain(String domain, String domainDescription) {
        String loginUsername = FakeSecurityUtil.getUsername();
        if (!fakeUserService.isFakeUserAdmin(loginUsername)) {
            throw new FakeServiceException("FORBIDDEN");
        }
        FakeDomainEntity fakeDomainEntity = new FakeDomainEntity();
        fakeDomainEntity.setFakeDomain(domain);
        if (domainDescription != null) {
            fakeDomainEntity.setFakeDescription(domainDescription);
        }
        fakeDomainEntity.setFakeCreated(new Date());
        fakeDomainEntity.setFakeModified(new Date());
        // 设置默认的虚拟配额
        String fakeDefaultSettings = "default_user_quota:2048;";
        fakeDomainEntity.setFakeSettings(fakeDefaultSettings);
        fakeDomainMapper.insertFakeDomain(fakeDomainEntity);
    }
}
- @Transactional(transactionManager = "primaryDbTransactionManager")使得- addFakeDomain方法使用- primaryDbTransactionManager来管理事务,这样该方法的事务操作会应用到- primaryDataSource数据源上。
三、问题说明:will not be managed by Spring
在使用 @Transactional 注解时,如果有多个事务管理器(例如多数据源配置),需要特别注意 指定事务管理器。
否则,Spring 无法自动决定使用哪个事务管理器来管理当前方法的事务,可能导致事务 无法被 Spring 管理,从而出现错误或者无法保证事务的一致性。
如果没有指定正确的事务管理器,Spring 可能会抛出类似如下的错误:

这个问题通常出现在以下两种情况下:
-  多个数据源: 
 应用程序配置了多个数据源,每个数据源有自己的事务管理器。Spring 可能不知道应该使用哪个事务管理器来管理当前方法的事务。
-  未指定事务管理器: 
 在有多个事务管理器的环境下,@Transactional默认使用主数据源的事务管理器。如果你没有显式指定事务管理器,可能会导致事务无法被正确管理,或者使用错误的事务管理器。
四、总结
通过上述配置,系统能够实现同时操作两个不同的数据源,每个数据源有独立的事务管理、MyBatis 配置等,确保了在进行跨库操作时能够稳定运行。每个模块的配置都进行了明确的注释,方便维护和修改。
-  在有 多个数据源 或 多个事务管理器 的情况下,必须显式指定 transactionManager,否则事务将不会被 Spring 管理,可能导致错误。
-  使用 @Transactional(transactionManager = "primaryDbTransactionManager")来明确指定事务管理器。
-  默认情况下,如果没有显式指定事务管理器,Spring 只会使用主数据源的事务管理器,其他数据源的事务管理器将无法生效。 
-  通过正确的事务管理器配置,可以确保每个方法或服务层类的事务控制是正确的,从而避免由于事务管理不当导致的数据库操作异常或数据不一致问题。 



















