MyBatis-Plus多数据源学习笔记

1,060 阅读10分钟

写在前面,发现问题

​ 在现在的环境中,开发时经常遇到一个程序需要同时访问多个数据库的情况,MyBatis-Plus提供了快捷方式的配置方式。如在yaml配置文件里配置多个数据源db1、db2、db3,当需要使用哪个数据源时只需要在想要使用的类或者方法上加上@DS("db1")、@DS("db2")、@DS("db3“)注解,轻松实现数据源的切换。如果没有指定数据源,默认使用master数据源,即配置里的第一个。

​ 使用过程中遇到了一个问题,在需要使用其他数据源的Mapper接口声明了@DS("slave")访问slave数据库,但是在动态sql构造器执行sql时,仍然访问master主数据源,于是乎就报错 表或视图不存在。

public void AService{
    @Resourece
    private AMapper a;
    
    @Resourece
    private BMapper b;
    
    void test(){
        //访问master数据源
        a.selectA();
        //访问slave数据源
        b.selectB();
        //访问master数据源,同时报错表或试图不存在
        b.list(new LambdaQueryWrapper().eq("id",1));
    }
}

@DS("master")
public Interface AMapper{
    void selectA();
}

@DS("slave")
public Interface BMapper extend BaseMapper<B>{
    void selectB();
}

解读源码,询找答案

1. pom文件引用

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
    <version>3.0.0</version>
</dependency>

2. 自动配置类

在jar包中找到 META-INF/spring.factories,一般里面指定自动配置类

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration

打开DynamicDataSourceAutoConfiguration

@Configuration
@EnableConfigurationProperties({DynamicDataSourceProperties.class})
//spring在装配DataSourceAutoConfiguration的bean之前,先装配当前类bean
@AutoConfigureBefore({DataSourceAutoConfiguration.class})
//引用了Druid的动态数据源配置
@Import({DruidDynamicDataSourceConfiguration.class})
//条件加载,配置文件的前缀是"spring.datasource.dynamic"时启动这个类
@ConditionalOnProperty(
    prefix = "spring.datasource.dynamic",
    name = {"enabled"},
    havingValue = "true",
    matchIfMissing = true
)
public class DynamicDataSourceAutoConfiguration {
    private static final Logger log = LoggerFactory.getLogger(DynamicDataSourceAutoConfiguration.class);
    @Autowired
    private DynamicDataSourceProperties properties;
    @Autowired(
        required = false
    )
    private ApplicationContext applicationContext;

    public DynamicDataSourceAutoConfiguration() {
    }
	//读取多数据源的配置,注入到Spring容器中
    @Bean
    @ConditionalOnMissingBean
    public DynamicDataSourceProvider dynamicDataSourceProvider() {
        return new YmlDynamicDataSourceProvider(this.properties);
    }
	//创建连接池的适配器
    @Bean
    @ConditionalOnMissingBean
    public DataSourceCreator dataSourceCreator() {
        DataSourceCreator dataSourceCreator = new DataSourceCreator();
        dataSourceCreator.setDruidDataSourceCreator(new DruidDataSourceCreator(this.properties.getDruid(), this.applicationContext));
        dataSourceCreator.setHikariDataSourceCreator(new HikariDataSourceCreator(this.properties.getHikari()));
        dataSourceCreator.setGlobalPublicKey(this.properties.getPublicKey());
        return dataSourceCreator;
    }

	//注册动态数据源DataSource
    @Bean
    @ConditionalOnMissingBean
    public DataSource dataSource(DynamicDataSourceProvider dynamicDataSourceProvider) {
        DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource();
        dataSource.setPrimary(this.properties.getPrimary());
        dataSource.setStrategy(this.properties.getStrategy());
        dataSource.setProvider(dynamicDataSourceProvider);
        dataSource.setP6spy(this.properties.getP6spy());
        dataSource.setStrict(this.properties.getStrict());
        dataSource.setSeata(this.properties.getSeata());
        return dataSource;
    }

	//注册使用Cglib方式实现动态代理,完成AOP切面,从而达到切换数据源的目的
    @Bean
    @ConditionalOnMissingBean
    public DynamicDataSourceAnnotationAdvisor dynamicDatasourceAnnotationAdvisor(DsProcessor dsProcessor) {
        DynamicDataSourceAnnotationInterceptor interceptor = new DynamicDataSourceAnnotationInterceptor();
        interceptor.setDsProcessor(dsProcessor);
        DynamicDataSourceAnnotationAdvisor advisor = new DynamicDataSourceAnnotationAdvisor(interceptor);
        advisor.setOrder(this.properties.getOrder());
        return advisor;
    }

	//注册spel表达式,动态参数解析器链
    @Bean
    @ConditionalOnMissingBean
    public DsProcessor dsProcessor() {
        DsHeaderProcessor headerProcessor = new DsHeaderProcessor();
        DsSessionProcessor sessionProcessor = new DsSessionProcessor();
        DsSpelExpressionProcessor spelExpressionProcessor = new DsSpelExpressionProcessor();
        headerProcessor.setNextProcessor(sessionProcessor);
        sessionProcessor.setNextProcessor(spelExpressionProcessor);
        return headerProcessor;
    }
	
	//使用spel方式来切换数据源(实验性功能)
    @Bean
    @ConditionalOnBean({DynamicDataSourceConfigure.class})
    public DynamicDataSourceAdvisor dynamicAdvisor(DynamicDataSourceConfigure dynamicDataSourceConfigure, DsProcessor dsProcessor) {
        DynamicDataSourceAdvisor advisor = new DynamicDataSourceAdvisor(dynamicDataSourceConfigure.getMatchers());
        advisor.setDsProcessor(dsProcessor);
        advisor.setOrder(-2147483648);
        return advisor;
    }
}

以上的几个bean的配置便是Mybatis-Plus的核心配置了。

3. 数据源配置

可以看出,配置文件中是以"spring.datasource.dynamic"为前缀读取配置信息

@ConfigurationProperties(
    prefix = "spring.datasource.dynamic"
)
public class DynamicRoutingDataSourceDynamicDataSourceProperties {
    private static final Logger log = LoggerFactory.getLogger(DynamicDataSourceProperties.class);
    public static final String PREFIX = "spring.datasource.dynamic";
    public static final String HEALTH = "spring.datasource.dynamic.health";
    // 默认的库是master
    private String primary = "master";
    //严格模式:true时未匹配数据源直接报错,false时未匹配数据源采用primary数据源
    private Boolean strict = false;
    private Boolean p6spy = false;
    private Boolean seata = false;
    //是否使用spring actuator监控检查,默认不检查
    private boolean health = false;
    //存储数据源
    private Map<String, DataSourceProperty> datasource = new LinkedHashMap();
    //多数据源的选择算法,默认负载均衡
    private Class<? extends DynamicDataSourceStrategy> strategy = LoadBalanceDynamicDataSourceStrategy.class;
    //AOP切面的顺序,默认优先级最高
    private Integer order = -2147483648;
    //Druid全局参数配置
    @NestedConfigurationProperty
    private DruidConfig druid = new DruidConfig();
    //Hikari全局参数配置
    @NestedConfigurationProperty
    private HikariCpConfig hikari = new HikariCpConfig();
    //全局默认的公钥
    private String publicKey = "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAJ4o6sn4WoPmbs7DR9mGQzuuUQM9erQTVPpwxIzB0ETYkyKffO097qXVRLA6KPmaV+/siWewR7vpfYYjWajw5KkCAwEAAQ==";

    public DynamicDataSourceProperties() {
    }
...
}

可以发现,所有在spring.datasource.dynamic配置的内容都是注入到这个bean中,其中在创建Druid和Hikari连接池的全局参数配置时,套用了@NestedConfigurationProperty注解,这是嵌套了又一层的复杂注解。分别点击进入配置类,就可以看到可以配置的更详细的配置项。

4. 注册动态数据源DataSource

在注册动态数据源DataSource时,创建了DynamicRoutingDataSource对象,而DynamicRoutingDataSource继承自AbstractRoutingDataSource抽象类,实现了InitializingBean接口。 关注1.重写的父类的抽象方法,2实现接口定义的方法。

public class DynamicRoutingDataSource extends AbstractRoutingDataSource implements InitializingBean, DisposableBean {
    private static final Logger log = LoggerFactory.getLogger(DynamicRoutingDataSource.class);
    private static final String UNDERLINE = "_";
    private DynamicDataSourceProvider provider;
    private Class<? extends DynamicDataSourceStrategy> strategy;
    private String primary;
    private boolean strict;
    private boolean p6spy;
    private boolean seata;
    //所有数据库
    private Map<String, DataSource> dataSourceMap = new LinkedHashMap();
    //分组数据库
    private Map<String, DynamicGroupDataSource> groupDataSources = new ConcurrentHashMap();

    public DynamicRoutingDataSource() {
    }
	//继承自父类的方法重写:决定选择数据源,看到peek()方法可以联想到这是一个队列
    public DataSource determineDataSource() {
        return this.getDataSource(DynamicDataSourceContextHolder.peek());
    }
	...
	
	//真正决定选择数据源的地方,无指定则首选,有指定则指定,严格模式未匹配报错,非严格模式未匹配首选
    public DataSource getDataSource(String ds) {
        if (StringUtils.isEmpty(ds)) {
            return this.determinePrimaryDataSource();
        } else if (!this.groupDataSources.isEmpty() && this.groupDataSources.containsKey(ds)) {
            log.debug("dynamic-datasource switch to the datasource named [{}]", ds);
            return ((DynamicGroupDataSource)this.groupDataSources.get(ds)).determineDataSource();
        } else if (this.dataSourceMap.containsKey(ds)) {
            log.debug("dynamic-datasource switch to the datasource named [{}]", ds);
            return (DataSource)this.dataSourceMap.get(ds);
        } else if (this.strict) {
            throw new RuntimeException("dynamic-datasource could not find a datasource named" + ds);
        } else {
            return this.determinePrimaryDataSource();
        }
    }
    
    //实现接口定义的方法:bean实例化后的属性赋值,获取关于dataSources的map集合,key是数据源名称,value是DataSource
     public void afterPropertiesSet() throws Exception {
        Map<String, DataSource> dataSources = this.provider.loadDataSources();
        Iterator var2 = dataSources.entrySet().iterator();
        while(var2.hasNext()) {
            Entry<String, DataSource> dsItem = (Entry)var2.next();
            //addDataSource添加数据源
            this.addDataSource((String)dsItem.getKey(), (DataSource)dsItem.getValue());
        }

        if (this.groupDataSources.containsKey(this.primary)) {
            log.info("dynamic-datasource initial loaded [{}] datasource,primary group datasource named [{}]", dataSources.size(), this.primary);
        } else {
            if (!this.dataSourceMap.containsKey(this.primary)) {
                throw new RuntimeException("dynamic-datasource Please check the setting of primary");
            }

            log.info("dynamic-datasource initial loaded [{}] datasource,primary datasource named [{}]", dataSources.size(), this.primary);
        }

    }
    
    ...
}

动态数据源注册到Map集合中,有需要用的时候再从Map中取,在初始化bean获取Map的过程中用到了负载均衡的算法,下章再看provider构造器。同时DynamicDataSourceContextHolder.peek()方法也引人注目。

5. 注册数据源的构造器DynamicDataSourceProvider

public interface DynamicDataSourceProvider {
    Map<String, DataSource> loadDataSources();
}

这是一个构造器的抽象接口,在查找实现类的时候发现了有两个。一个是JDBC的实现类,用于数据库的连接。另一个是Yml的实现类,用于创建上文中的数据源Map,此处重点关注创建。

public class YmlDynamicDataSourceProvider extends AbstractDataSourceProvider implements DynamicDataSourceProvider {
    private static final Logger log = LoggerFactory.getLogger(YmlDynamicDataSourceProvider.class);
    private DynamicDataSourceProperties properties;

    public YmlDynamicDataSourceProvider(DynamicDataSourceProperties properties) {
        this.properties = properties;
    }

    public Map<String, DataSource> loadDataSources() {
        Map<String, DataSourceProperty> dataSourcePropertiesMap = this.properties.getDatasource();
        return this.createDataSourceMap(dataSourcePropertiesMap);
    }
}

调用的创建方式在父类中,继续看父类

public abstract class AbstractDataSourceProvider implements DynamicDataSourceProvider {
    private static final Logger log = LoggerFactory.getLogger(AbstractDataSourceProvider.class);
    @Autowired
    private DataSourceCreator dataSourceCreator;

    public AbstractDataSourceProvider() {
    }

    protected Map<String, DataSource> createDataSourceMap(Map<String, DataSourceProperty> dataSourcePropertiesMap) {
        Map<String, DataSource> dataSourceMap = new HashMap(dataSourcePropertiesMap.size() * 2);
        Iterator var3 = dataSourcePropertiesMap.entrySet().iterator();

        while(var3.hasNext()) {
            Entry<String, DataSourceProperty> item = (Entry)var3.next();
            DataSourceProperty dataSourceProperty = (DataSourceProperty)item.getValue();
            String pollName = dataSourceProperty.getPollName();
            if (pollName == null || "".equals(pollName)) {
                pollName = (String)item.getKey();
            }

            dataSourceProperty.setPollName(pollName);
            dataSourceMap.put(pollName, this.dataSourceCreator.createDataSource(dataSourceProperty));
        }

        return dataSourceMap;
    }
}

跟踪又发现,createDataSourceMap方法中调用的是dataSourceCreator.createDataSource(dataSourceProperty)

6. 创建连接池的适配器

这里的createDataSource方法使用了适配器模式,每种配置数据源创建的方式不尽相同,根据配置中的type或者连接池的class来判定该创建什么类型的连接池。

public class DataSourceCreator {
    private static final Logger log = LoggerFactory.getLogger(DataSourceCreator.class);
    private static Boolean druidExists = false;
    private static Boolean hikariExists = false;
    private HikariDataSourceCreator hikariDataSourceCreator;
    private DruidDataSourceCreator druidDataSourceCreator;
    private String globalPublicKey;

    public DataSourceCreator() {
    }

    public DataSource createDataSource(DataSourceProperty dataSourceProperty) {
        String jndiName = dataSourceProperty.getJndiName();
        DataSource dataSource;
        if (jndiName != null && !jndiName.isEmpty()) {
            dataSource = this.createJNDIDataSource(jndiName);
        } else {
            Class<? extends DataSource> type = dataSourceProperty.getType();
            if (type == null) {
                if (druidExists) {
                    dataSource = this.createDruidDataSource(dataSourceProperty);
                } else if (hikariExists) {
                    dataSource = this.createHikariDataSource(dataSourceProperty);
                } else {
                    dataSource = this.createBasicDataSource(dataSourceProperty);
                }
            } else if ("com.alibaba.druid.pool.DruidDataSource".equals(type.getName())) {
                dataSource = this.createDruidDataSource(dataSourceProperty);
            } else if ("com.zaxxer.hikari.HikariDataSource".equals(type.getName())) {
                dataSource = this.createHikariDataSource(dataSourceProperty);
            } else {
                dataSource = this.createBasicDataSource(dataSourceProperty);
            }
        }

        this.runScrip(dataSourceProperty, dataSource);
        return dataSource;
    }
    ...
}

7. 数据源的新增

在注册动态数据源DataSource时,初始化过程中,先创建了连接池,此时回头再看如何加入数据源Map中的。

//递归的方式创建数据源Map和分组
public synchronized void addDataSource(String ds, DataSource dataSource) {
        if (this.p6spy) {
            dataSource = new P6DataSource((DataSource)dataSource);
            log.info("dynamic-datasource [{}] wrap p6spy plugin", ds);
        }

        if (this.seata) {
            dataSource = new DataSourceProxy((DataSource)dataSource);
            log.info("dynamic-datasource [{}] wrap seata plugin", ds);
        }

        this.dataSourceMap.put(ds, dataSource);
        if (ds.contains("_")) {
            String group = ds.split("_")[0];
            if (this.groupDataSources.containsKey(group)) {
                ((DynamicGroupDataSource)this.groupDataSources.get(group)).addDatasource((DataSource)dataSource);
            } else {
                try {
                    DynamicGroupDataSource groupDatasource = new DynamicGroupDataSource(group, (DynamicDataSourceStrategy)this.strategy.newInstance());
                    groupDatasource.addDatasource((DataSource)dataSource);
                    this.groupDataSources.put(group, groupDatasource);
                } catch (Exception var5) {
                    log.error("dynamic-datasource - add the datasource named [{}] error", ds, var5);
                    this.dataSourceMap.remove(ds);
                }
            }
        }

        log.info("dynamic-datasource - load a datasource named [{}] success", ds);
    }

8. DynamicDataSourceContextHolder的思考

上文提到获取数据源是获取DynamicDataSourceContextHolder.peek(),用于获取数据源

public final class DynamicDataSourceContextHolder {
	//这是一个链表存储,为什么要用链表存储
    private static final ThreadLocal<Deque<String>> LOOKUP_KEY_HOLDER = new ThreadLocal() {
        protected Object initialValue() {
            return new ArrayDeque();
        }
    };

    private DynamicDataSourceContextHolder() {
    }
	//获取当前线程的的数据源
    public static String peek() {
        return (String)((Deque)LOOKUP_KEY_HOLDER.get()).peek();
    }
	//设置当前线程的数据源
    public static void push(String ds) {
        ((Deque)LOOKUP_KEY_HOLDER.get()).push(StringUtils.isEmpty(ds) ? "" : ds);
    }
	//清空当前线程的数据源
    public static void poll() {
        Deque<String> deque = (Deque)LOOKUP_KEY_HOLDER.get();
        deque.poll();
        if (deque.isEmpty()) {
            LOOKUP_KEY_HOLDER.remove();
        }

    }
	//强制清空所有数据源
    public static void clear() {
        LOOKUP_KEY_HOLDER.remove();
    }
}

这里为什么使用栈?

9. DynamicDataSourceAnnotationAdvisor切面编程,DS注解切换

注解拦截处理离不开AOP,利用Cglib完成对动态代理。

Spring AOP的概念

Advices:描述需要在方法执行的什么时机织入代码

PointCut:描述在哪些类的哪些方法执行织入代码

Advisor:说明这个类是切面组成的类,由Advice和Pointcut两部分组成

  • Advisor 切面
public class DynamicDataSourceAnnotationAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware {
    //通知
    private Advice advice;
    //切点
    private Pointcut pointcut;
	//动态代理织入
    public DynamicDataSourceAnnotationAdvisor(@NonNull DynamicDataSourceAnnotationInterceptor dynamicDataSourceAnnotationInterceptor) {
        if (dynamicDataSourceAnnotationInterceptor == null) {
            throw new NullPointerException("dynamicDataSourceAnnotationInterceptor");
        } else {
            this.advice = dynamicDataSourceAnnotationInterceptor;
            this.pointcut = this.buildPointcut();
        }
    }

    public Pointcut getPointcut() {
        return this.pointcut;
    }

    public Advice getAdvice() {
        return this.advice;
    }

    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        if (this.advice instanceof BeanFactoryAware) {
            ((BeanFactoryAware)this.advice).setBeanFactory(beanFactory);
        }

    }

    private Pointcut buildPointcut() {
        //类上加了注解
        Pointcut cpc = new AnnotationMatchingPointcut(DS.class, true);
        //方法上添加了注解
        Pointcut mpc = AnnotationMatchingPointcut.forMethodAnnotation(DS.class);
        //方法的优先级更高
        return (new ComposablePointcut(cpc)).union(mpc);
    }
}
  • PointCut 切点,被用@DS注解的类和方法会作为切点
