MybatisPlus多数据源原理与问题简析

news2024/11/25 6:34:48

文章目录

  • 1. 使用
    • 1.1. 引包
    • 1.2 增加配置
    • 1.3 使用@DS注解
  • 2. 源码
    • 2.1 Configuration文件, 加载配置与bean注入
      • 2.1.1 配置类
    • 2.2 注册DataSource
      • 2.2.1 获取所有的DataSource
        • 2.2.1.1 构建DataSource
      • 2.2.2 对数据源分组
    • 2.3 切换数据源
      • 2.3.1 DynamicDataSourceContextHolder
      • 2.3.2 DynamicDataSourceAnnotationInterceptor
      • 2.3.4 DynamicDataSourceAnnotationAdvisor
    • 2.4 数据源的处理器
  • 3. Q&A
    • 3.1 @Transactional 和 @DS 注解一起使用时, @DS失效的问题
    • 3.2 低于V3.3.3版本无法切换数据源的问题

1. 使用

1.1. 引包

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

用最新版的就好 Maven Repository: dynamic-datasource-spring-boot-starter

不过做开发的经验来看, 应该选 倒数第二个大版本里最新的版本, 这样既能用更多的功能,又能保证没有大bug, 除非最新版本有你需要的功能

1.2 增加配置

spring:
  datasource:
    dynamic:
      primary: master #设置默认的数据源或者数据源组,默认值即为master, 所以此处不写也可以
      strict: false #设置严格模式,默认false不启动. 启动后在未匹配到指定数据源时候回抛出异常,不启动会使用默认数据源.
      datasource:
        master: # 第一个数据源的名称
          url: jdbc:mysql://127.0.0.1:3306/dynamic
          username: root
          password: 123456
          driver-class-name: com.mysql.jdbc.Driver
        db_1: # 第二个数据源的名称
          url: jdbc:gbase://127.0.0.1:5258/dynamic
          username: root
          password: 123456
          driver-class-name: com.mysql.jdbc.Driver

如果是多主多从或者一主多从,那么就用分组的功能实现, 即: 数据组名称_xxx,下划线前面的就是数据组名称,后面就是数据源名, 相同组名称的数据源会放在一个组下。切换数据源时,可以指定具体数据源名称,也可以指定组名然后会自动采用负载均衡算法切换,

例如: 上面配置中的"db_1", 表示db分组, 1 名称的数据源, 如果再增加一个"db_2", 则表示这两个数据源是同一个分组, 当在@DS中只指定"db", 则会在这个分组中采用负载均衡方式选数据源(默认是轮询)

1.3 使用@DS注解

@DS注解, 是用来表示使用哪个数据源的, 也是切换数据源功能的核心注解, 它可以用来类和方法上, 方法上的优先级更高, 例如 service层, Mapper层,都可以, 如果没有配置,则会使用默认数据源

@Service
@DS("master") // 在类上加
public class UserServiceImpl implements UserService {

  @Autowired
  private JdbcTemplate jdbcTemplate;

  public List<Map<String, Object>> selectAll() {
    return jdbcTemplate.queryForList("select * from user");
  }
  
  @Override
  @DS("db_1") // 在方法上加
  public List<Map<String, Object>> selectByCondition() {
    return jdbcTemplate.queryForList("select * from user where age >10");
  }
}

官方建议不建议@DS放在Mapper上, 所以网上有很多例子是放在service上,

但是这有个问题, 当在service中需要访问两个数据库时, 就会无法切换数据源, 毕竟@DS是加在service上, 在同一个service上操作, 自然不会切换, 官方也给出解决方案, 在方法上进一步指定数据源, (伪代码)例如:

class Person {
    @Autowired
    dbService1 db_1;
    @Autowired
    dbService2 db_2;
    
    void test(){
        db_1.save();
        // 如果 db_2.save() 不指定数据源, 数据源会使用db_1
        db_2.save();
    }
}

@DS("db_1")
class dbService1 {
    void save(){
        
    }
}

@DS("db_2")
class dbService2 {
    @DS("db_2")
    void save(){
        
    }
}

mybatis-plus中多数据源切换@DS注解到底放在哪一层合适?

@DS不建议加在mapper上的原因是?

2. 源码

一般starter自动配置,都是从 META-INF/spring.factories文件中指定自动配置类:

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

所以打开 DynamicDataSourceAutoConfiguration 文件

2.1 Configuration文件, 加载配置与bean注入

