你的位置:首页 > Java教程

[Java教程]spring 动态数据源


1、动态数据源:

   在一个项目中,有时候需要用到多个数据库,比如读写分离,数据库的分布式存储等等,这时我们要在项目中配置多个数据库。

2、原理:

    (1)、spring 单数据源获取数据连接过程:

    DataSource --> SessionFactory --> Session
    DataSouce   实现javax.sql.DateSource接口的数据源,
    DataSource  注入SessionFactory,
    从sessionFactory 获取 Session,实现数据库的 CRUD。

  (2)、动态数据源切换:  

    动态数据源原理之一:实现 javax.sql.DataSource接口, 封装DataSource, 在 DataSource 配置多个数据库连接,这种方式只需要一个dataSouce,就能实现多个数据源,最理想的实现,但是需要自己实现DataSource,自己实现连接池,对技术的要求较高,而且自己实现的连接池在性能和稳定性上都有待考验。

    动态数据源原理之二:配置多个DataSource, SessionFactory注入多个DataSource,实现SessionFactory动态调用DataSource,这种方式需要自己实现SessesionFactory,第三方实现一般不支持注入多个DataSource。

    动态数据源原理之三:配置多个DataSource, 在DataSource和SessionFactory之间插入 RoutingDataSource路由,即 DataSource --> RoutingDataSource --> SessionFactory --> Session, 在SessionFactory调用时在 RoutingDataSource 层实现DataSource的动态切换, spring提供了 AbstratRoutingDataSource抽象类, 对动态数据源切换提供了很好的支持, 不需要开发者实现复杂的底层逻辑, 推荐实现方式。

    动态数据源原理之四:配置多个SessionFactory,这种实现对技术要求最低,但是相对切换数据源最不灵活。  

3、实现:

  这里我们使用原理三以读写分离为例,具体实现如下:

  步骤一:配置多个DateSource,使用的基于阿里的 DruidDataSource

 1 <!-- 引入属性文件,方便配置内容修改 --> 2   <context:property-placeholder location="classpath:jdbc.properties" /> 3    4    5   <!-- 数据库链接(主库) --> 6   <bean id="dataSourceRW" class="com.alibaba.druid.pool.DruidDataSource" 7     destroy-method="close"> 8     <!-- 基本属性 url、user、password --> 9     <property name="url" value="${jdbc_url}" />10     <property name="username" value="${jdbc_username}" />11     <property name="password" value="${jdbc_password}" />12 13     <!-- 配置初始化大小、最小、最大 -->14     <property name="initialSize" value="${druid_initialSize}" />15     <property name="minIdle" value="${druid_minIdle}" />16     <property name="maxActive" value="${druid_maxActive}" />17 18     <!-- 配置获取连接等待超时的时间 -->19     <property name="maxWait" value="${druid_maxWait}" />20 21     <property name="validationQuery" value="SELECT 'x'" />22     <property name="testWhileIdle" value="true" />23 24     <!-- 打开PSCache,并且指定每个连接上PSCache的大小 -->25     <property name="poolPreparedStatements" value="true" />26     <property name="maxPoolPreparedStatementPerConnectionSize"27       value="100" />28 29     <!-- 密码加密 -->30     <property name="filters" value="config" />31     <property name="connectionProperties" value="config.decrypt=true" />32   </bean>33 34 35     <!-- 数据库链接(只读库) -->36   <bean id="dataSourceR" class="com.alibaba.druid.pool.DruidDataSource"37     destroy-method="close">38     <!-- 基本属性 url、user、password -->39     <property name="url" value="${jdbc_url_read}" />40     <property name="username" value="${jdbc_username_read}" />41     <property name="password" value="${jdbc_password_read}" />42 43     <!-- 配置初始化大小、最小、最大 -->44     <property name="initialSize" value="${druid_initialSize}" />45     <property name="minIdle" value="${druid_minIdle}" />46     <property name="maxActive" value="${druid_maxActive}" />47 48     <!-- 配置获取连接等待超时的时间 -->49     <property name="maxWait" value="${druid_maxWait}" />50 51     <property name="validationQuery" value="SELECT 'x'" />52     <property name="testWhileIdle" value="true" />53 54     <!-- 打开PSCache,并且指定每个连接上PSCache的大小 -->55     <property name="poolPreparedStatements" value="true" />56     <property name="maxPoolPreparedStatementPerConnectionSize"57       value="100" />58 59     <!-- 密码加密 -->60     <property name="filters" value="config" />61     <property name="connectionProperties" value="config.decrypt=true" />62   </bean>

View Code

  步骤二:配置 DynamicDataSource

 1 <!-- 动态数据源 -->  2  <bean id="dynamicDataSource" class="base.dataSource.DynamicDataSource">  3    <!-- 通过key-value关联数据源 -->  4    <property name="targetDataSources">  5      <map>  6        <entry value-ref="dataSourceRW" key="dataSourceRW"></entry>  7        <entry value-ref="dataSourceR" key="dataSourceR"></entry>  8      </map>  9    </property>10    <!-- 默认的DataSource配置-->11    <property name="defaultTargetDataSource" ref="dataSourceR" />   12  </bean> 

View Code
 1 package base.dataSource; 2  3 import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; 4  5 public class DynamicDataSource extends AbstractRoutingDataSource{ 6  7   @Override 8   protected Object determineCurrentLookupKey() { 9     return DBContextHolder.getDbType();10   }11 }

View Code

  DynamicDataSource 继承了spring 的 AbstractRoutingDataSource 抽象类 实现determineCurrentLookupKey()方法

  determineCurrentLookupKey()方法在 SessionFactory 获取 DataSoure时被调用,AbstractRoutingDataSource 代码:

 1 // 2 // Source code recreated from a .class file by IntelliJ IDEA 3 // (powered by Fernflower decompiler) 4 // 5  6 package org.springframework.jdbc.datasource.lookup; 7  8 import java.sql.Connection; 9 import java.sql.SQLException; 10 import java.util.HashMap; 11 import java.util.Iterator; 12 import java.util.Map; 13 import java.util.Map.Entry; 14 import javax.sql.DataSource; 15 import org.springframework.beans.factory.InitializingBean; 16 import org.springframework.jdbc.datasource.AbstractDataSource; 17 import org.springframework.jdbc.datasource.lookup.DataSourceLookup; 18 import org.springframework.jdbc.datasource.lookup.JndiDataSourceLookup; 19 import org.springframework.util.Assert; 20  21 public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean { 22   private Map<Object, Object> targetDataSources; 23   private Object defaultTargetDataSource; 24   private boolean lenientFallback = true; 25   private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup(); 26   private Map<Object, DataSource> resolvedDataSources; 27   private DataSource resolvedDefaultDataSource; 28  29   public AbstractRoutingDataSource() { 30   } 31  32   public void setTargetDataSources(Map<Object, Object> targetDataSources) { 33     this.targetDataSources = targetDataSources; 34   } 35  36   public void setDefaultTargetDataSource(Object defaultTargetDataSource) { 37     this.defaultTargetDataSource = defaultTargetDataSource; 38   } 39  40   public void setLenientFallback(boolean lenientFallback) { 41     this.lenientFallback = lenientFallback; 42   } 43  44   public void setDataSourceLookup(DataSourceLookup dataSourceLookup) { 45     this.dataSourceLookup = (DataSourceLookup)(dataSourceLookup != null?dataSourceLookup:new JndiDataSourceLookup()); 46   } 47  48   public void afterPropertiesSet() { 49     if(this.targetDataSources == null) { 50       throw new IllegalArgumentException("Property \'targetDataSources\' is required"); 51     } else { 52       this.resolvedDataSources = new HashMap(this.targetDataSources.size()); 53       Iterator var1 = this.targetDataSources.entrySet().iterator(); 54  55       while(var1.hasNext()) { 56         Entry entry = (Entry)var1.next(); 57         Object lookupKey = this.resolveSpecifiedLookupKey(entry.getKey()); 58         DataSource dataSource = this.resolveSpecifiedDataSource(entry.getValue()); 59         this.resolvedDataSources.put(lookupKey, dataSource); 60       } 61  62       if(this.defaultTargetDataSource != null) { 63         this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource); 64       } 65  66     } 67   } 68  69   protected Object resolveSpecifiedLookupKey(Object lookupKey) { 70     return lookupKey; 71   } 72  73   protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException { 74     if(dataSource instanceof DataSource) { 75       return (DataSource)dataSource; 76     } else if(dataSource instanceof String) { 77       return this.dataSourceLookup.getDataSource((String)dataSource); 78     } else { 79       throw new IllegalArgumentException("Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource); 80     } 81   } 82  83   public Connection getConnection() throws SQLException { 84     return this.determineTargetDataSource().getConnection(); 85   } 86  87   public Connection getConnection(String username, String password) throws SQLException { 88     return this.determineTargetDataSource().getConnection(username, password); 89   } 90  91   public <T> T unwrap(Class<T> iface) throws SQLException { 92     return iface.isInstance(this)?this:this.determineTargetDataSource().unwrap(iface); 93   } 94  95   public boolean isWrapperFor(Class<?> iface) throws SQLException { 96     return iface.isInstance(this) || this.determineTargetDataSource().isWrapperFor(iface); 97   } 98  99   protected DataSource determineTargetDataSource() {100     Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");101     Object lookupKey = this.determineCurrentLookupKey();102     DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);103     if(dataSource == null && (this.lenientFallback || lookupKey == null)) {104       dataSource = this.resolvedDefaultDataSource;105     }106 107     if(dataSource == null) {108       throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");109     } else {110       return dataSource;111     }112   }113 114   protected abstract Object determineCurrentLookupKey();115 }

