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可以"交流"。在后面我们可以看到,二者关联,比较关键的几个类是
- SpringManagedTrasaction和SpringManagedTransactionFactory和DataSourceUtils,前二者是mybatis的,在mybatis-spring包中,后者是Spring提供,三者保证Spring事务管理器和Mybatis的Statement使用的是同一连接
- 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时,有以下两种情况
- 当前线程有事务,使用该事务管理方提供的接口获取连接,关联事务
- 当前线程无事务,全由mybatis控制
如何配置多数据源
既然清楚了整个流程,我们只需要知道单数据源下Spring和Mybatis自动配置分别做了什么事,我们自己仿照配置多数据源就好了
- 在MybatisAutoConfiguration中
@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory)
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource)
其中SqlSessionFactory函数中设置了配置文件中mybatis各项配置
- 要知道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;
}
...
}
综上,我们需要配置的有四个类,均参考自动配置类重写即可
- DataSource
- TransactionManager
- SqlSessionFactory
- 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);
}
}
}
- 注意@Primary设置主数据源
- 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);
}
注意指定事务管理器,否则默认使用主数据源的事务管理器,可能导致事务失效