// 动态数据源核心自动配置类
@Slf4j
@Configuration
@EnableConfigurationProperties(DynamicDataSourceProperties.class)
// @AutoConfigureBefore 注解的意思是 在DataSourceAutoConfiguration和DruidDataSourceAutoConfigure之前就注册当前bean
// 这里特别注意一下, 在 V3.3.3 之前, 没有把DruidDataSourceAutoConfigure加进来, 所以导致项目中有druid时,数据源无法切换或没有druid配置则报错
@AutoConfigureBefore(value = DataSourceAutoConfiguration.class, name = "com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure")
//引入了Druid的autoConfig和各种数据源连接池的Creator
@Import(value = {DynamicDataSourceCreatorAutoConfiguration.class, DynamicDataSourceAopConfiguration.class, DynamicDataSourceAssistConfiguration.class})
@ConditionalOnProperty(prefix = DynamicDataSourceProperties.PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true)
public class DynamicDataSourceAutoConfiguration  {

    private final DynamicDataSourceProperties properties;

    //读取多数据源配置,注入到spring容器中
    @Bean
    @ConditionalOnMissingBean
    public DynamicDataSourceProvider dynamicDataSourceProvider() {
        Map<String, DataSourceProperty> datasourceMap = properties.getDatasource();
        return new YmlDynamicDataSourceProvider(datasourceMap);
    }

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

    //AOP切面,对DS注解过的方法进行增强,达到切换数据源的目的
    @Role(value = BeanDefinition.ROLE_INFRASTRUCTURE)
    @Bean
    @ConditionalOnMissingBean
    public DynamicDataSourceAnnotationAdvisor dynamicDatasourceAnnotationAdvisor(DsProcessor dsProcessor) {
        DynamicDataSourceAnnotationInterceptor interceptor = new DynamicDataSourceAnnotationInterceptor(properties.isAllowedPublicOnly(), dsProcessor);
        DynamicDataSourceAnnotationAdvisor advisor = new DynamicDataSourceAnnotationAdvisor(interceptor);
        advisor.setOrder(properties.getOrder());
        return advisor;
    }

    //关于分布式事务加强
    @Role(value = BeanDefinition.ROLE_INFRASTRUCTURE)
    @ConditionalOnProperty(prefix = DynamicDataSourceProperties.PREFIX, name = "seata", havingValue = "false", matchIfMissing = true)
    @Bean
    public Advisor dynamicTransactionAdvisor() {
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression("@annotation(com.baomidou.dynamic.datasource.annotation.DSTransactional)");
        return new DefaultPointcutAdvisor(pointcut, new DynamicTransactionAdvisor());
    }

    //动态参数解析器链
    @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;
    }
}

我们可以发现,在使用的时候配置的前缀为spring.datasource.dynamic的配置都会被读取到DynamicDataSourceProperties类,作为一个Bean注入到Spring容器。其实这种读取配置文件信息的方式在日常开发中也是很常见的。

2.1.1 配置类

DynamicDataSourceProperties

@ConfigurationProperties(prefix = DynamicDataSourceProperties.PREFIX)
public class DynamicDataSourceProperties {

    public static final String PREFIX = "spring.datasource.dynamic";
    public static final String HEALTH = PREFIX + ".health";

    /**
     * 必须设置默认的库,默认master
     */
    private String primary = "master";
    /**
     * 是否启用严格模式,默认不启动. 严格模式下未匹配到数据源直接报错, 非严格模式下则使用默认数据源primary所设置的数据源
     */
    private Boolean strict = false;
    
    // .......
}

2.2 注册DataSource

DynamicDataSourceAutoConfiguration 类中, 还加载了DataSource, 我们知道 DataSource是spring用来链接数据库的,

它注册了一个实现类 DynamicRoutingDataSource, 这个类还有一个父类AbstractRoutingDataSource, 我们先看AbstractRoutingDataSource

/**
 * 抽象动态获取数据源
 *
 * @author TaoYu
 * @since 2.2.0
 */
public abstract class AbstractRoutingDataSource extends AbstractDataSource {

    //抽象方法,由子类实现,让子类决定最终使用的数据源
    protected abstract DataSource determineDataSource();

    //重写getConnection()方法,实现切换数据源的功能
    @Override
    public Connection getConnection() throws SQLException {
        //这里xid涉及分布式事务的处理
        String xid = TransactionContext.getXID();
        if (StringUtils.isEmpty(xid)) {
            //不使用分布式事务,就是直接返回一个数据连接(实现类: ItemDataSource.class)
            return determineDataSource().getConnection();
        } else {
            String ds = DynamicDataSourceContextHolder.peek();
            ConnectionProxy connection = ConnectionFactory.getConnection(ds);
            return connection == null ? getConnectionProxy(ds, determineDataSource().getConnection()) : connection;
        }
    }
}

父类很简单, 只是区分了分布式事务的取connect的方式,所以再来看下其子类DynamicRoutingDataSource 的内容

@Slf4j
public class DynamicRoutingDataSource extends AbstractRoutingDataSource implements InitializingBean, DisposableBean {

