有些web應用需要連接多個數據源,本文講解一下如何使用多個數據源,大題思路是這樣的,系統啓動的時候創建多個數據源,然後具體執行sql的時候去切換數據源執行對應的sql。如何切換數據源呢?spring提供了一個AbstractRoutingDataSource抽象類,只要繼承這個類就可以了,這個類需要設置多個數據源,每個數據源有一個key對應,繼承這個類必須實現determineCurrentLookupKey()方法,這個方法返回一個Object值,這個值應該是數據源的key,執行sql的時候會調用這個方法 獲取key,然後根據這個key獲取到的數據源執行sql。下面看具體的例子。
前面說了determineCurrentLookupKey()方法的返回值決定,選擇什麼數據庫,方法執行的時候如何動態設置返回值呢?爲了不影響其他線程的使用,使用線程本地變量是最好的,這樣只會影響當前線程。看如下類
public class DataSourceRouter {
//默認數據源的key
public final static String DEFAULT = "default";
//數據源1的key
public final static String KEY_ONE = "key_one";
private final static ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> DEFAULT);
/**
* 獲取線程本地變量
*
* @return
*/
static String getCurrentDataSourceKey() {
return threadLocal.get();
}
/**
* @param key 數據源對應的key
* @param supplier sql執行的方法
* @param <T>
* @return sql執行的結果
*/
public static <T> T doWithKey(String key, Supplier<T> supplier) {
threadLocal.set(key);
T t = supplier.get();
threadLocal.set(DEFAULT);
return t;
}
}
這裏通過doWithKey執行sql,通過determineCurrentLookupKey動態獲取當前數據源。
下面來實現AbstractRoutingDataSource
public class DynamicDataSource extends AbstractRoutingDataSource {
@Autowired
ApplicationContext applicationContext;
/**
* 是否在創建數據源時,立即初始化連接
*/
private boolean initConnectionsOnCreate = false;
/**
* 返回配置的數據源的key,這樣下面執行的sql就是使用該數據源的
*
* @return
*/
@Override
protected Object determineCurrentLookupKey() {
return DataSourceRouter.getCurrentDataSourceKey();
}
public void init() {
DruidDataSource defaultTargetDataSource = applicationContext.getBean("defaultDataSource", DruidDataSource.class);
DruidDataSource dataSource1 = applicationContext.getBean("dataSourceTemplate", DruidDataSource.class);
dataSource1.setUrl("jdbc:mysql://localhost:3306/social?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC");
dataSource1.setUsername("root");
dataSource1.setPassword("998973");
if (initConnectionsOnCreate) {
try {
defaultTargetDataSource.init();
dataSource1.init();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
Map<Object, Object> map = new HashMap<>();
map.put(DataSourceRouter.DEFAULT, defaultTargetDataSource);
map.put(DataSourceRouter.KEY_ONE, dataSource1);
setTargetDataSources(map);
//默認先執行配置文件中的targetDataSources屬性的數據源,這裏設置的數據源不會生效,必須調用afterPropertiesSet
afterPropertiesSet();
}
public void setInitConnectionsOnCreate(boolean initConnectionsOnCreate) {
this.initConnectionsOnCreate = initConnectionsOnCreate;
}
}
這裏說明了,配置文件中配置的targetDataSources是必配的,對象創建的時候會讀取這個配置然後調用afterPropertiesSet加載數據源,因爲我們這裏的數據源可能通過數據庫等其他途徑獲取,所以沒有寫在配置文件中,這裏需要手動調用一下afterPropertiesSet重新設置一下數據源。
配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- 數據源模板,動態增加數據源時需要用到,scope是prototype,非單例對象 -->
<bean id="dataSourceTemplate" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="filters" value="stat"/>
<property name="maxActive" value="20"/>
<property name="initialSize" value="1"/>
<property name="maxWait" value="60000"/>
<property name="minIdle" value="1"/>
<property name="timeBetweenEvictionRunsMillis" value="60000"/>
<property name="minEvictableIdleTimeMillis" value="300000"/>
<property name="testWhileIdle" value="true"/>
<property name="testOnBorrow" value="false"/>
<property name="testOnReturn" value="false"/>
<property name="poolPreparedStatements" value="true"/>
<property name="maxOpenPreparedStatements" value="20"/>
<property name="asyncInit" value="true"/>
</bean>
<bean id="defaultDataSource" parent="dataSourceTemplate">
<property name="url"
value="jdbc:mysql://localhost:3306/easytour?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC"/>
<property name="username" value="root"/>
<property name="password" value="998973"/>
</bean>
<!--統一的dataSource-->
<bean id="dynamicDataSource" class="com.zhan.design.config.datasource.DynamicDataSource" init-method="init">
<!-- 設置true時,隨便一個key,找不到就走默認數據源,很可能帶來不好的效果 -->
<property name="lenientFallback" value="false"/>
<property name="targetDataSources">
<map></map>
</property>
<!--設置默認的dataSource-->
<property name="defaultTargetDataSource" ref="defaultDataSource">
</property>
<!--是否在創建數據源時,立即初始化連接-->
<property name="initConnectionsOnCreate" value="true"/>
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dynamicDataSource"/>
<!--掃描mybatis配置文件,在哪裏可以做細配置-->
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<!--掃描映射文件所在目錄-->
<property name="mapperLocations" value="classpath:com/zhan/design/mapper/**/*.xml"/>
</bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--掃描接口的基礎包,會把該包下面的所有接口註冊爲spring的bean-->
<property name="basePackage" value="com.zhan.design.dao"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>
<!--配置spring的事務-->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dynamicDataSource"/>
</bean>
<!--開啓事務註解-->
<tx:annotation-driven transaction-manager="txManager"/>
</beans>
這樣就完成了
下面看下具體使用的例子
@GetMapping("/test")
public Map<String, String> test() {
//通過調用doWithKey就完成對數據源的切換了
String label1 = DataSourceRouter.doWithKey(DataSourceRouter.DEFAULT, () -> dictionaryService.getByTypeAndKey("1", "1"));
String label2 = DataSourceRouter.doWithKey(DataSourceRouter.KEY_ONE, () -> dictionaryService.getByTypeAndKey("1", "1"));
Map<String, String> map = new HashMap<>();
map.put("default_label", label1);
map.put("one_label", label2);
return map;
}
至此就完成了。