1、 背景
之前做過一個數據遷移的項目,簡單來說就是將一個數據庫里面的數據遷移到另外一個數據庫。這樣的應用必然會涉及到多個數據源連接的問題,並且還要保證系統運行過程中數據源能夠隨意切換,查詢想要的數據。想要達到這個目的其實也不難,我們可以直接使用jdbc連接數據庫,在需要使用什么數據源的時候就直接獲取對應的連接,並進行后續操作。但是這種方法有兩個原因導致很多人不願意使用:1,需要自己寫相應的事務控制代碼;2,一般系統都是使用mybatis框架做數據庫操作,這樣會導致系統代碼風格不統一。所以,今天我要介紹的方法是基於Spring+Mybatis框架的多數據源處理。
2、 Spring數據源路由
Spring2.0后增加一個AbstractRoutingDataSource類用來做數據源路由,實現數據源切換的功能就是自定義一個類擴展AbstractRoutingDataSource抽象類,通過重寫抽象類中的方法determineCurrentLookupKey()來確定具體的數據源,具體實現代碼如下:
1 public class DynamicDataSource extends AbstractRoutingDataSource { 2 @Resource(name = "dynamicDataSourceSelector") 3 private DataSourceSelector dynamicDataSourceSelector; 4 5 @Override 6 protected Object determineCurrentLookupKey() { 7 return dynamicDataSourceSelector.getRouteKey(); 8 } 9 }
通過自定義的一個DataSourceSelector來設置需要路由的數據源Key,實現代碼如下(選擇過程可以按照需求自行變換):
1 public class DataSourceSelector { 2 3 private static ThreadLocal<String> localRouteKey = new ThreadLocal<>(); 4 public void setRouteKey(String routeKey){ 5 localRouteKey.set(routeKey); 6 } 7 8 public String getRouteKey(){ 9 return localRouteKey.get(); 10 } 11 12 }
在xml文件中配置多個數據源:
1 <!-- 配置數據源 --> 2 <!-- 數據源1 --> 3 <bean id="dynamicBaseDataSource1" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> 4 <property name="url" value="jdbc:mysql://localhost:3306?useUnicode=true&characterEncoding=UTF-8"/> 5 <property name="username" value="root"/> 6 <property name="password" value="root"/> 7 </bean> 8 <!-- 數據源2 --> 9 <bean id="dynamicBaseDataSource2" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> 10 <property name="url" value="jdbc:mysql://112.74.223.43:3306?useUnicode=true&characterEncoding=UTF-8"/> 11 <property name="username" value="root"/> 12 <property name="password" value="******"/> 13 </bean> 14 <!-- 數據源3 --> 15 <bean id="dynamicBaseDataSource3" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> 16 <property name="url" value="jdbc:mysql://21.123.45.14:3306?useUnicode=true&characterEncoding=UTF-8"/> 17 <property name="username" value="root"/> 18 <property name="password" value="******"/> 19 </bean>
還需要配置多個數據源對應的Key的映射關系:
1 <bean id="dynamicDataSource" class="com.guigui.datasource.DynamicDataSource"> 2 <property name="targetDataSources"> 3 <map> 4 <!-- 多個數據源Key-value列表 --> 5 <entry key="dynamicDS1" value-ref="dynamicBaseDataSource1"/> 6 <entry key="dynamicDS2" value-ref="dynamicBaseDataSource2"/> 7 <entry key="dynamicDS3" value-ref="dynamicBaseDataSource3"/> 8 </map> 9 </property> 10 </bean>
SessionFactory以及事務等配置如下:
1 <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> 2 <property name="basePackage" value="com.guigui.dynamic.dao"/> 3 <property name="sqlSessionFactoryBeanName" value="dynamicSqlSessionFactory"/> 4 </bean> 5 6 <bean id="dynamicDataSourceSelector" class="com.guigui.datasource.DataSourceSelector" /> 7 8 <!-- 事務管理相關配置... --> 9 <bean id="dynamicTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> 10 <property name="dataSource" ref="dynamicDataSource"/> 11 </bean> 12 13 <aop:config> 14 <aop:pointcut id="dynamicTxOperation" expression="execution(* com.guigui.dynamic.service.*Service.*(..))" /> 15 <aop:advisor id="dynamicAdvisor" pointcut-ref="dynamicTxOperation" advice-ref="dynamicAdvice"/> 16 </aop:config> 17 18 <tx:advice id="dynamicAdvice" transaction-manager="dynamicTransactionManager"> 19 <tx:attributes> 20 <tx:method name="*InTrx" propagation="REQUIRED" /> 21 <tx:method name="*InNewTrx" propagation="REQUIRES_NEW" /> 22 <tx:method name="*NoTrx" propagation="NOT_SUPPORTED" /> 23 <tx:method name="*" propagation="SUPPORTS" /> 24 </tx:attributes> 25 </tx:advice>
配置好以后就可以使用多數據源切換的功能了,通過DataSourceSelector中的setRouteKey()方法進行數據源切換,切換之后對數據庫的操作就是當前數據源的了。
這種方法相對於直接通過jdbc連接的方式確實方便了許多,直接使用了Spring框架提供的事務支持,對數據庫的操作也可以用Mybatis框架來做。But!! 這種方式也會存在一些讓人不是很爽的地方,細心的同學們可能已經發現了,那就是我們的多個數據源都是配置在Spring的xml配置文件里面的,這就導致了我們每次新增加一個數據源都得修改一次xml文件,並且進行一次版本發布,想想就很不爽啊~~~ 而且,隨着如果系統中連接的數據源越來越多,我們的配置文件也會越來越長,代碼也會很難看!那么能不能把這些變化的數據源信息做成配置的呢?雖然不是很容易,但是方法還是有的,這就是今天的主題:動態注入。
3、 Spring動態注入Bean
由於Spring傳統的注入Bean的方式是通過加載xml配置文件來依次注入配置文件中定義的Bean,如果數據源的Bean通過其他方式配置,就需要在代碼中進行動態注入。數據源的配置方式可以是任意方式,只要能夠在代碼中讀取到即可,本文通過從數據庫中讀取數據源配置內容來實現多數據源路由。
動態注入步驟:
- 從數據庫中讀取數據源配置列表,遍歷數據源配置列表,並且對每條配置單獨進行處理;
-
每條配置均需構造一個數據源的Bean並注入到Spring容器:
1 <!-- 配置數據源 --> 2 <!-- 其他多個數據源配置從配置表中讀取,並在應用啟動時進行加載(動態注入Spring容器) --> 3 <bean id="dynamicBaseDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> 4 <property name="url" value="jdbc:mysql://localhost:3306?useUnicode=true&characterEncoding=UTF-8"/> 5 <property name="username" value="root"/> 6 <property name="password" value="root"/> 7 </bean>
-
需要將新構造的數據源Bean加到動態數據源的targetDataSources這個Map結構的屬性中,並將動態數據源Bean重新注冊:
1 <!-- 其他多個數據源配置從配置表中讀取,並在應用啟動時進行加載(動態注入Spring容器) --> 2 <entry key="defaultDS" value-ref="dynamicBaseDataSource"/>
- 由於事務管理相關配置依賴了原有的動態數據源,而動態數據源已經更新,所以相應的事務管理配置也要更新;同樣的,事務相關的攔截器advisor、advice由於依賴事務管理器也都需要更新。
數據源動態注入代碼:
1 public class DynamicInjectDataSource { 2 3 @Autowired 4 private DatasourceConfigMapper datasourceConfigMapper; 5 6 private static final String URL_PREFIX = "jdbc:mysql://"; 7 private static final String URL_SURFIX = "?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull"; 8 private static final String DESTORY_METHOD = "close"; 9 private static final String DYNAMIC_DATASOURCE = "dynamicDataSource"; 10 11 public void startUp() throws Exception { 12 this.dynamicInject(); 13 } 14 15 private void dynamicInject() throws Exception { 16 ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) SpringContextHolder.getContext(); 17 DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getBeanFactory(); 18 ManagedMap<String, BeanDefinition> dataSourceMap = new ManagedMap<>(); 19 List<DatasourceConfig> dataSourceConfigList = datasourceConfigMapper.selectAllDataSource(); 20 if (CollectionUtils.isEmpty(dataSourceConfigList)) { 21 System.out.println("未查詢到相關數據源!"); 22 throw new Exception("初始化動態數據源失敗!"); 23 } 24 for (DatasourceConfig config : dataSourceConfigList) { 25 String beanId = config.getBeanId(); 26 System.out.println("開始注冊Mysql數據源:" + config.getDsKey()); 27 // 如果存在則需要重新注冊,防止有修改需要刷新 28 if (defaultListableBeanFactory.containsBean(beanId)) { 29 defaultListableBeanFactory.removeBeanDefinition(beanId); 30 } 31 // 注冊新的Bean 32 BeanDefinitionBuilder dataSourceBuilder = BeanDefinitionBuilder.genericBeanDefinition(BasicDataSource.class); 33 dataSourceBuilder.setDestroyMethodName(DESTORY_METHOD); 34 dataSourceBuilder.addPropertyValue("url", URL_PREFIX + config.getUrl() + URL_SURFIX); 35 dataSourceBuilder.addPropertyValue("username", config.getUserName()); 36 dataSourceBuilder.addPropertyValue("password", config.getPassword()); 37 dataSourceBuilder.addPropertyValue("maxActive", config.getMaxactive()); 38 defaultListableBeanFactory.registerBeanDefinition(beanId, dataSourceBuilder.getRawBeanDefinition()); 39 // 動態添加數據源 40 dataSourceMap.put(config.getDsKey(), dataSourceBuilder.getRawBeanDefinition()); 41 } 42 43 /* 重新注冊動態數據源**/ 44 Map<String, Object> dynamicDSPropertiesMap = new HashMap<>(); 45 dynamicDSPropertiesMap.put("targetDataSources", dataSourceMap); 46 BeanDefinition dynamicDataSourceBean = this.reRegisterBeanDefinition(DYNAMIC_DATASOURCE, dynamicDSPropertiesMap); 47 48 /* 重新注冊事務管理器**/ 49 Map<String, Object> dynamicDSManagerProsMap = new HashMap<>(); 50 dynamicDSManagerProsMap.put("dataSource", dynamicDataSourceBean); 51 BeanDefinition dynamicManageBean = this.reRegisterBeanDefinition("dynamicTransactionManager", dynamicDSManagerProsMap); 52 53 /* 重新注冊Advice**/ 54 Map<String, Object> dynamicAdviceProsMap = new HashMap<>(); 55 dynamicAdviceProsMap.put("transactionManager", dynamicManageBean); 56 this.reRegisterBeanDefinition("dynamicAdvice", dynamicAdviceProsMap); 57 58 /* 重新注冊Advisor**/ 59 Map<String, Object> dynamicAdvisorProsMap = new HashMap<>(); 60 dynamicAdvisorProsMap.put("adviceBeanName", "dynamicAdvice"); 61 this.reRegisterBeanDefinition("dynamicAdvisor", dynamicAdvisorProsMap); 62 63 } 64 65 /** 66 * 重新注冊Bean通用方法 67 * 68 * @param beanName bean名稱 69 * @param properties 屬性 70 */ 71 private BeanDefinition reRegisterBeanDefinition(String beanName, Map<String, Object> properties) { 72 ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) SpringContextHolder.getContext(); 73 DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getBeanFactory(); 74 BeanDefinition regBean = defaultListableBeanFactory.getBeanDefinition(beanName); 75 Set<String> propertyKeys = properties.keySet(); 76 // 重新設置Bean的屬性 77 for (String propertyKey : propertyKeys) { 78 regBean.getPropertyValues().removePropertyValue(propertyKey); 79 regBean.getPropertyValues().add(propertyKey, properties.get(propertyKey)); 80 } 81 // 刪除原有Bean 82 if (defaultListableBeanFactory.containsBean(beanName)) { 83 defaultListableBeanFactory.removeBeanDefinition(beanName); 84 } 85 // 重新注冊Bean 86 defaultListableBeanFactory.registerBeanDefinition(beanName, regBean); 87 return regBean; 88 } 89 }
其中存儲數據源配置的表結構如下:
4、基於配置的動態數據源路由測試
在數據庫中我配置了兩個數據源,一個是我本地創建的數據庫,另外一個是我VPS上部署的數據庫。
在應用啟動的時候會將這兩個數據源加載到Spring容器,並且可以通過ds_key來路由具體的數據源。測試程序分別打印出兩個數據源的數據庫里面的一張表的字段列表。
以下是具體測試代碼:
1 @Service("dynamicServiceImpl") 2 public class DynamicServiceImpl implements IDynamicService { 3 @Resource(name = "dynamicDataSourceSelector") 4 private DataSourceSelector dynamicDataSourceSelector; 5 @Autowired 6 private DynamicMapper dynamicMapper; 7 @Override 8 public void dynamicRouting(String routingKey, String tableName, String schema) { 9 // 路由數據源 10 System.out.println("路由到數據源:" + routingKey); 11 dynamicDataSourceSelector.setRouteKey(routingKey); 12 // 從當前數據源中進行查找 13 System.out.println("顯示數據源 " + routingKey + "的表: " + schema + "." + tableName + " 字段列表:"); 14 List<String> colnums = dynamicMapper.selectAllColumns(schema, tableName); 15 // 打印字段列表 16 StringBuilder sb = new StringBuilder(); 17 sb.append("["); 18 for (int i = 0; i < colnums.size(); i++) { 19 sb.append(colnums.get(i)).append(","); 20 if (i == colnums.size() - 1) { 21 sb.delete(sb.length() - 1, sb.length()); 22 sb.append("]"); 23 } 24 } 25 System.out.println(sb.toString()); 26 System.out.println(); 27 } 28 29 }
1 @Test 2 public void testDynamicSource() { 3 // 路由DSVps數據源 4 dynamicServiceImpl.dynamicRouting("DSVps", "article", "myblog"); 5 6 // 路由DSLocal數據源 7 dynamicServiceImpl.dynamicRouting("DSLocal", "khmessage", "weiyaqi"); 8 }
測試結果如下:
通過上面測試結果我們可以看到,在Spring的xml配置中不需要配置這些數據源,我們也做到了在這些數據源之間來回切換,而且數據源的個數我們也可以任意增加(只需要在數據庫表中添加一條配置的記錄即可),而我們的xml配置卻依舊保持不變並且很簡潔,配置一個默認的數據源,其他的都通過數據庫配置讀取並且動態注入:
1 <!-- 配置數據源 --> 2 <!-- 其他多個數據源配置從配置表中讀取,並在應用啟動時進行加載(動態注入Spring容器) --> 3 <bean id="dynamicBaseDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> 4 <property name="url" value="jdbc:mysql://localhost:3306?useUnicode=true&characterEncoding=UTF-8"/> 5 <property name="username" value="root"/> 6 <property name="password" value="root"/> 7 </bean> 8 9 <!-- 配置數據源路由,targetDataSources.key作為數據源唯一標識 --> 10 <bean id="dynamicDataSource" class="com.guigui.datasource.DynamicDataSource"> 11 <property name="targetDataSources"> 12 <map> 13 <!-- 其他多個數據源配置從配置表中讀取,並在應用啟動時進行加載(動態注入Spring容器) --> 14 <entry key="defaultDS" value-ref="dynamicBaseDataSource"/> 15 </map> 16 </property> 17 </bean>
新增了數據源后,由於配置和應用是分開的,也不需要重新發布應用了。如果想更進一步不重啟應用就能達到刷新數據源的目的,可以通過其他方式如定時任務或者頁面調用等方式觸發DynamicInjectDataSource. startUp()方法來完成數據源刷新。
以上便是本次要介紹的全部內容,如果有什么問題,歡迎各位讀者指正,感激不盡!
動態數據源路由demo源碼已上傳至GitHub: https://github.com/guishenyouhuo/dynamicdatasource