Spring Boot MyBatis 動態數據源切換 Spring事務與數據源切換執行順序設置

項目地址: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;
    }

總結

  1. 數據源動態切換主要由重寫AbstractRoutingDataSource中determineTargetDataSource()方法,ThreadLocal 存儲數據源名
  2. 使用AspectJ方式,獲取方法上註解value得知當前方法所需數據源名,修改ThreadLocal中數據源名
  3. 啓用事務時一定要注意代理方式的選擇
  4. 開源插件MyBatis-Plus支持動態數據源切換   https://mp.baomidou.com/guide/dynamic-datasource.html

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章