    private static final String UNDERLINE = "_";
    /**
     * 所有数据库
     */
    private final Map<String, DataSource> dataSourceMap = new ConcurrentHashMap<>();
    /**
     * 分组数据库
     */
    private final Map<String, GroupDataSource> groupDataSources = new ConcurrentHashMap<>();
    @Setter
    private DynamicDataSourceProvider provider;
    @Setter
    private Class<? extends DynamicDataSourceStrategy> strategy = LoadBalanceDynamicDataSourceStrategy.class;
    @Setter
    private String primary = "master";
    @Setter
    private Boolean strict = false;
    @Setter
    private Boolean p6spy = false;
    @Setter
    private Boolean seata = false;

    @Override
    public DataSource determineDataSource() {
        // 获得数据源
        return getDataSource(DynamicDataSourceContextHolder.peek());
    }
    
    private DataSource determinePrimaryDataSource() {
        log.debug("dynamic-datasource switch to the primary datasource");
        return groupDataSources.containsKey(primary) ? groupDataSources.get(primary).determineDataSource() : dataSourceMap.get(primary);
    }
    
    // bean的属性加载完后, 执行该方法
    @Override
    public void afterPropertiesSet() throws Exception {
        // 检查开启了配置但没有相关依赖
        checkEnv();
		// 拿到所有的数据源, Key是数据源的名称,Value则是DataSource。
        Map<String, DataSource> dataSources = provider.loadDataSources();
        // 添加并分组数据源
        for (Map.Entry<String, DataSource> dsItem : dataSources.entrySet()) {
            addDataSource(dsItem.getKey(), dsItem.getValue());
        }
        // 检测默认数据源是否设置
        if (groupDataSources.containsKey(primary)) {
            log.info("dynamic-datasource initial loaded [{}] datasource,primary group datasource named [{}]", dataSources.size(), primary);
        } else if (dataSourceMap.containsKey(primary)) {
            log.info("dynamic-datasource initial loaded [{}] datasource,primary datasource named [{}]", dataSources.size(), primary);
        } else {
            throw new RuntimeException("dynamic-datasource Please check the setting of primary");
        }
    }
    
    
    /**
     * 获取数据源
     *
     * @param ds 数据源名称
     * @return 数据源
     */
    public DataSource getDataSource(String ds) {
        if (StringUtils.isEmpty(ds)) {
            // 没有ds注解, 则默认用 Primary
            return determinePrimaryDataSource();
        } else if (!groupDataSources.isEmpty() && groupDataSources.containsKey(ds)) {
            // 如果有组, 则用负载均衡的方式取一个
            log.debug("dynamic-datasource switch to the datasource named [{}]", ds);
            return groupDataSources.get(ds).determineDataSource();
        } else if (dataSourceMap.containsKey(ds)) {
            // 没有组, 则直接取
            log.debug("dynamic-datasource switch to the datasource named [{}]", ds);
            return dataSourceMap.get(ds);
        }
        // 严格模式下直接报错
        if (strict) {
            throw new CannotFindDataSourceException("dynamic-datasource could not find a datasource named" + ds);
        }
        return determinePrimaryDataSource();
    }
    
    // .....
}

它实现了InitializingBean接口,这个接口需要实现afterPropertiesSet()方法,这是一个Bean的生命周期函数,在Bean初始化的时候做一些操作

2.2.1 获取所有的DataSource

先看一下它是怎么获得数据源的? provider.loadDataSources() 是抽象方法, 有两个实现

YmlDynamicDataSourceProviderAbstractJdbcDataSourceProvider, 在DynamicDataSourceAutoConfiguration 类中, 注入的是YmlDynamicDataSourceProvider, 所以直接看它的实现 , 最后会跳转到AbstractDataSourceProvider

@Slf4j
public abstract class AbstractDataSourceProvider implements DynamicDataSourceProvider {

    @Autowired
    private DefaultDataSourceCreator defaultDataSourceCreator;

    protected Map<String, DataSource> createDataSourceMap(
            Map<String, DataSourceProperty> dataSourcePropertiesMap) {
        Map<String, DataSource> dataSourceMap = new HashMap<>(dataSourcePropertiesMap.size() * 2);
        for (Map.Entry<String, DataSourceProperty> item : dataSourcePropertiesMap.entrySet()) {
            DataSourceProperty dataSourceProperty = item.getValue();
            String poolName = dataSourceProperty.getPoolName();
            if (poolName == null || "".equals(poolName)) {
                poolName = item.getKey();
            }
            dataSourceProperty.setPoolName(poolName);
//            这里的defaultDataSourceCreator.createDataSource()方法使用到适配器模式。因为每种配置数据源创建的DataSource实现类都不一定相同的,所以需要根据配置的数据源类型进行具体的DataSource创建。
            dataSourceMap.put(poolName, defaultDataSourceCreator.createDataSource(dataSourceProperty));
        }
        return dataSourceMap;
    }
}

2.2.1.1 构建DataSource

