zoukankan      html  css  js  c++  java
  • Spring中Bean动态加载实现多数据源路由

    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&amp;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&amp;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&amp;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通过其他方式配置,就需要在代码中进行动态注入。数据源的配置方式可以是任意方式,只要能够在代码中读取到即可,本文通过从数据库中读取数据源配置内容来实现多数据源路由。

           动态注入步骤:

    1. 从数据库中读取数据源配置列表,遍历数据源配置列表,并且对每条配置单独进行处理;
    2. 每条配置均需构造一个数据源的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&amp;characterEncoding=UTF-8"/>
      5     <property name="username" value="root"/>
      6     <property name="password" value="root"/>
      7 </bean>
    3. 需要将新构造的数据源Bean加到动态数据源的targetDataSources这个Map结构的属性中,并将动态数据源Bean重新注册:

      1 <!-- 其他多个数据源配置从配置表中读取,并在应用启动时进行加载(动态注入Spring容器) -->
      2 <entry key="defaultDS" value-ref="dynamicBaseDataSource"/>
    4. 由于事务管理相关配置依赖了原有的动态数据源,而动态数据源已经更新,所以相应的事务管理配置也要更新;同样的,事务相关的拦截器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&amp;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

         个人博客链接:http://www.perona.buzz/article/8

  • 相关阅读:
    对于大流量的网站,您采用什么样的方法来解决访问量问题?
    div section article区分--20150227
    不懂的code整理学习
    常用又容易忘记的代码
    【转】机器学习中常用损失函数
    姿态估计的两个数据集COCO和MPII的认识
    用caffe训练openpose过程中,出现异常
    编译caffe的诸多注意事项
    Win7下Anaconda3+Tensorflow
    论文阅读(Zhe Cao——【CVPR2017】Realtime Multi-Person 2D Pose Estimation using Part Affinity Fields )
  • 原文地址:https://www.cnblogs.com/guishenyouhuo/p/9956099.html
Copyright © 2011-2022 走看看