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