View Code
AbstractRoutingDataSource 两个主要变量:
  targetDataSources 初始化了 DataSource 的map集合, defaultTargetDataSource 初始化默认的DataSource 并实现了 DataSource的 getConnection() 获取数据库连接的方法,该方法从determineTargetDataSource()获取  DataSource, determineTargetDataSource() 调用了我们 DynamicDataSource 中实现的 determineCurrentLookupKey() 方法获取DataSource(determineCurrentLookupKey()方法返回的只是我们初始化的DataSource Ma  p集合key值, 通过key获取DataSource的方法这里不做赘述,感兴趣自己研究下),determineTargetDataSource()的主要逻辑是获取我们切换的DataSource, 如果没有的话读取默认的DataSource。

在DynamicDataSource中我们定义了一个线程变量DBContextHolder来存放我们切换的DataSource, 防止其它线程覆盖我们的DataSource。
 1 package base.dataSource; 2  3 /** 4  *  5  * @author xiao 6  * @date 下午3:27:52 7 */ 8 public final class DBContextHolder { 9   10   /** 11    * 线程threadlocal 12   */ 13   private static ThreadLocal<String> contextHolder = new ThreadLocal<>(); 14  15   private static String DEFAUL_DB_TYPE_RW = "dataSourceKeyRW";   16   17   /**18    * 获取本线程的dbtype19    * @return20   */21   public static String getDbType() { 22     String db = contextHolder.get(); 23     if (db == null) { 24       db = DEFAUL_DB_TYPE_RW;// 默认是读写库 25     } 26     return db; 27   } 28  29   /** 30    * 31    * 设置本线程的dbtype 32    * 33    * @param str 34   */ 35   public static void setDbType(String str) { 36     contextHolder.set(str); 37   } 38  39   /** 40    * clearDBType 41    * 42    * @Title: clearDBType 43    * @Description: 清理连接类型 44   */ 45   public static void clearDBType() { 46     contextHolder.remove(); 47   } 48 }

View Code

  至此我们获取DataSource的逻辑已完成, 接下来我们要考虑 设置DataSource, 即为DBContextHolder, set值。我们在代码中调用DBContextHolder.set()来设置DataSource,理论上可以在代码的任何位置设置, 不过为了统一规范,我们通过aop来实现,此时我们面临的问题,在哪一层切入, 方案一: 在dao层切入,dao封装了数据库的CRUD,在这一层切入控制最灵活,但是我们一般在service业务层切入事务,如果在dao层切换数据源,会遇到事务无法同步的问题,虽然有分布式事务机制,但是目前成熟的框架很难用,如果使用过 就会知道分布式事务是一件非常恶心的事情,而且分布式事务本就不是一个好的选择。方案二: 在service业务层切入,可以避免事务问题,但也相对影响了数据源切换的灵活性,这里要根据实际情况灵活选择,我们采用的在service业务层切入,具体实现如下:

步骤三:实现aop

 1 package base.dataSource.aop; 2  3 import java.util.Map; 4  5 import org.aspectj.lang.JoinPoint; 6 import org.springframework.core.Ordered; 7  8 import base.dataSource.DBContextHolder; 9 10 /**11  * 动态数据源切换aop12  * @author xiao13  * @date 2015年7月23日下午4:17:1314 */15 public final class DynamicDataSourceAOP implements Ordered{16 17 18   /**19    * 方法, 数据源应映射规则map20   */21   Map<String, String> methods;22 23   /**24    * 默认数据源25   */26   String defaultDataSource;27 28 29   public String getDefaultDataSource() {30     return defaultDataSource;31   }32 33   public void setDefaultDataSource(String defaultDataSource) {34     if(null == defaultDataSource || "".equals(defaultDataSource)){35       throw new NullPointerException("defaultDataSource Must have a default value");36     }37     this.defaultDataSource = defaultDataSource;38   }39 40   public Map<String, String> getMethods() {41     return methods;42   }43 44   public void setMethods(Map<String, String> methods) {45     this.methods = methods;46   }47 48   /**49    * before 数据源切换50    *51    * @param pjp52    * @throws Throwable53   */54   public void dynamicDataSource(JoinPoint pjp) throws Throwable {55     DBContextHolder.setDbType(getDBTypeKey(pjp.getSignature().getName()));56   }57 58   private String getDBTypeKey(String methodName) {59     methodName = methodName.toUpperCase();60     for (String method : methods.keySet()) {61       String m = method.toUpperCase();62       /**63        * 忽略大小写64        * method 如果不包含 '*', 则以方法名匹配 method65        * method 包含 '*', 则匹配以 method 开头, 或者 等于method 的方法66       */67       if (!method.contains("*")68           && m.equals(methodName)69           || methodName70           .startsWith(m.substring(0, m.indexOf("*") - 1))71           || methodName.equals(m.substring(0, m.indexOf("*") - 1))) {72         return methods.get(method);73       }74     }75     return defaultDataSource;76   }77 78   //设置AOP执行顺序, 这里设置优于事务79   @Override80   public int getOrder() {81     return 1;82   }83 }

View Code

这里有一个小知识点,aop实现类实现了orderd接口,这个接口有一个方法getOrder(),返回aop的执行顺序,就是在同一个切点如果切入了多个aop,则按order从小到大执行,这里我们设置优于事务aop,因为事务是 基于dataSource的,即先切换数据源,在开启事务,否则可能会存在切换了已开启了事务的数据源,导致事务不生效。

步骤四:配置aop切面

 1 <!-- 数据源读写分离 aop --> 2   <bean id="dynamicDataSourceAOP" class="base.dataSource.aop.DynamicDataSourceAOP"> 3     <property name="methods">  4       <map>          5         <entry key="select*" value="dataSourceKeyR" /> 6         <entry key="get*" value="dataSourceKeyR" /> 7         <entry key="find*" value="dataSourceKeyR" /> 8         <entry key="page*" value="dataSourceKeyR" />       9         <entry key="query*" value="dataSourceKeyRW" />10       </map>11      </property>12     <property name="defaultDataSource" value="dataSourceKeyRW"/>13    </bean>14    15    16   <aop:config>17     <!-- 切点 管理所有Service的方法 -->18     <aop:pointcut19       expression="execution(* com.b2c.*.service.*Service.*(..))"20       id="transactionPointCut" />        21     <!-- 进行事务控制 Advisor -->22     <aop:advisor advice-ref="txAdvice" pointcut-ref="transactionPointCut" />23     24     <!-- 动态数据源aop, aop:advisor配置一定要在 aop:aspect之前,否则报错  -->25     <aop:aspect ref="dynamicDataSourceAOP">      26       <aop:before method="dynamicDataSource" pointcut-ref="transactionPointCut" />    27     </aop:aspect>28     29   </aop:config>

View Code

至此全部完成, 另外这只是个人观点,有更好的想法欢迎交流指正。