来看一下defaultDataSourceCreator.createDataSource()的逻辑

    public DataSource createDataSource(DataSourceProperty dataSourceProperty) {
        DataSourceCreator dataSourceCreator = null;
		//this.creators是所有适配的DataSourceCreator实现类
        for (DataSourceCreator creator : this.creators) {
            //根据配置匹配对应的dataSourceCreator
            if (creator.support(dataSourceProperty)) {
                //如果匹配,则使用对应的dataSourceCreator
                dataSourceCreator = creator;
                break;
            }
        }
        if (dataSourceCreator == null) {
            throw new IllegalStateException("creator must not be null,please check the DataSourceCreator");
        }

        String publicKey = dataSourceProperty.getPublicKey();
        if (StringUtils.isEmpty(publicKey)) {
            dataSourceProperty.setPublicKey(properties.getPublicKey());
        }

        Boolean lazy = dataSourceProperty.getLazy();
        if (lazy == null) {
            dataSourceProperty.setLazy(properties.getLazy());
        }
		//然后再调用createDataSource方法进行创建对应DataSource
        DataSource dataSource = dataSourceCreator.createDataSource(dataSourceProperty);
        // (如果有则运行, 可以自定义)运行 建表语句和数据脚本
        this.runScrip(dataSource, dataSourceProperty);
        return wrapDataSource(dataSource, dataSourceProperty);
    }

创建DataSource的有很多实现类, 每种数据源都有自己的实现

对应的全部实现类是放在creator包下:

img

创建dataSource就很简单了, 就是把账号/密码/url等参数设置进去

2.2.2 对数据源分组

再回到之前的,当拿到DataSource的Map集合之后,再做什么呢?

接着调addDataSource()方法,这个方法是根据下划线"_"对数据源进行分组,最后放到groupDataSources成员变量里面。 com.baomidou.dynamic.datasource.DynamicRoutingDataSource#addDataSource

    private static final String UNDERLINE = "_";  
	// 轮询策略
    private Class<? extends DynamicDataSourceStrategy> strategy = LoadBalanceDynamicDataSourceStrategy.class;
/**
     * 新数据源添加到分组
     *
     * @param ds         新数据源的名字
     * @param dataSource 新数据源
     */
private void addGroupDataSource(String ds, DataSource dataSource) {
    if (ds.contains(UNDERLINE)) {
        String group = ds.split(UNDERLINE)[0];
        GroupDataSource groupDataSource = groupDataSources.get(group);
        if (groupDataSource == null) {
            try {
                //顺便设置负载均衡策略,strategy默认是LoadBalanceDynamicDataSourceStrategy
                groupDataSource = new GroupDataSource(group, strategy.getDeclaredConstructor().newInstance());
                groupDataSources.put(group, groupDataSource);
            } catch (Exception e) {
                throw new RuntimeException("dynamic-datasource - add the datasource named " + ds + " error", e);
            }
        }
        groupDataSource.addDatasource(ds, dataSource);
    }
}

至此, 项目启动时, 多数据做的事就完了, 那我们执行sql时, 它是怎么切换的呢?

2.3 切换数据源

在前面的DynamicRoutingDataSource我们知道, 这是mybatisPlus写的获取数据源的类 , 它最终是通过com.baomidou.dynamic.datasource.DynamicRoutingDataSource#determineDataSource获得数据源的, 在 2.2.1 也有描述.

   @Override
    public DataSource determineDataSource() {
        // 从threadLocal中获取当前数据源key (就是@DS注解里的字符串)
        String dsKey = DynamicDataSourceContextHolder.peek();
        // 根据key获取数据源
        return getDataSource(dsKey);
    }

那么上面的DynamicDataSourceContextHolder这个类是干嘛的呢?注解@DS的值又是怎么传进来的呢?

我们先看看这个类

2.3.1 DynamicDataSourceContextHolder

/**
 * 核心基于ThreadLocal的切换数据源工具类
 */
public final class DynamicDataSourceContextHolder {

    /**
     * 为什么要用链表存储(准确的是栈)
     * <pre>
     * 为了支持嵌套切换,如ABC三个service都是不同的数据源
     * 其中A的某个业务要调B的方法,B的方法需要调用C的方法。一级一级调用切换,形成了链。
     * 传统的只设置当前线程的方式不能满足此业务需求,必须使用栈,后进先出。
     * </pre>
     */
    private static final ThreadLocal<Deque<String>> LOOKUP_KEY_HOLDER = new NamedThreadLocal<Deque<String>>("dynamic-datasource") {
        @Override
        protected Deque<String> initialValue() {
            return new ArrayDeque<>();
        }
    };
    
    /**
     * 获得当前线程数据源
     *
     * @return 数据源名称
     */
    public static String peek() {
        return LOOKUP_KEY_HOLDER.get().peek();
    }

