项目地址: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