//作用在类上和在方法上
@Target({ElementType.TYPE, ElementType.METHOD})
//注解在运行时一直保留
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DS {
    String value();
}
  • Advices 通知
public class DynamicDataSourceAnnotationInterceptor implements MethodInterceptor {
    private static final String DYNAMIC_PREFIX = "#";
    private static final DynamicDataSourceClassResolver RESOLVER = new DynamicDataSourceClassResolver();
    private DsProcessor dsProcessor;

    public DynamicDataSourceAnnotationInterceptor() {
    }

    public Object invoke(MethodInvocation invocation) throws Throwable {
        Object var2;
        try {
            //找到执行的数据源,并推入栈中
            DynamicDataSourceContextHolder.push(this.determineDatasource(invocation));
            //执行当前方法
            var2 = invocation.proceed();
        } finally {
            //数据源出栈
            DynamicDataSourceContextHolder.poll();
        }

        return var2;
    }

    //责任链模式的处理
    private String determineDatasource(MethodInvocation invocation) throws Throwable {
        //获取DS注解的内容
        Method method = invocation.getMethod();
        DS ds = method.isAnnotationPresent(DS.class) ? (DS)method.getAnnotation(DS.class) : (DS)AnnotationUtils.findAnnotation(RESOLVER.targetClass(invocation), DS.class);
        //获取DS注解的内容
        String key = ds.value();
        //如果DS注解内容以#开头解析动态最终值,否则直接返回
        return !key.isEmpty() && key.startsWith("#") ? this.dsProcessor.determineDatasource(invocation, key) : key;
    }

