Spring Mybatis项目多数据源配置

文章详细介绍了SpringMybatis项目中配置多数据源的过程,包括Spring如何管理事务、事务的开始、隔离级别设置以及提交回滚等。关键点在于理解Spring如何与Mybatis协同进行事务控制,确保数据源和连接的一致性。同时,文章提到了配置多数据源时需要关注的自动配置类和重要组件,如SqlSessionFactory、SqlSessionTemplate等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Spring Mybatis项目多数据源配置

项目中使用到了多种数据库,需要配置多数据源,网上解决办法多种,但看来看去感觉总有点不靠谱,主要自己对于Spring如何管理事务,抽象统一接口供JPA、Mybatis整合,于是再翻了翻Mybatis和Spring事务管理的源码,调试了一次开启事务的数据库访问流程,如下代码所示。

@Transactional(rollbackFor = Exception.class)
public HrCandidate getCandidate(long candId) {
	candidateMapper.selectByPrimaryKey(candId);
	throw new RuntimeException("dfdfdf");
}

动态代理,事务管理开始

有了Tansactional注解,该类的该方法会被CGLib代理,由TransactionInterceptor的invokeWithinTransaction开始进入事务处理流程,关键代码如下

protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
			final InvocationCallback invocation) throws Throwable {

// If the transaction attribute is null, the method is non-transactional.
TransactionAttributeSource tas = getTransactionAttributeSource();
// txAttr即是Transactional注解中配置的属性
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
// 决定使用哪个事务管理器
final TransactionManager tm = determineTransactionManager(txAttr);

if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager) {
   // Standard transaction demarcation with getTransaction and commit/rollback calls.
   TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
   ...
}

PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
	// Standard transaction demarcation with getTransaction and commit/rollback calls.
	TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
	// 执行真正的service方法
	...

获取事务管理器

determineTransactionManager方法逻辑很简单
1、如果设置了Transactional的qualifier,则从容器中获取该Bean作为事务管理器
2、如果设置了TransactionInterceptor的transactionManagerBeanName(通常为null),从容器中获取该Bean
3、如果TransactionInterceptor的TransactionManager(可由TransactionManagementConfigurer设置)不为null,返回
4、从容器中获取事务管理器,并设置到缓存

createTransactionIfNecessary,创建事务

该方法返回TransactionStatus对象
在事务管理器的getTransaction方法中,判断了各项事务配置属性,超时时间、传播方式等

startTransaction,开始事务

在这个方法的doBegin方法中,从数据源获取了connection,设置了事务同步(供Mybatis、Jpa与Spring事务交互),关键代码

if (!txObject.hasConnectionHolder() ||
		txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
	Connection newCon = obtainDataSource().getConnection();
	if (logger.isDebugEnabled()) {
		logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
	}
	txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
}

txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
con = txObject.getConnectionHolder().getConnection();

Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
txObject.setPreviousIsolationLevel(previousIsolationLevel);
txObject.setReadOnly(definition.isReadOnly());

// Switch to manual commit if necessary. This is very expensive in some JDBC drivers,
// so we don't want to do it unnecessarily (for example if we've explicitly
// configured the connection pool to set it already).
if (con.getAutoCommit()) {
	txObject.setMustRestoreAutoCommit(true);
	if (logger.isDebugEnabled()) {
		logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
	}
	con.setAutoCommit(false);
}

prepareTransactionalConnection(con, definition);
txObject.getConnectionHolder().setTransactionActive(true);

int timeout = determineTimeout(definition);
if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
	txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
}

// Bind the connection holder to the thread.
if (txObject.isNewConnectionHolder()) {
	TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
}

最后一个if,将当前连接绑定到线程中,TransactionSynchronizationManager中有很多ThreadLocal变量,用以将当前线程的事务、连接绑定到线程,从而与Mybatis可以"交流"。在后面我们可以看到,二者关联,比较关键的几个类是

  1. SpringManagedTrasaction和SpringManagedTransactionFactory和DataSourceUtils,前二者是mybatis的,在mybatis-spring包中,后者是Spring提供,三者保证Spring事务管理器和Mybatis的Statement使用的是同一连接
  2. TransactionSynchronizationManager,Spring事务与ORM框架事务同步

doBegin之后,同步事务信息到当前线程,并prepareTransactionInfo,包装了当前事务的所有信息(事务管理器、事务属性、所切的方法等)并绑定到线程

protected void prepareSynchronization(DefaultTransactionStatus status, TransactionDefinition definition) {
if (status.isNewSynchronization()) {
	TransactionSynchronizationManager.setActualTransactionActive(status.hasTransaction());
	TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(
			definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT ?
					definition.getIsolationLevel() : null);
	TransactionSynchronizationManager.setCurrentTransactionReadOnly(definition.isReadOnly());
	TransactionSynchronizationManager.setCurrentTransactionName(definition.getName());
	TransactionSynchronizationManager.initSynchronization();
}
}
...
后面还有prepareTransactionInfo...

Spring事务准备工作完成

