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()方法进行数据源切换,切换之后对数据库的操作就是当前数据源的了。
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