需求:系統中要實現切換數據庫(業務數據庫和his數據庫)java
網上不少資料上有提到AbstractRoutingDataSource,大體是這麼說的mysql
在Spring 2.0.1中引入了AbstractRoutingDataSource, 該類充當了DataSource的路由中介, 能有在運行時, 根據某種key值來動態切換到真正的DataSource上。react
Spring動態配置多數據源,即在大型應用中對數據進行切分,而且採用多個數據庫實例進行管理,這樣能夠有效提升系統的水平伸縮性。而這樣的方案就會不一樣於常見的單一數據實例的方案,這就要程序在運行時根據當時的請求及系統狀態來動態的決定將數據存儲在哪一個數據庫實例中,以及從哪一個數據庫提取數據。git
Spring對於多數據源,以數據庫表爲參照,大致上能夠分紅兩大類狀況:
一是,表級上的跨數據庫。即,對於不一樣的數據庫卻有相同的表(表名和表結構徹底相同)。
二是,非表級上的跨數據庫。即,多個數據源不存在相同的表。
Spring2.x的版本中採用Proxy模式,就是咱們在方案中實現一個虛擬的數據源,而且用它來封裝數據源選擇邏輯,這樣就能夠有效地將數據源選擇邏輯從Client中分離出來。Client提供選擇所需的上下文(由於這是Client所知道的),由虛擬的DataSource根據Client提供的上下文來實現數據源的選擇。
具體的實現就是,虛擬的DataSource僅需繼承AbstractRoutingDataSource實現determineCurrentLookupKey()在其中封裝數據源的選擇邏輯github
1、原理spring
首先看下AbstractRoutingDataSource類結構,繼承了AbstractDataSource:sql
public abstract class AbstractRoutingDataSource extends org.springframework.jdbc.datasource.AbstractDataSource implements org.springframework.beans.factory.InitializingBean
既然是AbstractDataSource,固然就是javax.sql.DataSource的子類,因而咱們天然地回去看它的getConnection方法:數據庫
public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
}
public Connection getConnection(String username, String password) throws SQLException {
return determineTargetDataSource().getConnection(username, password);
}
原來關鍵就在determineTargetDataSource()裏:springboot
protected DataSource determineTargetDataSource() { Assert.notNull(this.resolvedDataSources, "DataSource router not initialized"); Object lookupKey = determineCurrentLookupKey();//業務代碼能更改這個值,就可以使用指定的DB DataSource dataSource = this.resolvedDataSources.get(lookupKey);//此處來獲取指定的DB if (dataSource == null && (this.lenientFallback || lookupKey == null)) { dataSource = this.resolvedDefaultDataSource; } if (dataSource == null) { throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]"); } return dataSource; }
這裏用到了咱們須要進行實現的抽象方法determineCurrentLookupKey(),該方法返回須要使用的DataSource的key值,而後根據這個key從resolvedDataSources這個map裏取出對應的DataSource,若是找不到,則用默認的resolvedDefaultDataSource。ruby
回過頭看AbstractDataSource的afterPropertiesSet方法:
1 public void afterPropertiesSet() { 2 if (this.targetDataSources == null) { 3 throw new IllegalArgumentException("Property 'targetDataSources' is required"); 4 } 5 this.resolvedDataSources = new HashMap<Object, DataSource>(this.targetDataSources.size()); 6 for (Map.Entry entry : this.targetDataSources.entrySet()) { 7 Object lookupKey = resolveSpecifiedLookupKey(entry.getKey()); 8 DataSource dataSource = resolveSpecifiedDataSource(entry.getValue()); 9 this.resolvedDataSources.put(lookupKey, dataSource);//這個值就是全部DataSource的集合 10 } 11 if (this.defaultTargetDataSource != null) { 12 this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource); 13 } 14 }
擴展:afterPropertiesSet方法調用時機:javaconfig配置場景,打斷點時顯示,在@Bean所在方法執行完成後,會調用此方法
package org.springframework.beans.factory; /** * Interface to be implemented by beans that need to react once all their * properties have been set by a BeanFactory: for example, to perform custom * initialization, or merely to check that all mandatory properties have been set. * * <p>An alternative to implementing InitializingBean is specifying a custom * init-method, for example in an XML bean definition. * For a list of all bean lifecycle methods, see the BeanFactory javadocs. * * @author Rod Johnson * @see BeanNameAware * @see BeanFactoryAware * @see BeanFactory * @see org.springframework.beans.factory.support.RootBeanDefinition#getInitMethodName * @see org.springframework.context.ApplicationContextAware */ public interface InitializingBean { /** * Invoked by a BeanFactory after it has set all bean properties supplied * (and satisfied BeanFactoryAware and ApplicationContextAware). * <p>This method allows the bean instance to perform initialization only * possible when all bean properties have been set and to throw an * exception in the event of misconfiguration. * @throws Exception in the event of misconfiguration (such * as failure to set an essential property) or if initialization fails. */ void afterPropertiesSet() throws Exception; }
配置數據源實例:
<bean id="onlineDynamicDataSource" class="com.xx.stat.base.dynamic.DynamicDataSource"> <property name="targetDataSources"> <map key-type="java.lang.String"> <entry key="xx" value-ref="dataSourceXX"/> <entry key="yy" value-ref="dataSourceYY"/> </map> </property> <property name="defaultTargetDataSource" ref="dataSource"/> </bean>
觀察上面的配置文件,發現咱們配置的是targetDataSources和defaultTargetDataSource
2、Spring配置多數據源的方式和具體使用過程
一、數據源的名稱常量類
public enum DatabaseTypeEnum { DB_DLHMC("dlhmc", "dlhmc數據庫,默認的數據庫"),DB_HIS("his", "HIS數據庫"); private String value; private String desc; private DatabaseTypeEnum(String value, String description) { this.value = value; this.desc = description; } public String getValue() { return value; } public String getDesc() { return desc; } @Override public String toString() { return "{" + value + ":" + desc + "}"; } public static DatabaseTypeEnum from(String value) { for (DatabaseTypeEnum item : values()) { if (item.getValue() == value) { return item; } } throw new IllegalArgumentException(String.format( "非法的輸入參數 '%s' ! 必須是%s中的其中一個。", value, Arrays.asList(values()) .toString())); } }
二、創建一個得到和設置上下文環境的類,主要負責改變上下文數據源的名稱
1 public class DatabaseContextHolder { 2 private static ThreadLocal<String> contextHolder=new ThreadLocal<String>(); 3 public static void setDbType(String dbType){ 4 contextHolder.set(dbType); 5 } 6 public static String getDbType(){ 7 return contextHolder.get(); 8 } 9 10 public static void clearDbType(){ 11 contextHolder.remove(); 12 } 13 14 }
三、創建動態數據源類,注意,這個類必須繼承AbstractRoutingDataSource,且實現方法 determineCurrentLookupKey,該方法返回一個Object,通常是返回字符串
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; public class DynamicDataSource extends AbstractRoutingDataSource{ @Override protected Object determineCurrentLookupKey() { return DatabaseContextHolder.getDbType();//能夠決定使用那個db } }
四、編寫spring的配置文件配置多個數據源
Java Config
一個示例
@Bean public AbstractRoutingDataSource routingDataSource(@Qualifier("dataSourceDLHMC") DataSource dataSourceDLHMC, @Qualifier("dataSourceHIS") DataSource dataSourceHIS) { Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put(DatabaseTypeEnum.DB_DLHMC, dataSourceDLHMC); targetDataSources.put(DatabaseTypeEnum.DB_HIS, dataSourceHIS); AbstractRoutingDataSource routingDataSource = new DynamicDataSource(); routingDataSource.setTargetDataSources(targetDataSources); routingDataSource.setDefaultTargetDataSource(dataSourceDLHMC); return routingDataSource; }
或
xml
<!-- 數據源配置 --> <bean id="defaultDS" class="com.alibaba.druid.pool.DruidDataSource" p:driverClassName="${jdbc.driver}" p:url="${jdbc.jdbcUrl}" p:username="${jdbc.username}" p:password="${jdbc.password}" p:initialSize="${jdbc.initialSize}" p:maxActive="${jdbc.maxActive}" p:testOnBorrow="${jdbc.testOnBorrow:false}" destroy-method="close"> </bean> <bean id="hisDS" class="com.alibaba.druid.pool.DruidDataSource" p:driverClassName="${his.jdbc.driver}" p:url="${his.jdbc.jdbcUrl}" p:username="${his.jdbc.username}" p:password="${his.jdbc.password}" p:initialSize="${his.jdbc.initialSize}" p:maxActive="${his.jdbc.maxActive}" p:testOnBorrow="${his.jdbc.testOnBorrow:false}" destroy-method="close"> </bean> <bean id="dataSource" class="com.supconit.util.datasource.DynamicDataSource"> <property name="targetDataSources"> <map key-type="java.lang.String"> <entry key="dlhmc" value-ref="defaultDS" /> <entry key="his" value-ref="hisDS" /> <!-- entry key="2" value-ref="ds2" / --> </map> </property> <property name="defaultTargetDataSource" ref="defaultDS" /> </bean>
五、使用
@Override public List<VBedPatientNew> selectNursinglevel() { DatabaseContextHolder.setDbType(DatabaseTypeEnum.DB_HIS.getValue());//指定一個數據源 List<VBedPatientNew> result=selectList("selectNursinglevel");//業務代碼 DatabaseContextHolder.clearDbType();//清除當前設置。防內存洗髮露 return result; }
也可使用AOP來實現:
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface HISDB { }
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; @Aspect @Component public class DriverDBAspect { public static final Logger LOGGER = LoggerFactory.getLogger(DriverDBAspect.class); @Around("@annotation(HISDB)") public Object proceed(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { Object result = null; try { LOGGER.info("=============begin set db to driver============="); DBContextHolder.setDatabase(DatabaseTypeEnum.DB_HIS); result = proceedingJoinPoint.proceed(); LOGGER.info("=============end set db to driver============="); } finally { DBContextHolder.clear(); } return result; } }
@Override @HISDB public List<VBedPatientNew> selectNursinglevel() { List<VBedPatientNew> result=selectList("selectNursinglevel"); return result; }
aop使用場景的另外一種用法:
import com.example.springboot.multidatasource.annotation.DataSourceTypeAnno; import com.example.springboot.multidatasource.common.DataSourceContextHolder; import com.example.springboot.multidatasource.common.DataSourceEnum; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component; import java.lang.reflect.Method; @Component @Aspect public class DataSourceAspect { @Pointcut("execution(* com.example.springboot.multidatasource.dao..*(..)) " + "&& @annotation(com.example.springboot.multidatasource.annotation.DataSourceTypeAnno)") public void dataSourcePointcut() { } @Around("dataSourcePointcut()") public Object doAround(ProceedingJoinPoint pjp) { MethodSignature methodSignature = (MethodSignature) pjp.getSignature(); Method method = methodSignature.getMethod(); DataSourceTypeAnno typeAnno = method.getAnnotation(DataSourceTypeAnno.class); DataSourceEnum sourceEnum = typeAnno.value(); if (sourceEnum == DataSourceEnum.master) { DataSourceContextHolder.setDataSourceType(DataSourceEnum.master); } else if (sourceEnum == DataSourceEnum.slaver) { DataSourceContextHolder.setDataSourceType(DataSourceEnum.slaver); } Object result = null; try { result = pjp.proceed(); } catch (Throwable throwable) { throwable.printStackTrace(); } finally { DataSourceContextHolder.resetDataSourceType(); } return result; } }
springboot多數據源讀寫分離和主庫數據源service層事務控制
讀寫分離若是撇開框架無非就是實現多個數據源,主庫用寫的數據源,從庫用讀的數據源。
由於想研究數據庫讀寫分離和分庫分表的設計,因此就本身搭建了一套springboot+druid+mybatis+aop 實現一主多從的設計。
第一步:首先須要自定義數據源的配置項,springboot默認解析的是帶前綴spring.datasource.下面的配置項,爲了避免衝突,就直接定義datasource.當成咱們的前綴,
@ConfigurationProperties(prefix = 「datasource.write」)能夠用來加載指定前綴的配置項,很是方便
由於使用druid,因此須要生成datasource的時候須要指定類型。
DataSourceBuilder.create().type(dataSourceType).build()
readSize是用來定義從庫的大小,有多少從庫就要配置多少個從庫datasource
第二步:從庫的負載均衡,主要是MyAbstractRoutingDataSource這個類
第三步,從寫springboot-mybatis架包的MybatisAutoConfiguration類的建立SqlSessionFactory方法,將裏面的數據源換成咱們自定義的AbstractRoutingDataSource
第四步驟。自定義事務MyDataSourceTransactionManagerAutoConfiguration
完整代碼和單元測試:
github:https://github.com/ggj2010/javabase.git
主要的架包
<!-- jdbc driver begin--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!--mybatis springboot--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> </dependency> <!-- jdbc driver end--> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> </dependency>
自定義數據源配置項:
#多數據源 1主2從 datasource: #從庫數量 readSize: 2 # 使用druid數據源 type: com.alibaba.druid.pool.DruidDataSource #主庫 write: url: jdbc:mysql://localhost:3306/master?useUnicode=true&characterEncoding=utf-8 username: root password: root driver-class-name: com.mysql.jdbc.Driver filters: stat maxActive: 20 initialSize: 1 maxWait: 60000 minIdle: 1 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQueryTimeout: 900000 validationQuery: SELECT SYSDATE() from dual testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true maxOpenPreparedStatements: 20 read1: url: jdbc:mysql://localhost:3306/slave1?useUnicode=true&characterEncoding=utf-8 username: root password: root driver-class-name: com.mysql.jdbc.Driver filters: stat maxActive: 20 initialSize: 1 maxWait: 60000 minIdle: 1 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQueryTimeout: 900000 validationQuery: SELECT SYSDATE() from dual testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true maxOpenPreparedStatements: 20 read2: url: jdbc:mysql://localhost:3306/slave2?useUnicode=true&characterEncoding=utf-8 username: root password: root driver-class-name: com.mysql.jdbc.Driver filters: stat maxActive: 20 initialSize: 1 maxWait: 60000 minIdle: 1 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQueryTimeout: 900000 validationQuery: SELECT SYSDATE() from dual testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true maxOpenPreparedStatements: 20
解析配置項:
@Configuration @Slf4j public class DataSourceConfiguration { @Value("${datasource.type}") private Class<? extends DataSource> dataSourceType; @Bean(name = "writeDataSource") @Primary @ConfigurationProperties(prefix = "datasource.write") public DataSource writeDataSource() { log.info("-------------------- writeDataSource init ---------------------"); return DataSourceBuilder.create().type(dataSourceType).build(); } /** * 有多少個從庫就要配置多少個 * @return */ @Bean(name = "readDataSource1") @ConfigurationProperties(prefix = "datasource.read1") public DataSource readDataSourceOne() { log.info("-------------------- readDataSourceOne init ---------------------"); return DataSourceBuilder.create().type(dataSourceType).build(); } @Bean(name = "readDataSource2") @ConfigurationProperties(prefix = "datasource.read2") public DataSource readDataSourceTwo() { log.info("-------------------- readDataSourceTwo init ---------------------"); return DataSourceBuilder.create().type(dataSourceType).build(); } }
重寫SqlSessionFactory
@Configuration @AutoConfigureAfter({ DataSourceConfiguration.class }) @Slf4j public class MybatisConfiguration extends MybatisAutoConfiguration { @Value("${datasource.readSize}") private String dataSourceSize; @Bean public SqlSessionFactory sqlSessionFactorys() throws Exception { log.info("-------------------- 重載父類 sqlSessionFactory init ---------------------"); return super.sqlSessionFactory(roundRobinDataSouceProxy()); } /** * 有多少個數據源就要配置多少個bean * @return */ @Bean public AbstractRoutingDataSource roundRobinDataSouceProxy() { int size = Integer.parseInt(dataSourceSize); MyAbstractRoutingDataSource proxy = new MyAbstractRoutingDataSource(size); Map<Object, Object> targetDataSources = new HashMap<Object, Object>(); DataSource writeDataSource = SpringContextHolder.getBean("writeDataSource"); // 寫 targetDataSources.put(DataSourceType.write.getType(), SpringContextHolder.getBean("writeDataSource")); for (int i = 0; i < size; i++) { targetDataSources.put(i, SpringContextHolder.getBean("readDataSource" + (i + 1))); } proxy.setDefaultTargetDataSource(writeDataSource); proxy.setTargetDataSources(targetDataSources); return proxy; } }
本地線程全局變量
public class DataSourceContextHolder { private static final ThreadLocal<String> local = new ThreadLocal<String>(); public static ThreadLocal<String> getLocal() { return local; } /** * 讀多是多個庫 */ public static void read() { local.set(DataSourceType.read.getType()); } /** * 寫只有一個庫 */ public static void write() { local.set(DataSourceType.write.getType()); } public static String getJdbcType() { return local.get(); } }
多數據源切換
public class MyAbstractRoutingDataSource extends AbstractRoutingDataSource { private final int dataSourceNumber; private AtomicInteger count = new AtomicInteger(0); public MyAbstractRoutingDataSource(int dataSourceNumber) { this.dataSourceNumber = dataSourceNumber; } @Override protected Object determineCurrentLookupKey() { String typeKey = DataSourceContextHolder.getJdbcType(); if (typeKey.equals(DataSourceType.write.getType())) return DataSourceType.write.getType(); // 讀 簡單負載均衡 int number = count.getAndAdd(1); int lookupKey = number % dataSourceNumber; return new Integer(lookupKey); } }
enum類型
public enum DataSourceType { read("read", "從庫"), write("write", "主庫"); @Getter private String type; @Getter private String name; DataSourceType(String type, String name) { this.type = type; this.name = name; } }
aop攔截設置本地線程變量
@Aspect @Component @Slf4j public class DataSourceAop { @Before("execution(* com.ggj.encrypt.modules.*.dao..*.find*(..)) or execution(* com.ggj.encrypt.modules.*.dao..*.get*(..))") public void setReadDataSourceType() { DataSourceContextHolder.read(); log.info("dataSource切換到:Read"); } @Before("execution(* com.ggj.encrypt.modules.*.dao..*.insert*(..)) or execution(* com.ggj.encrypt.modules.*.dao..*.update*(..))") public void setWriteDataSourceType() { DataSourceContextHolder.write(); log.info("dataSource切換到:write"); } }
自定義事務
@Configuration @EnableTransactionManagement @Slf4j public class MyDataSourceTransactionManagerAutoConfiguration extends DataSourceTransactionManagerAutoConfiguration { /** * 自定義事務 * MyBatis自動參與到spring事務管理中,無需額外配置, *只要org.mybatis.spring.SqlSessionFactoryBean引用的數據源與DataSourceTransactionManager引用的數據源一致便可, *不然事務管理會不起做用。 * @return */ @Bean(name = "transactionManager") public DataSourceTransactionManager transactionManagers() { log.info("-------------------- transactionManager init ---------------------"); return new DataSourceTransactionManager(SpringContextHolder.getBean("roundRobinDataSouceProxy")); } }
http://blog.csdn.net/ggjlvzjy/article/details/51544016
import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; import com.dao.annotation.TargetDataSource; import com.dao.route.DataSourceSwitch; import lombok.extern.slf4j.Slf4j; /** * 切換數據源AOP * */ @Aspect @Component @Slf4j public class TargetDataSourceAspect { @Before(" @annotation(ds)") public void changeDataSource(JoinPoint point, TargetDataSource ds){ String dsId = ds.value(); if (!DataSourceSwitch.containsDataSource(dsId)) { log.error("數據源[{}]不存在,使用default:{}", ds.value(), point.getSignature()); } else { log.debug("Use DataSource : {} > {}", ds.value(), point.getSignature()); DataSourceSwitch.setDataSource(ds.value()); } } @After(" @annotation(ds)") public void restoreDataSource(JoinPoint point, TargetDataSource ds) { log.debug("Revert DataSource : {} > {}", ds.value(), point.getSignature()); DataSourceSwitch.clearDataSource(); } }