至此,Spring事务的准备完成了,接下来真正执行Service方法中的Mapper增删改查了,这部分在Mybatis运行流程分析已经比较清楚了,但这次还是花了不少时间看了看源码,主要想找到如何将Mybatis事务、sql执行与Spring事务管理关联起来的,关键还是在于org.ibatis.spring这个包,这个包直接依赖spring-jdbc,spring-tx,如上文提到,其中通过mybatis的SpringManagedTrasaction、SpringManagedTransactionFactory和spring的TransactionSynchronizationManager、DataSourceUtils产生关联。

整个流程的关键问题

所有的事务开始、隔离级别设置、提交回滚等都是由Spring的事务管理器执行的,而sql的执行完全由mybatis管理,二者使用的数据源、数据源的连接必须是同一个,事务才可控,因此Spring提供了DataSourceUtils和TransactionSynchronizationManager两个类完成这个同步工作。
当mybatis获取连接准备执行sql时,有以下两种情况

  1. 当前线程有事务,使用该事务管理方提供的接口获取连接,关联事务
  2. 当前线程无事务,全由mybatis控制

如何配置多数据源

既然清楚了整个流程,我们只需要知道单数据源下Spring和Mybatis自动配置分别做了什么事,我们自己仿照配置多数据源就好了

  1. 在MybatisAutoConfiguration中
@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory)

@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource)

其中SqlSessionFactory函数中设置了配置文件中mybatis各项配置

  1. 要知道Springboot自动配置了哪些,应该去找jdbc相关,即org.springframework.boot.autoconfigure.jdbc包,下面有三个自动配置类
  • JdbcTemplateAutoConfiguration:我们使用mybatis,无需关注
  • DataSourceAutoConfiguration
@Configuration(proxyBeanMethods = false)
@Conditional(PooledDataSourceCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
		DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class,
		DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class })
protected static class PooledDataSourceConfiguration {

}

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(HikariDataSource.class)
@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) {
		HikariDataSource dataSource = createDataSource(properties, HikariDataSource.class);
		if (StringUtils.hasText(properties.getName())) {
			dataSource.setPoolName(properties.getName());
		}
		return dataSource;
	}

}

使用了Springboot默认的数据源Hikari

  • DataSourceTransactionManagerAutoConfiguration,当容器中只有一个数据源的时候,该自动配置类生效
@Configuration(proxyBeanMethods = false)
@ConditionalOnSingleCandidate(DataSource.class)
static class JdbcTransactionManagerConfiguration {

	@Bean
	@ConditionalOnMissingBean(TransactionManager.class)
	DataSourceTransactionManager transactionManager(Environment environment, DataSource dataSource,
			ObjectProvider<TransactionManagerCustomizers> transactionManagerCustomizers) {
		DataSourceTransactionManager transactionManager = createTransactionManager(environment, dataSource);
		transactionManagerCustomizers.ifAvailable((customizers) -> customizers.customize(transactionManager));
		return transactionManager;
	}
...
}

综上,我们需要配置的有四个类,均参考自动配置类重写即可

  1. DataSource
  2. TransactionManager
  3. SqlSessionFactory
  4. SqlSessionTemplate

以下为一个数据源的相关配置

@Configuration
@MapperScan(basePackages = "com.windcf.springmybatismultipledatasource.mapper.mjms", sqlSessionTemplateRef = MjmsDataSourceConfig.SESSION_TEMP_NAME)
@EnableConfigurationProperties({MjmsDataSourceConfig.MjmsDataSourceProperties.class, MjmsDataSourceConfig.MjmsMybatisProperties.class})
public class MjmsDataSourceConfig {
    public static final String TRANSACTION_MANAGER = "mjmsTransactionManager";
    public static final String DATASOURCE_BEAN_NAME = "mjmsDataSource";
    public static final String SESSION_TEMP_NAME = "mjmsSqlSessionTemplate";
    public static final String SESSION_FACTORY_NAME = "mjmsSqlSessionFactory";
    private final ResourceLoader resourceLoader;

    private final MjmsMybatisProperties properties;

    public MjmsDataSourceConfig(ResourceLoader resourceLoader, MjmsMybatisProperties properties) {
        this.resourceLoader = resourceLoader;
        this.properties = properties;
        System.out.println(this.properties.getConfiguration());
    }

    @Bean
    public TransactionManager mjmsTransactionManager(@Qualifier(DATASOURCE_BEAN_NAME) DataSource dataSource) {
        return new JdbcTransactionManager(dataSource);
    }

    @ConfigurationProperties(prefix = "mybatis.mjms")
    protected static class MjmsMybatisProperties extends MybatisProperties {

    }

    @Data
    @ConfigurationProperties(prefix = "spring.datasource.mjms")
    protected static class MjmsDataSourceProperties {
        private String username;
        private String password;
        private String driverClassName;
        private String url;
    }

    @Bean
    public DataSource mjmsDataSource(MjmsDataSourceProperties properties) {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(properties.url);
        dataSource.setPassword(properties.password);
        dataSource.setUsername(properties.username);
        dataSource.setDriverClassName(properties.driverClassName);
        return dataSource;
    }

