SpringBoot整合Mybatisplus实现完全动态获取多数据源

时间:2025-04-17 08:13:00

上篇我们分享了SpringBoot整合Mybatisplus 完成基本多数据源的配置,此篇我们从更高的层面去去分享 SpringBoot整合Mybatisplus实现完全动态获取多数据源,此方案适合更多的业务场景,比如每个用户一个数据源、每种类型以一个数据源、每种请求一个数据源等等,即此方案也就是常说的多租户、读写分离的业务场景等!下面开始切入正题:

1、核心pom 相关文件配置

   <dependency>
      <groupId></groupId>
      <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
      <version>3.5.1</version>
    </dependency>
    
      <dependency>
           <groupId></groupId>
           <artifactId>mybatis-plus-boot-starter</artifactId>
           <version>3.5.1</version>
       </dependency>

2、yml配置默认数据源:


spring:
  datasource:
    dynamic:
      primary: master# 配置默认数据库,及没有注解指定时走的数据库
      #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
      strict: false
      datasource:
        master: # 数据源1配置
          url: jdbc:mysql://localhost:3306/user_db1?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
          username: root
          password: root
          driver-class-name: 
      durid:
        initial-size: 1
        max-active: 20
        min-idle: 1
        max-wait: 60000
  autoconfigure:
    exclude:   
 

此数据源是默认的数据源,服务启动后会有一个 key 为master的主数据源;如果服务启动不设置默认数据源,启动时会报错,健康检查失败,错误提示核心是:

: dynamic-datasource can not find primary datasource
	at (:91) ~[dynamic-datasource-spring-boot-starter-3.5.:3.5.1]
	at (:120) ~[dynamic-datasource-spring-boot-starter-3.5.:3.5.1]
	at (:78) ~[dynamic-datasource-spring-boot-starter-3.5.:3.5.1]

 

 但是,服务最终是启动成功的,如果健康检查不拦截的话。

3、封装数据源处理类

 import ;
import ;
import ;
import ;
import ;
import ;
import ;
import .slf4j.Slf4j;
import ;
import ;
import ;

import ;
import ;
import ;
import ;
import ;
import ;

/**
* @Author nandao
* @Date 2021/12/01
**/
@Service
@Slf4j
public class CustomDataSourceService {

    @Resource
    private DataSource dataSource;
    @Resource
    private DefaultDataSourceCreator dataSourceCreator;

    @PostConstruct
    public void  init (){
        //可以初始化数据源在此
    }

    /**
     * 获取当前所有数据源的key
     */
    public Set<String> getAllDataSourceKey() {
        DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
        return ().keySet();
    }

    /**
     * 获取当前所有数据源url
     */
    public String userCurrentDataSource(String tenantId) {
        DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
        Set<String> urlList = ().keySet();
        if((tenantId)){
            DataSource dataSource = (tenantId);
            if((dataSource)){
                ItemDataSource druidDataSource = (ItemDataSource) dataSource;
                String name = ();
                
                return null;
            }
        }
         return null;
    }

    /**
     * 添加数据源
     */
    public Set<String> add(DataSourcePO dto) {
        DataSourceProperty dataSourceProperty = new DataSourceProperty();
        (dto, dataSourceProperty);
        DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
        DataSource dataSource = (dataSourceProperty);
        // 测试连接
        try {
            ();
        }catch (Exception e){
            throw new IllegalStateException("[" + (dto) + "]" + ":数据源连接不上可能是连接参数有误或库未创建!");
        }
        ((), dataSource);
        return ().keySet();
    }

    /**
     * 删除容器中数据源
     */
    @DeleteMapping
    public Boolean remove(String name) {
        DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
        (name);
        return ;
    }
}

4、业务切面处理:


import ;
import ;
import ;
import .slf4j.Slf4j;
import ;
import ;
import ;
import ;
import ;
import ;

import ;
import ;
import ;
import ;

/**
 * @Author nandao
 * @Date 2021/11/5
 **/
@Aspect
@Component
@Slf4j
@Order(-1)
public class DynamicDataSourceAspect {

    @Resource
    private CustomDataSourceService customDataSourceService ;

    
    @Resource
    private HttpServletRequest request;

    //@Pointcut("@annotation()")
    @Pointcut("execution(* .*.*(..))")
    public void dbMoreProduct() {
    }

    @Around("dbMoreProduct()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        // 获取当前租户
        String tenantId = ("tenant");
        
        // 获取当前所有的数据源
        Set<String> dateSourceKeyList = ();
         
        if (!dateSourceKeyList .contains(tenantId)) {
            // 获取租户对应数据库链接配置
            DataSourceVO dataSourceInfo = getTenantDateSource();
            // 添加数据源
            (dataSourceInfo);
           
        }
        // 手动指定数据源
        String push = (tenantId);
        ("租户:{},当前租户的数据源:{}",tenantId,push);
        try {
            return ();
        } finally {
            ();
        }
    }

    private DataSourceVO getTenantDateSource() {
        String tenantId =  ("tenant");
        DatabaseUrl databaseUrl = checkDataSource(tenantId);
 
        DataSourceVO dataSource = new DataSourceVO();
        (tenantId);
        (());
        (());
        (());
        return dataSource;
    }

    private DatabaseUrl checkDataSource(String tenantId) {
        //去数据库查询 返回databaseUrl,此处省略.....

        DatabaseUrl databaseUrl  ;
 
        return databaseUrl;
    }
}

5,注意事项:

            /**
             * 此处根据业务场景判断是否要加,放在finally中:尽量加,利大于弊!
             * 好处:此线程数据源使用后,清楚,避免内存泄露,同时避免其他线程拿到此数据源(垃圾回收之前),造成数据库串用,后果很严重,因为此处是栈的原理;
             * 弊端:如果扫描mapper文件,可能造成多次重置数据源,但是安全稳定起见,还是要清除;
             */
             ();

业务场景:是张三、李四分别操作A、B两个数据库,张三一直操作A,然后李四操作B,这期间李四很可能拿到A库的数据源执行!

到此大概业务逻辑分享清楚,用户和自己对应的数据源信息可以通过管理后台提前维护到数据库,用户首次登录获取对应的数据源存到数据源容器中,根据租户和线程调用相关的数据源;当然数据源也可以采用缓存预热的方案,服务启动时把所有用户的数据源加载到容器中,功能接口调用时就不用在去数据库查询了,进一步提高了性能!

注意:AOP切面可以用扫描自定义注解、包名等等,具体选择根据业务来判断哪种合适,所有技术方案脱离了业务就是耍流氓!!!下面我们详细分享多数据源的源码,敬请期待!