    /**
     * 设置当前线程数据源
     * <p>
     * 如非必要不要手动调用,调用后确保最终清除
     * </p>
     *
     * @param ds 数据源名称
     */
    public static String push(String ds) {
        String dataSourceStr = StringUtils.isEmpty(ds) ? "" : ds;
        LOOKUP_KEY_HOLDER.get().push(dataSourceStr);
        return dataSourceStr;
    }

    // ....
}

这里用的是NamedThreadLocal, 比ThreadLocal多了一个名字字段而已, 大家知道ThreadLocal 的特性: 线程隔离/内存泄露风险, 所以 当开多线程会让多数据源切换失败(看你@DS注解打在哪和具体使用); 网上说用到 DynamicDataSourceContextHolder类时, 都是push()clean()/poll()一起使用.其实就是避免内存泄露

既然有peek(), 那肯定有地方执行push(), 我们来反推一下, 看一下哪些地方在调用push(), 可以发现有两个地方, 分别是

DynamicDataSourceAnnotationInterceptorMasterSlaveAutoRoutingPlugin,

MasterSlaveAutoRoutingPlugin是做主从数据库切换的, 我们自然看 DynamicDataSourceAnnotationInterceptor

2.3.2 DynamicDataSourceAnnotationInterceptor

public class DynamicDataSourceAnnotationInterceptor implements MethodInterceptor {

    /**
     * The identification of SPEL.
     */
    private static final String DYNAMIC_PREFIX = "#";

    private final DataSourceClassResolver dataSourceClassResolver;
    private final DsProcessor dsProcessor;

    public DynamicDataSourceAnnotationInterceptor(Boolean allowedPublicOnly, DsProcessor dsProcessor) {
        dataSourceClassResolver = new DataSourceClassResolver(allowedPublicOnly);
        this.dsProcessor = dsProcessor;
    }

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        //找到@DS注解的属性值,也就是数据源名称
        String dsKey = determineDatasourceKey(invocation);
        //把数据源名称push到当前线程的栈
        DynamicDataSourceContextHolder.push(dsKey);
        try {
            //执行当前方法
            return invocation.proceed();
        } finally {
            //从栈里释放数据源
            DynamicDataSourceContextHolder.poll();
        }
    }
    
    private String determineDatasourceKey(MethodInvocation invocation) {
        // 从方法上和类上 获取key
        String key = dataSourceClassResolver.findDSKey(invocation.getMethod(), invocation.getThis());
        // 如果数据源是以 # 开头, 则进入ds处理器, 这里用的是责任链模式
        return (!key.isEmpty() && key.startsWith(DYNAMIC_PREFIX)) ? dsProcessor.determineDatasource(invocation, key) : key;
    }
}

它是invoke方法, 一看就是反射标准写法, 看一下它的父类, MethodInterceptor, 它可以对AOP的切面进行增强,

简单来说, 如果被切到后, 就会调用这个invoke方法

Spring动态代理之MethodInterceptor拦截器详解 - 简书 (jianshu.com)

我们继续看切面是如何使用的, 而ds处理器等会讲

2.3.4 DynamicDataSourceAnnotationAdvisor

直接看 DynamicDataSourceAnnotationInterceptor 在哪里用到了, 会发现回到了DynamicDataSourceAutoConfiguration

    @Role(value = BeanDefinition.ROLE_INFRASTRUCTURE)
    @Bean
    public Advisor dynamicDatasourceAnnotationAdvisor(DsProcessor dsProcessor) {
        // 创建拦截器
        DynamicDataSourceAnnotationInterceptor interceptor = new DynamicDataSourceAnnotationInterceptor(properties.isAllowedPublicOnly(), dsProcessor);
        // 把拦截器放到Advisor上
        DynamicDataSourceAnnotationAdvisor advisor = new DynamicDataSourceAnnotationAdvisor(interceptor);
        advisor.setOrder(properties.getOrder());
        return advisor;
    }

Advisor是Spring AOP的顶层抽象,用来管理Advice和Pointcut

@Transactional 也是用这种方式做的

从 AbstractPointcutAdvisor 开始: Spring AOP 之 Advisor、PointcutAdvisor 介绍

DynamicDataSourceAnnotationAdvisor是用于AOP切面编程的,针对注解@DS的切面进行处理:

public class DynamicDataSourceAnnotationAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware {

    private final Advice advice;

    private final Pointcut pointcut;

    // 初始化时调用了构造方法
    public DynamicDataSourceAnnotationAdvisor(@NonNull DynamicDataSourceAnnotationInterceptor dynamicDataSourceAnnotationInterceptor) {
        this.advice = dynamicDataSourceAnnotationInterceptor;
        this.pointcut = buildPointcut();
    }

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

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


    private Pointcut buildPointcut() {
        Pointcut cpc = new AnnotationMatchingPointcut(DS.class, true);
        Pointcut mpc = new AnnotationMethodPoint(DS.class);
        //方法优于类
        return new ComposablePointcut(cpc).union(mpc);
    }
 // .....   
}

