現在有一個Mysql數據源和一個Postgresql數據源,使用Mybatis對兩個數據源進行操作:
1. 注入多數據源
可以對兩個數據源分別實現其Service層和Mapper層,以及Mybatis的配置類:
@Configuration
// 這裏需要配置掃描包路徑,以及sqlSessionTemplateRef
@MapperScan(basePackages = "com.example.mybatisdemo.mapper.mysql", sqlSessionTemplateRef = "mysqlSqlSessionTemplate")
public class MysqlMybatisConfigurer {
/**
* 注入Mysql數據源
*/
@Bean
@ConfigurationProperties(prefix = "spring.datasource.mysql")
public DataSource mysqlDatasource() {
return new DruidDataSource();
}
/**
* 注入mysqlSqlSessionFactory
*/
@Bean
public SqlSessionFactory mysqlSqlSessionFactory(DataSource mysqlDatasource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(mysqlDatasource);
// 設置對應的mapper文件
factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:" +
"/mappers/MysqlMapper.xml"));
return factoryBean.getObject();
}
/**
* 注入mysqlSqlSessionTemplate
*/
@Bean
public SqlSessionTemplate mysqlSqlSessionTemplate(SqlSessionFactory mysqlSqlSessionFactory) {
return new SqlSessionTemplate(mysqlSqlSessionFactory);
}
/**
* 注入mysqlTransactionalManager
*/
@Bean
public DataSourceTransactionManager mysqlTransactionalManager(DataSource mysqlDatasource) {
return new DataSourceTransactionManager(mysqlDatasource);
}
}
@Configuration
// 這裏需要配置掃描包路徑,以及sqlSessionTemplateRef
@MapperScan(basePackages = "com.example.mybatisdemo.mapper.postgresql", sqlSessionTemplateRef = "postgresqlSqlSessionTemplate")
public class PostgresqlMybatisConfigurer {
/**
* 注入Postgresql數據源
*/
@Bean
@ConfigurationProperties(prefix = "spring.datasource.postgresql")
public DataSource postgresqlDatasource() {
return new DruidDataSource();
}
/**
* 注入postgresqlSqlSessionFactory
*/
@Bean
public SqlSessionFactory postgresqlSqlSessionFactory(DataSource postgresqlDatasource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(postgresqlDatasource);
// 設置對應的mapper文件
factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:" +
"/mappers/PostgresqlMapper.xml"));
return factoryBean.getObject();
}
/**
* 注入postgresqlSqlSessionTemplate
*/
@Bean
public SqlSessionTemplate postgresqlSqlSessionTemplate(SqlSessionFactory postgresqlSqlSessionFactory) {
return new SqlSessionTemplate(postgresqlSqlSessionFactory);
}
/**
* 注入postgresqlTransactionalManager
*/
@Bean
public DataSourceTransactionManager postgresqlTransactionalManager(DataSource postgresqlDatasource) {
return new DataSourceTransactionManager(postgresqlDatasource);
}
}
在配置類中,分別注入了一個事務管理器TransactionManager,這個和事務管理是相關的。在使用@Transactional註解時,需要配置其value屬性指定對應的事務管理器。
2. 動態數據源
Spring中提供了AbstractRoutingDataSource抽象類,可以用於動態地選擇數據源。
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
@Nullable
private Map<Object, Object> targetDataSources;
@Nullable
private Object defaultTargetDataSource;
private boolean lenientFallback = true;
private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
@Nullable
private Map<Object, DataSource> resolvedDataSources;
@Nullable
private DataSource resolvedDefaultDataSource;
// 略
}
通過源碼可以看到,該抽象類實現了InitializingBean接口,並在其afterPropertiesSet方法中將數據源以<lookupkey, dataSource>的形式放入一個Map中。
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
} else {
this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());
this.targetDataSources.forEach((key, value) -> {
Object lookupKey = this.resolveSpecifiedLookupKey(key);
DataSource dataSource = this.resolveSpecifiedDataSource(value);
// 將數據源以<lookupkey, dataSource>的形式放入Map中
this.resolvedDataSources.put(lookupKey, dataSource);
});
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
}
}
該類中還有一個determineTargetDataSource方法,是根據lookupkey從Map中獲取對應的數據源,如果沒有獲取到,則使用默認的數據源。
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = this.determineCurrentLookupKey();
// 根據lookupkey從Map中獲取對應的數據源
DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
} else {
return dataSource;
}
}
lookupkey是通過determineTargetDataSource方法獲取到的,而它是一個抽象方法,我們要做的就是通過實現這個方法,來控制獲取到的數據源。
@Nullable
protected abstract Object determineCurrentLookupKey();
(1) 創建並注入動態數據源
創建AbstractRoutingDataSource的子類,實現determineCurrentLookupKey方法
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.get();
}
}
這裏的DataSourceContextHolder是一個操作ThreadLocal對象的工具類
public class DataSourceContextHolder {
/**
* 數據源上下文
*/
private static final ThreadLocal<DataSourceType> contextHolder = new ThreadLocal<>();
/**
* 設置數據源類型
*/
public static void set(DataSourceType type) {
contextHolder.set(type);
}
/**
* 獲取數據源類型
*
* @return DataSourceType
*/
public static DataSourceType get() {
return contextHolder.get();
}
/**
* 使用MYSQL數據源
*/
public static void mysql() {
set(DataSourceType.MYSQL);
}
/**
* 使用Postgresql數據源
*/
public static void postgresql() {
set(DataSourceType.POSTGRESQL);
}
public static void remove() {
contextHolder.remove();
}
}
通過調用DataSourceContextHolder.mysql()或者DataSourceContextHolder.postgresql()就能修改contextHolder的值,從而在動態數據源的determineTargetDataSource方法中就能獲取到對應的數據源。
在數據源配置類中,將mysql和postgresql的數據源設置到動態數據源的Map中,並注入容器。
@Configuration
public class DataSourceConfigurer {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.mysql")
public DataSource mysqlDatasource() {
return new DruidDataSource();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.postgresql")
public DataSource postgresqlDatasource() {
return new DruidDataSource();
}
@Bean
public RoutingDataSource routingDataSource(DataSource mysqlDatasource, DataSource postgresqlDatasource) {
Map<Object, Object> dataSources = new HashMap<>();
dataSources.put(DataSourceType.MYSQL, mysqlDatasource);
dataSources.put(DataSourceType.POSTGRESQL, postgresqlDatasource);
RoutingDataSource routingDataSource = new RoutingDataSource();
routingDataSource.setDefaultTargetDataSource(mysqlDatasource);
// 設置數據源
routingDataSource.setTargetDataSources(dataSources);
return routingDataSource;
}
}
(2) Mybatis配置類
由於使用了動態數據源,所以只需要編寫一個配置類即可。
@Configuration
@MapperScan(basePackages = "com.example.mybatisdemo.mapper", sqlSessionTemplateRef = "sqlSessionTemplate")
public class MybatisConfigurer {
// 注入動態數據源
@Resource
private RoutingDataSource routingDataSource;
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(routingDataSource);
// 這裏可以直接設置所有的mapper.xml文件
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath" +
":mappers/*.xml"));
return sqlSessionFactoryBean.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
@Bean
public DataSourceTransactionManager transactionalManager(DataSource mysqlDatasource) {
return new DataSourceTransactionManager(mysqlDatasource);
}
}
(3) 使用註解簡化數據源切換
我們雖然可以使用DataSourceContextHolder類中的方法進行動態數據源切換,但是這種方式有些繁瑣,不夠優雅。可以考慮使用註解的形式簡化數據源切換。
我們先定義兩個註解,表示使用Mysql數據源或Postgresql數據源:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Mysql {
}
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Postgresql {
}
再定義一個切面,當使用了註解時,會先調用切換數據源的方法,再執行後續邏輯。
@Component
@Aspect
public class DataSourceAspect {
@Pointcut("@within(com.example.mybatisdemo.aop.Mysql) || @annotation(com.example.mybatisdemo.aop.Mysql)")
public void mysqlPointcut() {
}
@Pointcut("@within(com.example.mybatisdemo.aop.Postgresql) || @annotation(com.example.mybatisdemo.aop.Postgresql)")
public void postgresqlPointcut() {
}
@Before("mysqlPointcut()")
public void mysql() {
DataSourceContextHolder.mysql();
}
@Before("postgresqlPointcut()")
public void postgresql() {
DataSourceContextHolder.postgresql();
}
}
在使用動態數據源的事務操作時有兩個需要注意的問題:
問題一 同一個事務操作兩個數據源
Mybatis使用Executor執行SQL時需要獲取連接,BaseExecutor類中的getConnection方法調用了SpringManagedTransaction中的getConnection方法,這裏優先從connection字段獲取連接,如果connection爲空,纔會調用openConnection方法,並把連接賦給connection字段。
也就是說,如果你使用的是同一個事務來操作兩個數據源,那拿到的都是同一個連接,會導致數據源切換失敗。
protected Connection getConnection(Log statementLog) throws SQLException {
Connection connection = this.transaction.getConnection();
return statementLog.isDebugEnabled() ? ConnectionLogger.newInstance(connection, statementLog, this.queryStack) : connection;
}
public Connection getConnection() throws SQLException {
if (this.connection == null) {
this.openConnection();
}
return this.connection;
}
private void openConnection() throws SQLException {
this.connection = DataSourceUtils.getConnection(this.dataSource);
this.autoCommit = this.connection.getAutoCommit();
this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);
LOGGER.debug(() -> {
return "JDBC Connection [" + this.connection + "] will" + (this.isConnectionTransactional ? " " : " not ") + "be managed by Spring";
});
}
問題二 兩個獨立事務分別操作兩個數據源
(1) 在開啓事務的時候,DataSourceTransactionManager中的doBegin方法會先獲取Connection,並保存到ConnectionHolder中,將數據源和ConnectionHolder的對應關係綁定到TransactionSynchronizationManager中。
protected void doBegin(Object transaction, TransactionDefinition definition) {
DataSourceTransactionManager.DataSourceTransactionObject txObject = (DataSourceTransactionManager.DataSourceTransactionObject)transaction;
Connection con = null;
try {
if (!txObject.hasConnectionHolder() || txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
// 獲取連接
Connection newCon = this.obtainDataSource().getConnection();
if (this.logger.isDebugEnabled()) {
this.logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
}
// 保存到ConnectionHolder中
txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
}
txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
// 從ConnectionHolder獲取連接
con = txObject.getConnectionHolder().getConnection();
// 略
// 將數據源和ConnectionHolder的關係綁定到TransactionSynchronizationManager中
if (txObject.isNewConnectionHolder()) {
TransactionSynchronizationManager.bindResource(this.obtainDataSource(), txObject.getConnectionHolder());
}
// 略
}
(2) TransactionSynchronizationManager的bindResource方法將數據源和ConnectionHolder的對應關係存入線程變量resources中。
public abstract class TransactionSynchronizationManager {
// 線程變量
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
// 略
// 綁定數據源和ConnectionHolder的對應關係
public static void bindResource(Object key, Object value) throws IllegalStateException {
Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
Assert.notNull(value, "Value must not be null");
Map<Object, Object> map = resources.get();
// set ThreadLocal Map if none found
if (map == null) {
map = new HashMap<>();
resources.set(map);
}
Object oldValue = map.put(actualKey, value);
// Transparently suppress a ResourceHolder that was marked as void...
if (oldValue instanceof ResourceHolder && ((ResourceHolder) oldValue).isVoid()) {
oldValue = null;
}
if (oldValue != null) {
throw new IllegalStateException(
"Already value [" + oldValue + "] for key [" + actualKey + "] bound to thread");
}
}
// 略
}
(3) 上邊提到的openConnection方法,其實最終也是從TransactionSynchronizationManager的resources中獲取連接的
public static Connection doGetConnection(DataSource dataSource) throws SQLException {
Assert.notNull(dataSource, "No DataSource specified");
// 獲取ConnectionHolder
ConnectionHolder conHolder = (ConnectionHolder)TransactionSynchronizationManager.getResource(dataSource);
if (conHolder == null || !conHolder.hasConnection() && !conHolder.isSynchronizedWithTransaction()) {
logger.debug("Fetching JDBC Connection from DataSource");
Connection con = fetchConnection(dataSource);
if (TransactionSynchronizationManager.isSynchronizationActive()) {
try {
ConnectionHolder holderToUse = conHolder;
if (conHolder == null) {
holderToUse = new ConnectionHolder(con);
} else {
conHolder.setConnection(con);
}
holderToUse.requested();
TransactionSynchronizationManager.registerSynchronization(new DataSourceUtils.ConnectionSynchronization(holderToUse, dataSource));
holderToUse.setSynchronizedWithTransaction(true);
if (holderToUse != conHolder) {
TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
}
} catch (RuntimeException var4) {
releaseConnection(con, dataSource);
throw var4;
}
}
return con;
} else {
conHolder.requested();
if (!conHolder.hasConnection()) {
logger.debug("Fetching resumed JDBC Connection from DataSource");
conHolder.setConnection(fetchConnection(dataSource));
}
// 從ConnectionHolder中獲取連接
return conHolder.getConnection();
}
}
也就是說,如果修改了數據源,那麼resources中就找不到對應的連接,就可以重新獲取連接,從而達到切換數據源的目的。然而我們數據源的只有一個,就是動態數據源,因此即使使用兩個獨立事務,也不能成功切換數據源。
3. 結語
如果想要使用動態數據源的事務處理,可能需要考慮使用多線程分佈式的事務處理機制;
如果使用直接注入多個數據源的方式實現事務處理,實現簡單,但是各數據源事務是獨立的;
應該根據具體情況進行選擇。