    @Bean
    public SqlSessionFactory mjmsSqlSessionFactory(@Qualifier(DATASOURCE_BEAN_NAME) DataSource dataSource) throws Exception {
        SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
        factory.setDataSource(dataSource);
        factory.setVfs(SpringBootVFS.class);
        if (StringUtils.hasText(this.properties.getConfigLocation())) {
            factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
        }
        if (this.properties.getConfigurationProperties() != null) {
            factory.setConfigurationProperties(this.properties.getConfigurationProperties());
        }
        /* 代替applyConfiguration */
        factory.setConfiguration(new org.apache.ibatis.session.Configuration());
//        applyConfiguration(factory);

//        if (!ObjectUtils.isEmpty(this.interceptors)) {
//            factory.setPlugins(this.interceptors);
//        }
        if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
            factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
        }
        if (this.properties.getTypeAliasesSuperType() != null) {
            factory.setTypeAliasesSuperType(this.properties.getTypeAliasesSuperType());
        }
        if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
            factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
        }
//        if (this.databaseIdProvider != null) {
//            factory.setDatabaseIdProvider(this.databaseIdProvider);
//        }
//        if (!ObjectUtils.isEmpty(this.typeHandlers)) {
//            factory.setTypeHandlers(this.typeHandlers);
//        }
        Resource[] mapperLocations = this.properties.resolveMapperLocations();
        if (!ObjectUtils.isEmpty(mapperLocations)) {
            factory.setMapperLocations(mapperLocations);
        }
//        Set<String> factoryPropertyNames = Stream.of(new BeanWrapperImpl(SqlSessionFactoryBean.class).getPropertyDescriptors()).map(PropertyDescriptor::getName).collect(Collectors.toSet());
//        Class<? extends LanguageDriver> defaultLanguageDriver = this.properties.getDefaultScriptingLanguageDriver();
//        if (factoryPropertyNames.contains("scriptingLanguageDrivers") && !ObjectUtils.isEmpty(this.languageDrivers)) {
//            // Need to mybatis-spring 2.0.2+
//            factory.setScriptingLanguageDrivers(this.languageDrivers);
//            if (defaultLanguageDriver == null && this.languageDrivers.length == 1) {
//                defaultLanguageDriver = this.languageDrivers[0].getClass();
//            }
//        }
//        if (factoryPropertyNames.contains("defaultScriptingLanguageDriver")) {
//            // Need to mybatis-spring 2.0.2+
//            factory.setDefaultScriptingLanguageDriver(defaultLanguageDriver);
//        }
//        applySqlSessionFactoryBeanCustomizers(factory);
        return factory.getObject();
    }

    @Bean
    public SqlSessionTemplate mjmsSqlSessionTemplate(@Qualifier(SESSION_FACTORY_NAME) SqlSessionFactory sqlSessionFactory) {
        ExecutorType executorType = this.properties.getExecutorType();
        if (executorType != null) {
            return new SqlSessionTemplate(sqlSessionFactory, executorType);
        } else {
            return new SqlSessionTemplate(sqlSessionFactory);
        }
    }
}
  1. 注意@Primary设置主数据源
  2. SqlSessionFactory参考Mybatis自动配置类,按需配置

application.yml

server:
  port: 8000

spring:
  # datasource
  datasource:
    ams:
      driver-class-name: com.mysql.cj.jdbc.Driver
      username: root
      password: '123456'
      url: jdbc:mysql://localhost:3306/healthman_test?serverTimezone=Asia/Shanghai
    mjms:
      driver-class-name: dm.jdbc.driver.DmDriver
      username: VHR
      password: '1234567890.'
      url: jdbc:dm://localhost:5237/vhr
  # jackson
  jackson:
    default-property-inclusion: non_null
    date-format: yyyy-MM-dd HH:mm:SS
    time-zone: GMT+8
  # http file
  servlet:
    multipart:
      max-request-size: 1000MB
      max-file-size: 1000MB
      resolve-lazily: true
  # default false
  #  main:
  #    allow-circular-references: false
  # mybatis
mybatis:
  ams:
    configuration:
      map-underscore-to-camel-case: true
      cache-enabled: false
      use-generated-keys: true
      jdbc-type-for-null: null
      local-cache-scope: statement
    mapper-locations: classpath:/mapper/ams/*.xml
  mjms:
    configuration:
      map-underscore-to-camel-case: true
      cache-enabled: false
      use-generated-keys: true
      jdbc-type-for-null: null
      local-cache-scope: statement
    mapper-locations: classpath:/mapper/mjms/*.xml
#  type-aliases-package: com.rufeng.healthman.pojo
#vhr:
#  work-dir: ${user.home}/.vhr

Service层

@Override
@Transactional(rollbackFor = Exception.class, transactionManager = MjmsDataSourceConfig.TRANSACTION_MANAGER)
public HrCandidate getCandidate(long candId) {
	return hrCandidateMapper.selectByPrimaryKey(candId);
}

注意指定事务管理器,否则默认使用主数据源的事务管理器,可能导致事务失效

示例代码git仓库

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值