所以总的来说, 当执行的方法或者类被 @DS切到时, 就会执行增强方法, 把数据源key放到threadLocal中, 获取数据源时从threadLocal中拿到key进而拿到数据源

至此, 多数据源的初始化和使用时切换就结束了

2.4 数据源的处理器

com.baomidou.dynamic.datasource.aop.DynamicDataSourceAnnotationInterceptor#determineDatasourceKey处, 有说过ds处理器, 它是干什么的呢?

我们常用的指定数据源的方式时是 @DS("master") @DS("salve_1"), 这些都是固定字符串, 如果我们需要根据租户来切换数据源呢?或者引入灰度系统后按Header的属性来切换呢? 因此plus的提供了各种数据源的处理器

  1. DsHeaderProcessor : 从header中获取
  2. DsSessionProcessor: 从session中获取
  3. DsSpelExpressionProcessor: 解析Spel语法获取

常见的@vaule()注解就支持spel语法, 从配置文件中取值或者写默认值都是spel语法

玩转Spring中强大的spel表达式! - 知乎 (zhihu.com)

继续 com.baomidou.dynamic.datasource.aop.DynamicDataSourceAnnotationInterceptor#determineDatasourceKey, 看一下它的代码

  private String determineDatasourceKey(MethodInvocation invocation) {
        String key = dataSourceClassResolver.findDSKey(invocation.getMethod(), invocation.getThis());
      // dsProcessor.determineDatasource() 从处理器获取数据源
        return (!key.isEmpty() && key.startsWith(DYNAMIC_PREFIX)) ? dsProcessor.determineDatasource(invocation, key) : key;
    }

dsProcessor 反向定位这个变量, 会追踪到com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration#dsProcessor

 @Bean
    @ConditionalOnMissingBean
    public DsProcessor dsProcessor(BeanFactory beanFactory) {
        // 先header处理器
        DsHeaderProcessor headerProcessor = new DsHeaderProcessor();
        DsSessionProcessor sessionProcessor = new DsSessionProcessor();
        DsSpelExpressionProcessor spelExpressionProcessor = new DsSpelExpressionProcessor();
        spelExpressionProcessor.setBeanResolver(new BeanFactoryResolver(beanFactory));
        // 再session处理器
        headerProcessor.setNextProcessor(sessionProcessor);
        // 最后spel处理器
        sessionProcessor.setNextProcessor(spelExpressionProcessor);
        return headerProcessor;
    }

回到使用的地方, 进入方法

com.baomidou.dynamic.datasource.processor.DsProcessor

    /**
     * 决定数据源
     * <pre>
     *     调用底层doDetermineDatasource,
     *     如果返回的是null则继续执行下一个,否则直接返回
     * </pre>
     *
     * @param invocation 方法执行信息
     * @param key        DS注解里的内容
     * @return 数据源名称
     */
    public String determineDatasource(MethodInvocation invocation, String key) {
        // key的格式是否匹配
        if (matches(key)) {
            // 解析数据源key
            String datasource = doDetermineDatasource(invocation, key);
            if (datasource == null && nextProcessor != null) {
                // 下一个处理器
                return nextProcessor.determineDatasource(invocation, key);
            }
            return datasource;
        }
        if (nextProcessor != null) {
            return nextProcessor.determineDatasource(invocation, key);
        }
        return null;
    }

我们以DsHeaderProcessor 为例:

public class DsHeaderProcessor extends DsProcessor {

    /**
     * header prefix
     */
    private static final String HEADER_PREFIX = "#header";

    @Override
    public boolean matches(String key) {
        // 是否 以#header 开头
        return key.startsWith(HEADER_PREFIX);
    }

    @Override
    public String doDetermineDatasource(MethodInvocation invocation, String key) {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        // 从请求头中直接获取(很粗暴,竟然连分隔符都不用), 比如@DS("#headeruserId"), 这样就会从header中取出userId字段
        return request.getHeader(key.substring(8));// 刚好把 #header 这几个字符截断
    }
}

session处理器是以#session 开头

按顺序处理都不是后, 就会按DsSpelExpressionProcessor 处理

举一些例子:

@DS("#headerUserId") : 取header中的UserId字段作为数据源key

@DS("#sessionTentendId") : 取session中的TentendId字段作为数据源key

@DS("#person"): 把方法上的变量person的值作为数据源key (SPEL语法)

学习SPEL语法就会发现很多奇妙用法, 比如可与获取方法的参数;方法的返参类型; 类的名称等

3. Q&A

3.1 @Transactional 和 @DS 注解一起使用时, @DS失效的问题

这是因为 @Transactional 开启事务后, 就不会重新拿数据源,因为@DS也得通过切面去获取数据源, 这样就导致了@DS失效.