    public void setDsProcessor(DsProcessor dsProcessor) {
        this.dsProcessor = dsProcessor;
    }
}

10. 切换的场景

默认master为主数据源,slave的第二数据源,非严格模式。

  1. AService无注解==》访问master数据源

  2. AService无注解,AMethod注解DS("slave")==》访问slave数据源

  3. AService无注解,AMapper注解DS("slave")==》访问slave数据源

  4. AService无注解,BSerivice注解DS("slave"),AService中方法调用BService的方法=》访问slave数据源,B类方法出栈后重新访问master数据源。

  5. AService无注解,BMapper注解DS("slave"),AService中方法调用BMapper的方法=》访问slave数据源,B类方法出栈后重新访问master数据源。

找到答案,解决问题

重新看待问题

public void AService{
    @Resourece
	private AMapper a;
    
    @Resourece
	private BMapper b;
    
    void test(){
        //访问master数据源
        a.selectA();
        //访问slave数据源
        b.selectB();
        //list方法是属于BaseMapper类的方法,没有被DS注解修饰,不织入切换数据源的代码当前线程是master数据源
        b.list(new LambdaQueryWrapper().eq("id",1));
    }
}

@DS("master")
public Interface AMapper{
    void selectA();
}

@DS("slave")
public Interface BMapper extend BaseMapper<B>{
    void selectB();
}

修改方案

  1. AService不直接调用BMapper.list(),改为调用BService,再在BService的方法中调用BMapper
  2. AService不直接调用BMapper.list(),讲BMapper.list()抽取成private方法并用DS("slave")修饰,再调用此方法。

总结

曾经给自己定下了学习时,阅读XXX、XXX源码的目标,但是现在看来这是一个非常愚蠢的想法。源码编译耗时,断点Debug耗时,去学习去了解也非常耗时,但是学习出来的效果确实非常的糟糕,容易事倍功半。只有带着问题,带着目的去看去学源码,才能有的放矢,做到事半功倍。

本次学习感触有三:

  1. 只会API只能说明在应用层会使用,有实际的使用经验,但是如果产生问题只能凭借经验和印象去还原能用的方案。
  2. 解决问题时,只有深入了解其根本原理,才能一刀见血解决问题,避免重复试错返工,耗时耗力。
  3. 学习的理解深刻,初次分享文章成就感满满,我将再接再厉!如果有错误,不吝赐教,望指正。