項目地址:https://github.com/jinyousen/blog/tree/master/problem/master-slave-switch
該工程使用spring boot 和 Mybatis 實現多數據源,動態數據源切換。以及在過程遇到Spring事務執行順序與數據源切換執行順序設置
數據源動態切換由conf/dal 包下4個類實現;
- DynamicDataSource.java
- DataSourceConfig.java
- TargetDataSource.java
- DataSourceAspect.java
DynamicDataSource.java
利用ThreadLocal存取數據源名稱
DynamicDataSource繼承 AbstractRoutingDataSource.java 重寫父類 determineCurrentLookupKey 獲取當前線程鏈接的數據源名
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 本地線程共享對象
* 動態數據源持有者,負責利用ThreadLocal存取數據源名稱
*/
private static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();
public static void putDataSource(String name) {
THREAD_LOCAL.set(name);
}
public static String getDataSource() {
return THREAD_LOCAL.get();
}
public static void removeDataSource() {
THREAD_LOCAL.remove();
}
@Override
protected Object determineCurrentLookupKey() {
return getDataSource();
}
}
DataSourceConfig.java
配置數據源,從配置文件中獲取數據源,放入DynamicDataSource Bean單例對象中
@Bean
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
DruidDataSource druidDataSource = new DruidDataSource();
return druidDataSource;
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
DruidDataSource druidDataSource = new DruidDataSource();
return druidDataSource;
}
/**
* @Primary 該註解表示在同一個接口有多個實現類可以注入的時候,默認選擇哪一個,而不是讓@autowire註解報錯
* @Qualifier 根據名稱進行注入,通常是在具有相同的多個類型的實例的一個注入(例如有多個DataSource類型的實例)
*/
@Bean
@Primary
public DynamicDataSource dataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("slaveDataSource") DataSource userDataSource
) {
//按照目標數據源名稱和目標數據源對象的映射存放在Map中
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DalConstant.DATA_SOURCE_MASTER, masterDataSource);
targetDataSources.put(DalConstant.DATA_SOURCE_SLAVE, userDataSource);
//採用是想AbstractRoutingDataSource的對象包裝多數據源
DynamicDataSource dataSource = new DynamicDataSource();
dataSource.setTargetDataSources(targetDataSources);
//設置默認的數據源,當拿不到數據源時,使用此配置
dataSource.setDefaultTargetDataSource(masterDataSource);
return dataSource;
}
TargetDataSource.java
標註方法調用的數據源名稱
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface TargetDataSource {
String value();
}
DataSourceAspect.java
自定義實現Aspect切面,獲取當前執行方法名 TargetDataSource 註解上調用的數據源名,修改ThreadLocal中數據源名
/*
* 定義一個切入點
*/
@Pointcut("execution(* org.yasser.service.*..*(..))")
public void dataSourcePointCut() {
}
/*
* 通過連接點切入
*/
@Before("dataSourcePointCut()")
public void doBefore(JoinPoint joinPoint) {
try {
String method = joinPoint.getSignature().getName();
Object target = joinPoint.getTarget();
Class<?>[] classz = target.getClass().getInterfaces();
Class<?>[] parameterTypes = ((MethodSignature) joinPoint.getSignature()).getMethod().getParameterTypes();
Method m = classz[0].getMethod(method, parameterTypes);
if (m != null && m.isAnnotationPresent(TargetDataSource.class)) {
TargetDataSource data = m.getAnnotation(TargetDataSource.class);
String dataSourceName = data.value();
DynamicDataSource.putDataSource(dataSourceName);
}
} catch (Throwable e) {
e.printStackTrace();
DynamicDataSource.putDataSource(DalConstant.DATA_SOURCE_MASTER);
log.error("DataSourceAspect is error!", e);
}
}
工程中對於數據庫的異常操作,我們將會創建事務進行回滾。在創建事務的過程中博主犯了一個錯誤:
spring中有BeanNameAutoProxyCreator和AnnotationAwareAspectJAutoProxyCreator兩種AOP代理方式
1、匹配Bean的名稱自動創建匹配到的Bean的代理,實現類BeanNameAutoProxyCreator
2、根據Bean中的AspectJ註解自動創建代理,實現類AnnotationAwareAspectJAutoProxyCreator
3、根據Advisor的匹配機制自動創建代理,會對容器中所有的Advisor進行掃描,自動將這些切面應用到匹配的Bean中,實現類DefaultAdvisorAutoProxyCreator
BeanNameAutoProxyCreator攔截優先級高於AnnotationAwareAspectJAutoProxyCreator
在我們數據源切換的DataSourceAspect中我們採用了 AspectJ註解開發,使用了AnnotationAwareAspectJAutoProxyCreator代理方式實現AOP
但是如果我們在創建事務時使用BeanNameAutoProxyCreator代理方式,則事務的代理優先級高於AnnotationAwareAspectJAutoProxyCreator。這也就導致我們事務切換無效,且Order註解設置無效。
對於數據源動態切換的事務代理選擇方式,應選擇AnnotationAwareAspectJAutoProxyCreator
@Bean
public AnnotationAwareAspectJAutoProxyCreator txProxy() {
/*
* 必須使用AspectJ方式的AutoProxy,這樣才能和DataSourceSwitchAspect保持統一的aop攔截方式,否則不同的攔截方式會導致order失效
*/
AnnotationAwareAspectJAutoProxyCreator creator = new AnnotationAwareAspectJAutoProxyCreator();
creator.setInterceptorNames("txAdvice");
creator.setIncludePatterns(Arrays.asList("execution (public org.yasser..*Service(..))"));
creator.setProxyTargetClass(true);
creator.setOrder(2);
return creator;
}
總結
- 數據源動態切換主要由重寫AbstractRoutingDataSource中determineTargetDataSource()方法,ThreadLocal 存儲數據源名
- 使用AspectJ方式,獲取方法上註解value得知當前方法所需數據源名,修改ThreadLocal中數據源名
- 啓用事務時一定要注意代理方式的選擇
- 開源插件MyBatis-Plus支持動態數據源切換 https://mp.baomidou.com/guide/dynamic-datasource.html