爱程序网

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      * 获取本线程的dbtype
19      * @return
20      */
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  * 动态数据源切换aop
12  * @author xiao
13  * @date 2015年7月23日下午4:17:13
14  */
15 public final class DynamicDataSourceAOP implements Ordered{
16 
17 
18     /**
19      * 方法, 数据源应映射规则map
20      */
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 pjp
52      * @throws Throwable
53      */
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 如果不包含 '*', 则以方法名匹配 method
65              * method 包含 '*', 则匹配以 method 开头, 或者 等于method 的方法
66              */
67             if (!method.contains("*")
68                     && m.equals(methodName)
69                     || methodName
70                     .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     @Override
80     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:pointcut
19             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

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

 




  

关于爱程序网 - 联系我们 - 广告服务 - 友情链接 - 网站地图 - 版权声明 - 人才招聘 - 帮助