浅谈 Mybatis 动态数据源切换是如何实现的

Posted 张子行的博客

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅谈 Mybatis 动态数据源切换是如何实现的相关的知识,希望对你有一定的参考价值。

前言

小憩是辣么的让人神往,就像备战高考靠窗位置的那个你,肆无忌道的放空自己,望着深蓝色宁静的天空,思考着未来该何去何从,近处一颗高大魁梧的银杏树在炎炎夏日中尽情的摇曳着自己嫩绿的枝丫,迸发出无尽的希望,回想起来一切都是这么的美好。好了今日的杂想到此结束,回归正文,关于动态数据源切换那点事。

引导思考

如果咱们现在生活在互联网刚开始兴起的那个时期,万物堵塞,所有和数据库打交道的操作只能通过 JDBC 来实现,恰巧你是一个技术狂热爱好者,想通过自己的努力封装一套半 ORM 框架,造福千千万程序员,你会如何实现动态数据源切换这个扩展点呢?

  1. 当用户没有动态数据源切换的需求时:框架加载默认数据源给用户使用
  2. 当用户有动态数据源切换的需求时:提供一个官方认证的工具给用户使用,用户只需按照要求将多数据源配置好,系统会加载用户自定义的数据源

对于点一很好办:我们在框架内部默认创建一个名字为 dataSource(Spring Boot 默认数据源:HikariDataSource)、类型为 javax.sql.DataSource 的这么一个 数据源就好了。
对于点二来说:这个工具应该让用户的学习使用成本越低越好。从用户角度上分析,应该没有人希望把多数据源配置配在 Excel、Txt文件中吧,最优解配在 yml 文件中,然后利用 Spring 中的 Environment 类可以很容易加载到多数据源的配置。(题外话:基于 Spring 生态开发,少走弯路 30 年),到底最后是选择使用哪个数据源,设计思路有俩种:

  1. 可以支持加载多个数据源到系统中,写一个抽象类 AbstractDataSourceRoute 里面实现了 DataSource choiceDataSource(String type),将到底是使用哪个数据源的方法交给子类去实现 abstract String type(),这样用户只需实现 AbstractDataSourceRoute 类,指定一下使用哪个数据源,就可以了。伪代码如下
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

import javax.sql.DataSource;
import java.util.Map;

public abstract class DataSourceUtil implements DataSource, ApplicationContextAware, DisposableBean 
    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException 
        this.applicationContext = applicationContext;
    

    DataSource choiceDataSource() 
        //1:通过 applicationContext 获取所有数据源
        Map<String, DataSource> dataSourceMap = applicationContext.getBeansOfType(DataSource.class);
        //2:通过 type 筛选指定要用的数据源
        DataSource dataSource = dataSourceMap.get(type());
        //3:返回用户指定要使用的数据源
        return dataSource;
    
    abstract String type();

2: 限制系统加载一个数据源,扫描到自定义数据源,就不加载默认数据源,反之加载默认数据源

动态数据源切换之应用

先来简单介绍一下啊动态数据源用到的开发场景:

  1. 项目需要与其他系统对接,库是别人,因此需要配置多套数据源
  2. 项目本身根据实际需求设计数据库的时候,用到了多种的数据库,例如 mysql、Oracle同时使用,当然数据互通是个问题

使用场景介绍完了,接下来简单介绍一下如何使用吧~,就是读取 yml 配置中我们配置的数据源属性,利用@ConfigurationProperties注解完成属性的自动填充,继而注入到 IOC 容器中。这时候系统读取到了多个数据源,但是还不清楚什么时候用哪个数据源呢,因此我们可以编写一个切面,来动态的告知系统该如何选择数据源

@Configuration
public class DataSourceConfig 
    @Bean(name = "master")
    @ConfigurationProperties(prefix = "spring.datasource.druid.master.datasource")
    public DataSource master() 
        return new DruidDataSource();
    

    @Bean(name = "slave")
    @ConfigurationProperties(prefix = "spring.datasource.druid.salve.datasource")
    public DataSource slave() 
        return new DruidDataSource();
    

    @Bean
    @Primary
    public DynamicDataSource multipleDataSource(@Qualifier("master") DataSource db1, @Qualifier("slave") DataSource db2) 
        DynamicDataSource multipleDataSource = new DynamicDataSource();
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceType.getMASTER(), db1);
        targetDataSources.put(DataSourceType.getSALVE(), db2);
        multipleDataSource.setTargetDataSources(targetDataSources);
        multipleDataSource.setDefaultTargetDataSource(db1);
        DynamicDataSourceContextHolder.dataSourceIds.add(DataSourceType.getSALVE());
        DynamicDataSourceContextHolder.dataSourceIds.add(DataSourceType.getMASTER());
        return multipleDataSource;
    

    @Data
    static class DataSourceType 

        private static String MASTER = "master";
        private static String SALVE = "salve";

        public static String getMASTER() 
            return MASTER;
        

        public static String getSALVE() 
            return SALVE;
        
    

Aspect 切面

编写自定义注解 TargetDataSource ,并通过切面拦截它,根据 TargetDataSource 中的属性做判断,选择特定的数据源。里面的逻辑没啥好看的,就是在方法执行之前,获取方法或者类上面 TargetDataSource 中指定的属性,填充到 DynamicDataSourceContextHolder 中,后续我们重写 Mybatis 为我们提供的数据源选择钩子方法,返回 DynamicDataSourceContextHolder 中的数据就好了。这样一来就实现了,数据源切换的需求了,接下来看下这个钩子方法里面的源码吧~~

