博客摘录「 基于dynamic-datasource实现多租户动态切换数据源」2024年10月15日

需求:根据数据库里面配置来动态切换数据源。

下面开始具体实现:

1.创建实体类,这个实体从数据库获取值,这里就省略了。

每个租户的数据库地址,code是租户编码

@Data
public class TenantDbSource {
    private String id;
    private String url;
    private String username;
    private String password;
    private String driverClassName;
    private String code;
}

2.加入依赖

<dependencies>
        <!--        lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!--        mybatis-plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>
        <!-- 动态切换数据源 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
            <!--        <version>3.5.1</version>-->
            <version>${dynamic.datasource}</version>
        </dependency>
        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

3.yaml配置

两种配置

下面这种没有用数据库连接池

spring:
  datasource:
    dynamic:
      primary: master #设置默认的数据源或者数据源组,默认值即为master
      strict: false #设置严格模式,默认false不启动. 启动后在未匹配到指定数据源时候回抛出异常,不启动会使用默认数据源.
      datasource:
        # 主库数据源
        master:
          url: 
          username:
          password: 
          driver-class-name: com.mysql.cj.jdbc.Driver

下面这种用数据库连接池

spring:
  autoconfigure:
    exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
  datasource:
    dynamic:
      primary: master #设置默认的数据源或者数据源组,默认值即为master
      strict: false #设置严格模式,默认false不启动. 启动后在未匹配到指定数据源时候回抛出异常,不启动会使用默认数据源.
      datasource:
        # 主库数据源
        master:
          url: 
          username: 
          password: 
          driver-class-name: com.mysql.cj.jdbc.Driver
      druid:
        # 初始连接数
        initialSize: 5
        # 最小连接池数量
        minIdle: 10
        # 最大连接池数量
        maxActive: 20
        # 配置获取连接等待超时的时间
        maxWait: 60000
        # 配置连接超时时间
        connectTimeout: 30000
        # 配置网络超时时间
        socketTimeout: 60000
        # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
        timeBetweenEvictionRunsMillis: 60000
        # 配置一个连接在池中最小生存的时间,单位是毫秒
        minEvictableIdleTimeMillis: 300000
        # 配置一个连接在池中最大生存的时间,单位是毫秒
        maxEvictableIdleTimeMillis: 900000
        # 配置检测连接是否有效
        validationQuery: SELECT 1 FROM DUAL
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        webStatFilter:
          enabled: true
        statViewServlet:
          enabled: true
          # 设置白名单,不填则允许所有访问
          allow:
          url-pattern: /druid/*
          # 控制台管理用户名和密码
          login-username: ruoyi
          login-password: 123456
        filter:
          stat:
            enabled: true
            # 慢SQL记录
            log-slow-sql: true
            slow-sql-millis: 1000
            merge-sql: true
          wall:
            config:
              multi-statement-allow: true

4.从基础数据库读取其他租户的数据库地址,并放入DynamicRoutingDataSource中,提供初始化、删除、新增、切换数据源等方法

@Slf4j
@Service
public class InjectionAllDatasourceService {

    @Autowired
    private DataSource dataSource;

    @Resource
    private DefaultDataSourceCreator dataSourceCreator;

    @Autowired
    private ITenantDbSourceService databaseService;

    @PostConstruct
    private void init(){
        log.info("项目启动中,加载用户数据");
        List<TenantDbSource> sysDatabases = databaseService.list();
        for (TenantDbSource database:sysDatabases){
            try {
                DataSourceProperty dataSourceProperty = new DataSourceProperty();
                // 这里主要是将dto的属性赋值给dataSourceProperty
                //所以dataSourceProperty中必要的参数,dto都要提供
                BeanUtils.copyProperties(database, dataSourceProperty);
                DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
                DataSource dataSource = dataSourceCreator.createDataSource(dataSourceProperty);
                // code就是我们yaml配置中说的数据源名称
                ds.addDataSource(database.getCode(), dataSource);
            } catch (Exception e){
                e.printStackTrace();
            }

        }

        log.info("项目启动中,加载用户数据完成");
    }

    /**
     * 所有的数据源
     * @return
     */
    public Set<String> allDataSource() {
        DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
        return ds.getDataSources().keySet();
    }


    /**
     * 新增数据源
     */
    public Set<String> add(@Validated TenantDbSource dto) {
        DataSourceProperty dataSourceProperty = new DataSourceProperty();
        // 这里主要是将dto的属性赋值给dataSourceProperty
        //所以dataSourceProperty中必要的参数,dto都要提供
        BeanUtils.copyProperties(dto, dataSourceProperty);
        DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
        DataSource dataSource = dataSourceCreator.createDataSource(dataSourceProperty);
        // code就是我们yaml配置中说的数据源名称
        ds.addDataSource(dto.getCode(), dataSource);
        return ds.getDataSources().keySet();
    }

    /**
     * 删除数据源
     * @param code
     * @return
     */
    public String remove(String code) {
        DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
        ds.removeDataSource(code);
        return "删除成功";
    }

    /**
     * 切换数据源
     * @param code
     */
    public static void switchDataSource(String code) {
        //如果当前的请求的数据源和当前数据源一致,则不切换
        String peek = DynamicDataSourceContextHolder.peek();
        if (code.equals(DynamicDataSourceContextHolder.peek())) {
            return;
        }
        //清空当前线程的数据源信息。
        DynamicDataSourceContextHolder.clear();
        //切换到对应poolName的数据源
        DynamicDataSourceContextHolder.push(code);
    }


    /**
     * 当前数据源code
     * @return
     */
//    public static String currentDataSource() {
//        //获取当前数据源code
//        return DynamicDataSourceContextHolder.peek();
//    }
}

5.自定义注解

@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Tenant {
}

6.aop

@Aspect
@Component
public class TenantAspect {
    @Pointcut("@annotation(com.cmct.common.annotation.Tenant)")
    public void tenantPointcut() {
    }

    @Around("tenantPointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        // 在目标方法执行前执行的逻辑
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
        //租户编码
        String tenantCode = null;
        //请求头
        tenantCode = requestAttributes.getRequest().getHeader("tenantCode");
        if(StringUtils.isBlank(tenantCode)){
            //请求参数
            tenantCode = requestAttributes.getRequest().getParameter("tenantCode");
            if(StringUtils.isBlank(tenantCode)){
                String name = joinPoint.getSignature().getName();
                throw new Exception(String.format("[%s]租户编码异常",name));
            }
        }
        //切换数据源
        InjectionAllDatasourceService.switchDataSource(tenantCode);
        // 执行目标方法
        Object result = joinPoint.proceed();

        // 在目标方法执行后执行的逻辑
        System.out.println("After method execution");

        // 返回方法执行结果
        return result;
    }
}

7.使用

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值