mybatis-plus多数据源切换失败

摘要:
提供对Mybatis-Plus,Quartz,ShardingJdbc,P6sy,Jndi等组件的集成方案。提供Mybatis环境下的纯读写分离方案。支持多层数据源嵌套切换。注解结果没有@DS默认数据源@DSdsName可以为组名也可以为具体某个库的名称@Service@DSpublicclassUserServiceImplimplementsUserService{@AutowiredprivateJdbcTemplatejdbcTemplate;publicListselectAll(){returnjdbcTemplate.queryForList;}@Override@DSpublicListselectByCondition(){returnjdbcTemplate.queryForList;}}二、源码调试开启动态数据源的debug日志。
一、正常使用流程

https://www.kancloud.cn/tracy5546/dynamic-datasource

特性

  • 支持 数据源分组 ,适用于多种场景 纯粹多库 读写分离 一主多从 混合模式。
  • 支持数据库敏感配置信息 加密 ENC()。
  • 支持每个数据库独立初始化表结构schema和数据库database。
  • 支持无数据源启动,支持懒加载数据源(需要的时候再创建连接)。
  • 支持 自定义注解 ,需继承DS(3.2.0+)。
  • 提供并简化对Druid,HikariCp,BeeCp,Dbcp2的快速集成。
  • 提供对Mybatis-Plus,Quartz,ShardingJdbc,P6sy,Jndi等组件的集成方案。
  • 提供 自定义数据源来源 方案(如全从数据库加载)。
  • 提供项目启动后 动态增加移除数据源 方案。
  • 提供Mybatis环境下的 纯读写分离 方案。
  • 提供使用 spel动态参数 解析数据源方案。内置spel,session,header,支持自定义。
  • 支持 多层数据源嵌套切换 。(ServiceA >>> ServiceB >>> ServiceC)。
  • 提供 **基于seata的分布式事务方案。
  • 提供 本地多数据源事务方案。

#约定

  1. 本框架只做 切换数据源 这件核心的事情,并不限制你的具体操作,切换了数据源可以做任何CRUD。
  2. 配置文件所有以下划线 _ 分割的数据源 首部 即为组的名称,相同组名称的数据源会放在一个组下。
  3. 切换数据源可以是组名,也可以是具体数据源名称。组名则切换时采用负载均衡算法切换。
  4. 默认的数据源名称为 master ,你可以通过 spring.datasource.dynamic.primary 修改。
  5. 方法上的注解优先于类上注解。
  6. DS支持继承抽象类上的DS,暂不支持继承接口上的DS。

使用

  1. 引入dynamic-datasource-spring-boot-starter。
<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
  <version>${version}</version>
</dependency>
  1. 配置数据源。
spring:
  datasource:
    dynamic:
      primary: master #设置默认的数据源或者数据源组,默认值即为master
      strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
      datasource:
        master:
          url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic
          username: root
          password: 123456
          driver-class-name: com.mysql.jdbc.Driver # 3.2.0开始支持SPI可省略此配置
        slave_1:
          url: jdbc:mysql://xx.xx.xx.xx:3307/dynamic
          username: root
          password: 123456
          driver-class-name: com.mysql.jdbc.Driver
        slave_2:
          url: ENC(xxxxx) # 内置加密,使用请查看详细文档
          username: ENC(xxxxx)
          password: ENC(xxxxx)
          driver-class-name: com.mysql.jdbc.Driver
       #......省略
       #以上会配置一个默认库master,一个组slave下有两个子库slave_1,slave_2
# 多主多从                      纯粹多库(记得设置primary)                   混合配置
spring:                               spring:                               spring:
  datasource:                           datasource:                           datasource:
    dynamic:                              dynamic:                              dynamic:
      datasource:                           datasource:                           datasource:
        master_1:                             mysql:                                master:
        master_2:                             oracle:                               slave_1:
        slave_1:                              sqlserver:                            slave_2:
        slave_2:                              postgresql:                           oracle_1:
        slave_3:                              h2:                                   oracle_2:
  1. 使用 @DS 切换数据源。

@DS 可以注解在方法上或类上,同时存在就近原则 方法上注解 优先于 类上注解

注解结果
没有@DS默认数据源
@DS("dsName")dsName可以为组名也可以为具体某个库的名称
@Service
@DS("slave")
public class UserServiceImpl implements UserService {