@Slf4j
@Aspect
@Order(-10)
@Component
public class AspectWithinAnnotation 

    /**
     * @within:拦截类上的注解
     * @annotation:拦截方法上的注解
     */
    @Before("@within(com.example.oraceldemo.config.aspect.TargetDataSource)||@annotation(com.example.oraceldemo.config.aspect.TargetDataSource)")
    public void changeDataSource(JoinPoint joinPoint) 
        TargetDataSource targetDataSource = getTargetDataSource(joinPoint);
        if (targetDataSource == null || !DynamicDataSourceContextHolder.isContainsDataSource(targetDataSource.name())) 
            log.error("使用默认的数据源 -> " + joinPoint.getSignature());
         else 
            log.debug("使用数据源:" + targetDataSource.name());
            DynamicDataSourceContextHolder.setDataSourceType(targetDataSource.name());
        
    

    @After("@within(com.example.oraceldemo.config.aspect.TargetDataSource)||@annotation(com.example.oraceldemo.config.aspect.TargetDataSource)")
    public void clearDataSource(JoinPoint joinPoint) 
        log.debug("清除数据源 " + getTargetDataSource(joinPoint).name() + " !");
        DynamicDataSourceContextHolder.clearDataSourceType();
    

    /**
     * 先从方法上获取 TargetDataSource 注解,获取不到从类上面获取
     */
    public TargetDataSource getTargetDataSource(JoinPoint joinPoint) 
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        TargetDataSource targetDataSource = method.getAnnotation(TargetDataSource.class);
        if (targetDataSource == null) 
            Class<?> declaringClass = method.getDeclaringClass();
            targetDataSource = declaringClass.getAnnotation(TargetDataSource.class);
        
        return targetDataSource;
    


Mybatis 扩展点之 AbstractRoutingDataSource

又是这个 Abstract 开头的类~,记住所有以 Abstract 开头的类,他的父类才是真正干活的人,因为详细的逻辑都封装在父类中了,爸爸才是全家的顶梁柱、主心骨啊,所有的风雨、压力都是爸爸来抗,只为了子女脸上洋溢着的灿烂的笑容。

public class DynamicDataSource extends AbstractRoutingDataSource 
    /**
     * 选择数据源钩子方法
     */
    @Override
    protected Object determineCurrentLookupKey() 
        return DynamicDataSourceContextHolder.getDataSourceType();
    

    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) 
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    

    public DynamicDataSource() 
    


浅浅的 debug 一下源码吧

在钩子方法中打一个断点,看一下它的执行链路,一次往上翻就好了,来到 AbstractRoutingDataSource 中的 determineTargetDataSource 方法,粗略的一看,这代码和本文开头引导思考中的伪代码,不就是一个模子里刻出来的咩~由于代码思路都是一样的,就不过多的做分析了。为了读者方便理解再次贴一遍原话如下~

写一个抽象类 AbstractDataSourceRoute 里面实现了 DataSource choiceDataSource(String type),将到底是使用哪个数据源的方法交给子类去实现 abstract String type(),这样用户只需实现 AbstractDataSourceRoute 类中的钩子方法,指定一下使用哪个数据源,就可以了。

浅浅的解读一下 resolvedDataSources 吧

上图一从 resolvedDataSources 中根据钩子方法的返回值,获取指定的数据源返回,那么 resolvedDataSources 中的数据源是什么时候注入的呢?答案入下图,利用了 Spring 中的 InitializingBean 接口,在Bean属性填充完毕后,将 targetDataSources 中数据源全部放到 resolvedDataSources 中,这样一来,我们用户只需指定指定钩子方法中的数据源类别,当方法被调用的时候,切面就会截取方法、类上面的自定义注解,填充到 ThreadLocal 中,然后后续的 Mybatis 获取数据源查 DB 的时候,根据钩子方法的返回值,从 resolvedDataSources 中获取指定的数据源然后查 DB 从而实现了,数据源随意切换的效果

注意点

由于本文中的切面拦截的是自定义注解,且切入点是使用 @within、@annotation来进行修饰的(即只会识别被调用方法对应的类上、或者是被调用方法上是否存在自定义注解),考虑到现在大多数人都在用 MybatisPlus~,正确用法入下图一、二,图三为错误用法,因为,我们调用的 List 方法本质还是 ServiceImpl 类上的,然而ServiceImpl 类上没有有被我们的注解修饰,故此时切面会失效,本文切面解析注解的范围如下图四。当然可能还有人切入点采用 @Before(“execution(* com.example.oraceldemo.service..(…))”) ,也可能出现切面失效、切换数据源失效的情况,大致的分析过程差不多,读者有兴趣可以自行去分析哦~~~



@Autowired
private TabStarService tabStarService;

@Test
void xiaomi() 
    List<Goods> list = goodsService.list();
    System.err.println(list);

以上是关于浅谈 Mybatis 动态数据源切换是如何实现的的主要内容,如果未能解决你的问题,请参考以下文章

mybatis plus自定义的mapper如何动态切换数据源

springboot+mybatis实现动态切换数据源

spring+mybatis+tkmapper+atomikos实现分布式事务-动态切换数据源

Spring+Mybatis动态切换数据源

基于spring+mybatis+atomikos+jta实现分布式事务-动态切换数据源

mybatis动态切换数据源