springboot 動態資料來源(Mybatis+Druid)
Spring多資料來源實現的方式大概有2中,一種是新建多個 MapperScan
掃描不同包,另外一種則是通過繼承 AbstractRoutingDataSource
實現動態路由。今天作者主要基於後者做的實現,且方式1的實現比較簡單這裡不做過多探討。
實現方式
方式1的實現(核心程式碼):
@Configuration @MapperScan(basePackages = "com.goofly.test1", sqlSessionTemplateRef= "test1SqlSessionTemplate") public class DataSource1Config1 { @Bean(name = "dataSource1") @ConfigurationProperties(prefix = "spring.datasource.test1") @Primary public DataSource testDataSource() { return DataSourceBuilder.create().build(); } // .....略 } @Configuration @MapperScan(basePackages = "com.goofly.test2", sqlSessionTemplateRef= "test1SqlSessionTemplate") public class DataSourceConfig2 { @Bean(name = "dataSource2") @ConfigurationProperties(prefix = "spring.datasource.test2") @Primary public DataSource testDataSource() { return DataSourceBuilder.create().build(); } // .....略 }
方式2的實現(核心程式碼):
public class DynamicRoutingDataSource extends AbstractRoutingDataSource { private static final Logger log = Logger.getLogger(DynamicRoutingDataSource.class); @Override protected Object determineCurrentLookupKey() { //從ThreadLocal中取值 return DynamicDataSourceContextHolder.get(); } }
第1種方式雖然實現比較加單,劣勢就是不同資料來源的mapper檔案不能在同一包名,就顯得不太靈活了。所以為了更加靈活的作為一個元件的存在,作者採用的第二種方式實現。
設計思路
- 當請求經過被註解修飾的類後,此時會進入到切面邏輯中。
- 切面邏輯會獲取註解中設定的key值,然後將該值存入到
ThreadLocal
中 - 執行完切面邏輯後,會執行
AbstractRoutingDataSource.determineCurrentLookupKey()
方法,然後從ThreadLocal
中獲取之前設定的key值,然後將該值返回。 - 由於
AbstractRoutingDataSource
的targetDataSources
是一個map,儲存了資料來源key和資料來源的對應關係,所以能夠順利的找到該對應的資料來源。
原始碼解讀
org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource
,如下:
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean { private Map<Object, Object> targetDataSources; private Object defaultTargetDataSource; private boolean lenientFallback = true; private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup(); private Map<Object, DataSource> resolvedDataSources; private DataSource resolvedDefaultDataSource; protected DataSource determineTargetDataSource() { Assert.notNull(this.resolvedDataSources, "DataSource router not initialized"); Object lookupKey = determineCurrentLookupKey(); 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 + "]"); } return dataSource; } /** * Determine the current lookup key. This will typically be * implemented to check a thread-bound transaction context. * <p>Allows for arbitrary keys. The returned key needs * to match the stored lookup key type, as resolved by the * {@link #resolveSpecifiedLookupKey} method. */ protected abstract Object determineCurrentLookupKey(); //........略
targetDataSources
是一個map結構,儲存了key與資料來源的對應關係;
dataSourceLookup
是一個 DataSourceLookup
型別,預設實現是 JndiDataSourceLookup
。點開該類原始碼會發現,它實現了通過key獲取DataSource的邏輯。當然,這裡可以通過 setDataSourceLookup()
來改變其屬性,因為關於此處有一個坑,後面會講到。
public class JndiDataSourceLookup extends JndiLocatorSupport implements DataSourceLookup { public JndiDataSourceLookup() { setResourceRef(true); } @Override public DataSource getDataSource(String dataSourceName) throws DataSourceLookupFailureException { try { return lookup(dataSourceName, DataSource.class); } catch (NamingException ex) { throw new DataSourceLookupFailureException( "Failed to look up JNDI DataSource with name '" + dataSourceName + "'", ex); } } }
配置示例
多資料來源
# db1 spring.datasource.master.url = jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=false spring.datasource.master.username = root spring.datasource.master.password = 123456 spring.datasource.master.driverClassName = com.mysql.jdbc.Driver spring.datasource.master.validationQuery = true spring.datasource.master.testOnBorrow = true ## db2 spring.datasource.slave.url = jdbc:mysql://127.0.0.1:3306/test1?useUnicode=true&characterEncoding=utf8&useSSL=false spring.datasource.slave.username = root spring.datasource.slave.password = 123456 spring.datasource.slave.driverClassName = com.mysql.jdbc.Driver spring.datasource.slave.validationQuery = true spring.datasource.slave.testOnBorrow = true #主資料來源名稱 spring.maindb=master #mapperper包路徑 mapper.basePackages =com.btps.xli.multidb.demo.mapper
### 單資料來源 為了讓使用者能夠用最小的改動實現最好的效果,作者對單資料來源的多種配置做了相容。 配置1:
spring.datasource.master.url = jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=false
spring.datasource.master.username = root
spring.datasource.master.password = 123456
spring.datasource.master.driverClassName = com.mysql.jdbc.Driver
spring.datasource.master.validationQuery = true
spring.datasource.master.testOnBorrow = true
mapper包路徑
mapper.basePackages = com.goofly.xli.multidb.demo.mapper
主資料來源名稱
spring.maindb=master
配置2
spring.datasource.url = jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=false
spring.datasource.username = root
spring.datasource.password = 123456
spring.datasource.driverClassName = com.mysql.jdbc.Driver
spring.datasource.validationQuery = true
spring.datasource.testOnBorrow = true
mapper包路徑
mapper.basePackages = com.goofly.xli.multidb.demo.mapper
## 踩坑之路 ### 多資料來源的迴圈依賴
Description:
The dependencies of some of the beans in the application context form a cycle:
happinessController (field private com.db.service.HappinessService com.db.controller.HappinessController.happinessService)
↓
happinessServiceImpl (field private com.db.mapper.MasterDao com.db.service.HappinessServiceImpl.masterDao)
↓
masterDao defined in file [E:GitRepositoryframework-graytest-dbtargetclassescomdbmapperMasterDao.class]
↓
sqlSessionFactory defined in class path resource [com/goofly/xli/datasource/core/DynamicDataSourceConfiguration.class]
┌─────┐
| dynamicDataSource defined in class path resource [com/goofly/xli/datasource/core/DynamicDataSourceConfiguration.class]
↑ ↓
| firstDataSource defined in class path resource [com/goofly/xli/datasource/core/DynamicDataSourceConfiguration.class]
↑ ↓
| dataSourceInitializer
**解決方案:** 在Spring boot啟動的時候排除`DataSourceAutoConfiguration`即可。如下:
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class DBMain {
public static void main(String[] args) { SpringApplication.run(DBMain.class, args); }
}
> 但是作者在建立多資料來源的時候由於並未建立多個`DataSource`的Bean,而是隻建立了一個即需要做動態資料來源的那個Bean。 其他的`DataSource`則直接建立例項然後存放在Map裡面,然後再設定到`DynamicRoutingDataSource#setTargetDataSources`即可。 > >因此這種方式也不會出現迴圈依賴的問題! ### 動態重新整理資料來源 > 筆者在設計之初是想構建一個動態重新整理資料來源的方案,所以利用了`SpringCloud`的`@RefreshScope`去標註資料來源,然後利用`RefreshScope#refresh`實現重新整理。但是在實驗的時候發現由Druid建立的資料來源會因此而關閉,由Spring的`DataSourceBuilder`建立的資料來源則不會發生任何變化。 > > 最後關於此也沒能找到解決方案。同時思考,如果只能的可以實現動態重新整理的話,那麼資料來源的原有連線會因為重新整理而中斷嗎還是會有其他處理? ### 多資料來源事務 > 有這麼一種特殊情況,一個事務中呼叫了兩個不同資料來源,這個時候動態切換資料來源會因此而失效。 > > 翻閱了很多文章,大概找了2中解決方案,一種是Atomikos進行事務管理,但是貌似效能並不是很理想。 > >另外一種則是通過優先順序控制,切面的的優先順序必須要大於資料來源的優先順序,用註解`@Order`控制。 >