  @Autowired
  private JdbcTemplate jdbcTemplate;

  public List selectAll() {
    return  jdbcTemplate.queryForList("select * from user");
  }
  
  @Override
  @DS("slave_1")
  public List selectByCondition() {
    return  jdbcTemplate.queryForList("select * from user where age >10");
  }
}
二、源码调试
  1. 开启动态数据源的debug日志。
logging:
  level:
    com.baomidou.dynamic: debug

检查日志输出是否正确。

  1. 断点调试DynamicDataSourceAnnotationInterceptor。
public class DynamicDataSourceAnnotationInterceptor implements MethodInterceptor {

    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 {
        try {
            //这里把获取到的数据源标识如master存入本地线程
            String dsKey = determineDatasourceKey(invocation);
        ●  DynamicDataSourceContextHolder.push(dsKey);
            return invocation.proceed();
        } finally {
            DynamicDataSourceContextHolder.poll();
        }
    }

    private String determineDatasourceKey(MethodInvocation invocation) {
        String key = dataSourceClassResolver.findDSKey(invocation.getMethod(), invocation.getThis());
        return (!key.isEmpty() && key.startsWith(DYNAMIC_PREFIX)) ? dsProcessor.determineDatasource(invocation, key) : key;
    }
}
  1. 断点调试DynamicRoutingDataSource。
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {

    private Map<String, DataSource> dataSourceMap = new LinkedHashMap<>();

    private Map<String, DynamicGroupDataSource> groupDataSources = new ConcurrentHashMap<>();

    @Override
    public DataSource determineDataSource() {
        //从本地线程获取key解析最终真实的数据源
     ●  String dsKey = DynamicDataSourceContextHolder.peek();
        return getDataSource(dsKey);
    }

    private DataSource determinePrimaryDataSource() {
        log.debug("从默认数据源中返回数据");
        return groupDataSources.containsKey(primary) ? groupDataSources.get(primary).determineDataSource() : dataSourceMap.get(primary);
    }

    public DataSource getDataSource(String ds) {
        if (StringUtils.isEmpty(ds)) {
            return determinePrimaryDataSource();
        } else if (!groupDataSources.isEmpty() && groupDataSources.containsKey(ds)) {
            log.debug("从 {} 组数据源中返回数据源", ds);
            return groupDataSources.get(ds).determineDataSource();
        } else if (dataSourceMap.containsKey(ds)) {
            log.debug("从 {} 单数据源中返回数据源", ds);
            return dataSourceMap.get(ds);
        }
        return determinePrimaryDataSource();
    }
}
不可用版本

某些版本存在一些问题,强烈不建议使用。

强烈推荐使用最新版本。

以下版本不建议使用。

  • v3.3.3 严重BUG-Advisor启动失败导致无法切换数据源 。
  • v3.3.0 打包后启动强制依赖seata。
  • v3.1.0 使用NamedInheritableThreadLocal导致并发高切换数据源失败。
  • v2.3.3 嵌套切换数据源会导致会到主数据源。
  • v2.3.2 记不清具体BUG了,不用就对了。
  • v2.3.1 记不清具体BUG了,不用就对了。
  • v2.2.2 记不清具体BUG了,不用就对了。
三、@DS注解失效的情况

1.开启了spring的事务。

原因: spring开启事务后会维护一个ConnectionHolder,保证在整个事务下,都是用同一个数据库连接。

请检查整个调用链路涉及的类的方法和类本身还有继承的抽象类上是否有@Transactional注解。

2.方法内部调用。

查看以下示例 回答 外部调用 userservice.test1() 能在执行到 test2() 切换到second数据源吗?

public UserService {

    @DS("first")
    public void test1() {
        // do something
         test2();
    }

    @DS("second")
    public void test2() {
        // do something
    }
}

答案:!!!不能不能不能!!!! 数据源核心原理是基于aop代理实现切换,内部方法调用不会使用aop。

解决方法:

把test2()方法提到另外一个service,单独调用。

3.shiroAop代理。

在shiro框架中(UserRealm)使用@Autowire 注入的类, 缓存注解和事务注解都失效。

@Component
public class UserRealm extends AuthorizingRealm {

    @Lazy
    @Autowired
    private IUserService userService;
	//... 省略其他无关的内容
}

解决方法:
1.在Shiro框架中注入Bean时, 不使用@Autowire, 使用ApplicationContextRegister.getBean()方法,手动注入bean。

