原文鏈接: Spring Boot2.x 動態數據源配置
基於 Spring Boot 2.x、Spring Data JPA、druid、mysql 的動態數據源配置Demo,適合用於數據庫的讀寫分離等應用場景。通過在Service層方法上添加自定義注解實現讀寫不同的數據庫。
配置文件已配置好druid監控相關屬性,監控頁面鏈接:ip:8080/druid。賬號:admin,密碼:123456。詳情查看 application.yml 文件。
注意事項(前言)
在網上有很多關於動態切換數據源的配置教程,其中百分之九十的都是基於 Mybatis 的。當然也有零星的幾篇基於 Spring Data JPA 的配置教程,不過當你按着這些教程使用后就會發現靠譜一點的還可以做到不同的請求可以使用不同的數據源,但是無法做到在同一個請求內進行多個數據源之間的切換。在業務邏輯相對復雜的情況下肯定是不能滿足需求的。
那么是什么原因導致在同一請求內切換數據源失敗呢?經過單步調試和查看日志發現自己寫的注解確實生效了,只不過在第二次切換數據源時沒有執行 AbstractRoutingDataSource
的 determineCurrentLookupKey()
的方法而是直接拿到了數據庫連接去執行了SQL語句。那么這個方法是做什么的呢?
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = this.determineCurrentLookupKey();
DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
} else {
return dataSource;
}
}
從方法命名也能看出來這是個來決定使用哪個數據源的方法。上述源碼第三行通過調用 this.determineCurrentLookupKey();
方法獲取應該使用的數據源所對應的 key 值。也就是我們在 DataSourceContextHolder
放到 contextHolder
中的值。因為我們使用 DynamicDataSource
繼承 AbstractRoutingDataSource
並重寫了 determineCurrentLookupKey()
方法。在重寫的方法中我們獲取到了之前存入的數據源所對應的key,所以如果每次切換數據源時執行此方法后才算切換成功。
那么為什么使用 Spring Data JPA 切換一次數據源后第二次就切不過去了呢?經過查閱各種資料發現,在一個事務中如果不配置事務的傳播級別是不會開啟一個新事務的,因為 Spring 默認的事務級別是 PROPAGATION_REQUIRED
。也就是說如果不開啟一個新的事務就不會進行數據源的切換。因為Spring Data JPA 整合了 hibernate ,且 hibernate 的 session 是與 transaction 綁定的,所以多次切換數據源時獲取到的 session 的 hashCode 是同一個也就是第一次切換的數據源。這也就是為什么在同一個 Service 中無法做到可以切換多個數據源。(注:此 session 非常說的 web 中的那個 session)
那怎么解決這個問題呢?既然session和當前的事務時綁定的,那是不是在切片中把要切換的 key 值存儲到 contextHolder
中后,手動斷掉原來的session連接就可以了?在切片操作中加入下面兩行代碼:
SessionImplementor session = entityManager.unwrap(SessionImplementor.class);
//最關鍵的一句代碼, 手動斷開連接,不用重新設置 ,會自動重新設置連接。
session.disconnect();
經過測試這樣設置后則可以在同一個 Service 中切換操作不同的數據源讀寫數據。問題解決方案代碼見 https://www.changxuan.top/?p=772
注意:如果在一次請求中通過數據源A執行的一條SQL語句,然后又切換到數據源B執行同樣的SQL語句。此時框架為了性能會直接返回從數據源A的數據庫中查詢到的數據。所以這種情況是會切換失敗。
配置 pom.xml 文件
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.21</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
配置application.yml文件
spring:
datasource:
druid:
primary:
driverClassName: com.mysql.cj.jdbc.Driver
username: root
password: root
url: jdbc:mysql://localhost:3306/primary?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
filters: stat,wall
local:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
url: jdbc:mysql://localhost:3306/local?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
filters: stat,wall
stat-view-servlet:
enabled: true
login-username: admin
login-password: 123456
reset-enable: false
url-pattern: /druid/*
web-stat-filter:
enabled: true
# 添加過濾規則
url-pattern: /*
# 忽略過濾格式
exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"
jpa:
database: MYSQL
hibernate:
show_sql: true
format_sql: true
primary-dialect: org.hibernate.dialect.MySQL5InnoDBDialect
secondary-dialect: org.hibernate.dialect.MySQL5InnoDBDialect
# 打開后會自動在主庫生成表
# ddl-auto: update
database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
# 打開后會自動在主庫生成表
# generate-ddl: true
項目目錄結構

DataSource.java
package dynamic.data.annotation;
import dynamic.data.common.ContextConst;
import java.lang.annotation.*;
/**
* @Author: ChangXuan
* @Decription:
* @Date: 22:25 2020/2/23
**/
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
ContextConst.DataSourceType value() default ContextConst.DataSourceType.PRIMARY;
}
DynamicDataSourceAspect.java
package dynamic.data.aspect;
import dynamic.data.common.ContextConst;
import dynamic.data.datasource.DataSourceContextHolder;
import dynamic.data.annotation.DataSource;
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.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* @Author: ChangXuan
* @Decription:
* @Date: 22:28 2020/2/23
**/
@Component
@Aspect
public class DynamicDataSourceAspect {
@Before("execution(* dynamic.data.service..*.*(..))")
public void before(JoinPoint point){
try {
DataSource annotationOfClass = point.getTarget().getClass().getAnnotation(DataSource.class);
String methodName = point.getSignature().getName();
Class[] parameterTypes = ((MethodSignature) point.getSignature()).getParameterTypes();
Method method = point.getTarget().getClass().getMethod(methodName, parameterTypes);
DataSource methodAnnotation = method.getAnnotation(DataSource.class);
methodAnnotation = methodAnnotation == null ? annotationOfClass:methodAnnotation;
ContextConst.DataSourceType dataSourceType = methodAnnotation != null && methodAnnotation.value() !=null ? methodAnnotation.value() :ContextConst.DataSourceType.PRIMARY ;
DataSourceContextHolder.setDataSource(dataSourceType.name());
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
@After("execution(* dynamic.data.service..*.*(..))")
public void after(JoinPoint point){
DataSourceContextHolder.clearDataSource();
}
}
ContextConst.java
package dynamic.data.common;
/**
* @Author: ChangXuan
* @Decription:
* @Date: 22:17 2020/2/23
**/
public interface ContextConst {
enum DataSourceType{
PRIMARY,LOCAL
}
}
DataSourceContextHolder .java
package dynamic.data.datasource;
/**
* @Author: ChangXuan
* @Decription:
* @Date: 22:23 2020/2/23
**/
public class DataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
public static void setDataSource(String dbType){
System.out.println("切換到["+dbType+"]數據源");
contextHolder.set(dbType);
}
public static String getDataSource(){
return contextHolder.get();
}
public static void clearDataSource(){
contextHolder.remove();
}
}
DynamicDataSource.java
package dynamic.data.datasource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* @Author: ChangXuan
* @Decription:
* @Date: 22:22 2020/2/23
**/
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
}
MutiplyDataSource.java
package dynamic.data.datasource; import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder; import dynamic.data.common.ContextConst; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.PlatformTransactionManager; import javax.sql.DataSource; import java.util.HashMap; /** * @Author: ChangXuan * @Decription: * @Date: 22:15 2020/2/23 **/ @Configuration public class MutiplyDataSource { @Bean(name = "dataSourcePrimary") @ConfigurationProperties(prefix = "spring.datasource.druid.primary") public DataSource primaryDataSource(){ return DruidDataSourceBuilder.create().build(); } @Bean(name = "dataSourceLocal") @ConfigurationProperties(prefix = "spring.datasource.druid.local") public DataSource localDataSource(){ return DruidDataSourceBuilder.create().build(); } @Primary @Bean(name = "dynamicDataSource") public DataSource dynamicDataSource() { DynamicDataSource dynamicDataSource = new DynamicDataSource(); //配置默認數據源 dynamicDataSource.setDefaultTargetDataSource(primaryDataSource()); //配置多數據源 HashMap<Object, Object> dataSourceMap = new HashMap(); dataSourceMap.put(ContextConst.DataSourceType.PRIMARY.name(),primaryDataSource()); dataSourceMap.put(ContextConst.DataSourceType.LOCAL.name(),localDataSource()); dynamicDataSource.setTargetDataSources(dataSourceMap); return dynamicDataSource; } /** * 配置@Transactional注解事務 * @return */ @Bean public PlatformTransactionManager transactionManager() { return new DataSourceTransactionManager(dynamicDataSource()); } }
使用
在 DynamicDataSourceAspect.java 中配置的service下使用注解的方式指定執行的方法使用哪個數據庫。示例參考下方代碼:


