您好,登錄后才能下訂單哦!
今天小編給大家分享的是Mybatis操作多數據源實現的方法,相信很多人都不太了解,為了讓大家更加了解,所以給大家總結了以下內容,一起往下看吧。一定會有所收獲的哦。
現在有一個Mysql數據源和一個Postgresql數據源,使用Mybatis對兩個數據源進行操作:
可以對兩個數據源分別實現其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屬性指定對應的事務管理器。
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();
創建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; } }
由于使用了動態數據源,所以只需要編寫一個配置類即可。
@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); } }
我們雖然可以使用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中就找不到對應的連接,就可以重新獲取連接,從而達到切換數據源的目的。然而我們數據源的只有一個,就是動態數據源,因此即使使用兩個獨立事務,也不能成功切換數據源。
如果想要使用動態數據源的事務處理,可能需要考慮使用多線程分布式的事務處理機制;
如果使用直接注入多個數據源的方式實現事務處理,實現簡單,但是各數據源事務是獨立的;
應該根據具體情況進行選擇。
關于Mybatis操作多數據源實現的方法就分享到這里了,希望以上內容可以對大家有一定的參考價值,可以學以致用。如果喜歡本篇文章,不妨把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。