2.使用@Autowire+@Lazy注解,设置注入到Shiro框架的Bean延时加载(推荐)。

4.PostConstruct初始化顺序。

初始化包括: @PostConstruct 注解, InitializingBean接口, 自定义init-method

@Component
public class MyConfiguration {
    @Resource
    private UserMapper userMapper;
    @DS("slave")
    @PostConstruct
    public void init(){
        // 无法选择正确的数据源
        userMapper.selectById(1);
    }
}

解决方法:监听容器启动完成事件, 在容器完成后做初始化。

@Component
public class MyConfiguration {

    @DS("slave")
    @EventListener
    public void onApplicationEvent(ContextRefreshedEvent event) {
        // 成功选择正确的数据源
        userMapper.selectById(1);
    }
}

相关spring源码 : `org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#initializeBean

在初始化完成后, bean 进入增强阶段, 所以这个阶段的任何AOP都是无效的。

5.Druid版本过低。

请升级druid1.1.22及以上版本,这个版本修复了在高并发下偶发的切换失效问题。

6.@Async或者java8的ParallelStream并行流之类方法。

这种情况都是新开了线程去处理,不受当前线程管控了。 可以在新开的方法上加对应的DS注解解决。


扩展阅读

shiro失效原因

在spring初始化bean的阶段中,大致上分为三段: BeanFactoryPostProcessor -> BeanPostProcessor -> Bean

这三种都是单例bean. 只不过会按照这个优先级进行初始化, shiro引起AOP失效的原因:

ShiroFilterFactoryBean 是一个 BeanPostProcessor , 比普通的单例Bean都优先加载, 所以他依赖注入的bean都无法正确的进行切面。

ShiroFilterFactoryBean 实际的依赖情况:
ShiroFilterFactoryBean -> SecurityManager -> UserRealm -> IUserService

IUserService依赖的其他 service 也会失效
IUserService-> MenuService -> RoleService

免责声明:文章转载自《mybatis-plus多数据源切换失败》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇OPPO X9007 升级到Android5.0 Color2.1(root版) 详细纪实windows下复制文件报错“文件名对目标文件夹可能过长 。您可以缩短文件名并重试,或者......”下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

Spring Boot集成MyBatis的2种方式

目录 写在前面 准备工作 配置数据库驱动 配置数据源 原生集成MyBatis 依赖配置 注册MyBatis核心组件 定义并使用映射器 通过MyBatis-Spring-Boot-Starter集成 默认配置 高级定制 总结与比较 写在前面 最近总是有同事和技术群的朋友提问在Spring Boot中使用MyBatis时遇到的问题,大多...

【MyBatis】 MyBatis入门

1、MyBatis简介 MyBatis是这个框架现在的名字,而此框架最早的名字是IBatis,其名字的含义是“internet”、“abatis”两个单词的组合,是在2002年的时候开始的一个开源项目,在2010年6月16日Apache将项目交与Google进行管理,更名MyBatis。所以2010年之后,MyBatis框架逐渐成为主流,比Hiberna...

SpringBoot添加对Mybatis的支持

1、修改maven配置文件pom.xml,添加对mybatis的支持: <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-bo...

SpringBoot与动态多数据源切换

本文简单的介绍一下基于SpringBoot框架动态多数据源切换的实现,采用主从配置的方式,配置master、slave两个数据库。 一、配置主从数据库 spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driverClassName:...

Spring Boot(七):Mybatis 多数据源最简解决方案

说起多数据源,一般都来解决那些问题呢,主从模式或者业务比较复杂需要连接不同的分库来支持业务。我们遇到的情况是后者,网上找了很多,大都是根据 Jpa 来做多数据源解决方案,要不就是老的 Spring 多数据源解决方案,还有的是利用 Aop 动态切换,感觉有点小复杂,其实我只是想找一个简单的多数据支持而已,折腾了两个小时整理出来,供大家参考。 废话不多说直接上...

Mybatis笔记

这几天学习了Mybatis相关内容,在此整理一下这几天的笔记。 第一天 mybatis入门 mybatis的概述 mybatis的环境搭建 mybatis的入门案例 自定义mybatis框架 1 什么是框架 他是我们软件开发中的一套解决方案,不同的框架解决的是不同的问题。 使用框架的好处:框架封装了很多的细节,使开发者可以使用极简的方式实现功能。大大提...