要解决的话, 就在要切换数据源的方法上也打上@DS, 或者多个数据源有修改操作可以都打上事务注解并改变传播机制,(但这其实是分布式事务的范畴, 这样操作不能保证事务了, plus也提供了@DSTransactional 来支持, 不够需要借助seata)

@Transactional跟@DS动态数据源注解冲突_林蜗牛snail的博客-CSDN博客

关于 @DSTransactional 以后再补充一下

3.2 低于V3.3.3版本无法切换数据源的问题

这是因为在V3.3.3版本之前, DruidDataSourceAutoConfigure会比DynamicDataSourceAutoConfiguration先注册, 导致数据库有读取druid的配置, 所以会出现 ,没有druid配置启动报错; 数据源无法切换等等原因, 官方在V3.3.3这个版本修复了, 但是这个版本有个其他的重点bug,官方不让下载, 所以github上找不到这个版本的描述, 只在官网有描述

分两步解决的,

  1. 去除 druid pom坐标
  2. DynamicDataSourceAutoConfiguration 上指定优于 DruidDataSourceAutoConfigure加载

解决办法就是排除DruidDataSourceAutoConfigure 类;

使用dynamic-datasource-spring-boot-starter做多数据源及源码分析_0x2015的博客-CSDN博客

Mybatis-Plus多数据源解析 - 掘金 (juejin.cn)

多数据源官网-版本记录

Mybatis plus的多数据源@DS切换为什么不起作用了,谁的锅,@Transactional

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/684894.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【LLMs 入门实战 】第一式:Vicuna 模型学习与实战

UC伯克利学者联手CMU、斯坦福等&#xff0c;再次推出一个全新模型70亿/130亿参数的Vicuna&#xff0c;俗称「小羊驼」&#xff0c;小羊驼号称能达到GPT-4的90%性能。 欢迎使用小羊驼&#x1f999;环境搭建权重下载下载 Vicuna Weight下载 LLAMA Weight构建真正的 working weigh…

InceptionNext实战:使用InceptionNext实现图像分类任务(一)

文章目录 摘要安装包安装timm安装 grad-cam 数据增强Cutout和MixupEMA项目结构计算mean和std生成数据集 摘要 论文翻译&#xff1a;https://wanghao.blog.csdn.net/article/details/131347001?spm1001.2014.3001.5502 官方源码&#xff1a;https://github.com/sail-sg/incept…

Spring Boot详细应用示例:构建一个简单的图书管理系统

文章目录 摘要正文1. 创建Spring Boot项目2. 配置数据库3. 创建实体类4. 创建数据访问层5. 创建业务逻辑层6. 创建控制器层7. 创建前端页面8. 添加身份验证9. 测试运行 小结 摘要 本文将介绍如何使用Spring Boot框架构建一个简单而完整的图书管理系统。通过这个示例&#xff0…

学术小技巧:如何使用easyscholar来提高论文查找效率

0 摘要 easyScholar是一款很好用的科研插件&#xff0c;可以显示会议期刊登记&#xff0c;支持轻量翻译&#xff0c;一键下载等等功能。 1 效果预览 可以直接显示SCI分区&#xff0c;目前中科院SCI分区基础版是免费的&#xff0c;升级版要收费。 目前支持的网站有&#xff1…

【C++】红黑树的插入实现

目录 红黑树的概念红黑树的性质 红黑树节点的定义红黑树的插入操作当p(父节点)在g(祖父节点)左子树grandfather->_left parent当p(父节点)在g(祖父节点)右子树grandfather->_right parent &#x1f4d6; 前言 本篇文章中红黑树的插入用到左单旋和右单旋在AVL树的插入中…

【Python】python进阶篇之面向对象编程

面向对象编程 封装、继承、多态 封装&#xff1a;提高程序安全性 将数据&#xff08;属性&#xff09;和行为&#xff08;方法&#xff09;包装到类中。在方法内部对属性进行操作&#xff0c;在类的外部调用方法。无需关心方法内部的具体实现细节&#xff0c;从而隔离代码复杂…

生成器模式(Builder)

定义 生成器是一种创建型设计模式&#xff0c;使你能够分步骤创建复杂对象。该模式允许你使用相同的创建 代码生成不同类型和形式的对象。 前言 1. 问题 假设有这样一个复杂对象&#xff0c;在对其进行构造时需要对诸多成员变量和嵌套对象进行繁复的初始化工作。这些初始化…

体验Vue神奇的响应式原理:让你的应用更快、更流畅

文章目录 I. 引言介绍Vue.js的响应式原理及其重要性概述本文的内容 II. 数据劫持解释什么是数据劫持Vue如何实现数据劫持示例说明 II. 依赖收集解释什么是依赖收集Vue如何实现依赖收集示例说明 IV. 派发更新解释什么是派发更新Vue如何实现派发更新示例说明 V. 响应式原理运作流…

数据仓库建设指导说明

