1、动态数据源:
在一个项目中,有时候需要用到多个数据库,比如读写分离,数据库的分布式存储等等,这时我们要在项目中配置多个数据库。
2、原理:
单数据源获取数据连接过程:
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
至此全部完成, 另外这只是个人观点,有更好的想法欢迎交流指正。