文章目录 1、概念2、数仓特点3、数仓架构3.1、数据集市3.2、Inmon 架构3.3、Kimball 架构3.3.1、表分区3.3.1.1、事实表3.3.1.2、维度表3.3.1.2.1、维表设计步骤3.3.1.2.2、维度设计的建议3.3.1.2.3、主键设计3.3.1.2.4、缓慢变化维 SCD3.3.1.2.5、维表的整合与拆分3.3.1.2.5.1…

Verdi 之配置及波形打开

目录 写在前边 1.verdi的配置 2. 波形的产生及打开 写在前边 本部分内容主要对Verdi的学习进行总结&#xff0c;大概分三篇文章进行叙述。 1.verdi的配置 1.首先打开.bashrc文件进行环境配置 2.Verdi 配置如下&#xff1a; verdi_HOME: 配置Verdi的home目录&#xff0…

如何制作数字人的模型

首先我们先来了解一下什么是数字人&#xff0c;根据 中国人工智能产业发展联盟发布的《2020年虚拟数字人发展白皮书》指出&#xff0c;数字人意 指具有数字化外形的虚拟人物&#xff0c;除了拥有人的外观、人的行为之外&#xff0c;还拥有人的思想&#xff0c;具有识别外界环境…

【深入浅出RocketMQ原理及实战】「底层原理挖掘系列」透彻剖析贯穿RocketMQ的消息顺序消费和并发消费机制体系的原理分析

透彻剖析贯穿RocketMQ的消息顺序消费和并发消费机制体系的原理分析 DefaultMQPushConsumerImpl拉取消息consumeMessageService的并发消费和顺序消费并发消费顺序消费concurrently 创建 ConsumeRequestconcurrently ConsumeRequest#run 消费主体逻辑消费结束之后清除数据 orderl…

黑马程序员前端 Vue3 小兔鲜电商项目——(十)订单页

文章目录 路由配置和基础数据渲染模板代码配置路由封装接口渲染数据 切换地址-打开弹框交互切换地址-地址切换交互生成订单支付页组件封装订单接口绑定事件 路由配置和基础数据渲染 模板代码 新建 src\views\Checkout\index.vue 文件&#xff0c;添加以下代码&#xff1a; &…

容器管理中关于CGroup的那些事

前言 在一个docker宿主机上可以启动多个容器&#xff0c;默认情况下&#xff0c;docker并没有限制其中运行的容器使用硬件资源。 但如果在实际环境中&#xff0c;容器的负载过高&#xff0c;会占用宿主机大量的资源。这里的资源主要指的CPU&#xff0c;内存&#xff0c;和IO带…

Python Pandas 筛选数据以及字符串替换

str.replace使用示例 假设有一个DataFrame df&#xff0c;其中有一个列名为text&#xff0c;包含一些文本字符串&#xff1a; import pandas as pd data {text: [hello world, foo bar, hello there]} df pd.DataFrame(data) 我们可以使用str.replace方法来替换字符串。比…

操作系统——Linux 进程控制

一、实验题目 Linux 进程控制 二、实验目的 通过进程的创建、撤销和运行加深对进程概念和进程并发执行的理解&#xff0c;明确进程和程序之间的区别。 三、实验内容&#xff08;实验原理/运用的理论知识、算法/程序流程图、步骤和方法、关键代码&#xff09; &#xff08;…

开源网安S-SDLC解决方案,为银行打造主动防御的安全体系

​某银行是全国上市最早的一批股份制商业银行&#xff0c;总部位于深圳&#xff0c;在全国拥有上百家分行、上千家营业机构&#xff0c;资产总额达数千亿元。近年来&#xff0c;该银行围绕数据化、智能化、生态化&#xff0c;全力打造“数字银行”&#xff0c;助力建设“数字中…

第十六届CISCN复现----MISC

1.被加密的生产流量 下载附件&#xff0c;发现是一个文件名为modus的压缩包&#xff0c;解压后是一个pcap文件&#xff0c;用wireshark打开 文件名modus&#xff0c;已经提示了工控流量&#xff0c;很多情况下都是和TCP协议结合起来的 工控CTF之协议分析1——Modbus_ctf modb…

基于java+swing+mysql学生信息管理系统V2.0

基于javaswingmysql学生信息管理系统V2.0 一、系统介绍二、功能展示1.项目骨架2.数据库表3.项目内容4.登陆5.学生信息查询6、学生信息添加7、学生信息修改8、学生信息删除 四、其它1.其他系统实现五.获取源码 一、系统介绍 项目类型&#xff1a;Java SE项目&#xff08;awtswi…

Gorm Many To Many

写cmdb的时候要去做一些软件资源的落库&#xff0c;发布要使用到的应用属性。应用有哪些属性&#xff1f; 应用有它的type类型&#xff0c;是api还是admin&#xff0c;还是job或者task。它的语言是go java.....&#xff0c;它的own也就是属于哪个开发的&#xff0c;这是它的属…