springboot 实现动态切换数据源-多租户-数据库存储数据源-程序运行期间动态添加数据源-基于dynamic-datasource

文章目录

  • 1、目标
  • 2、源代码
  • 3、动态实时数据源
    • pom.xml配置
    • application.yml配置
    • 表结构
    • 动态操作数据源
  • 4、动态切换数据源
    • aop模式切换
    • 拦截器切换
    • 测试
  • 外传

1、目标

1、实现程序运行间动态切换数据源
2、实现数据库存储数据源
3、选择
dynamic-datasource集成了很多ORM的框架,其中,使用比较多的是druid,但有一些东西开始收费了
druid也可以自行配置,配置多了点

目前版本只支持单一位置加载数据源(只能从配置文件或者自定义加载数据源),不能多位置加载数据源
目前版本未实现动态添加数据源(在切换数据源时,不存在的数据源在数据库查询,添加进数据源连接池)

2、源代码

springboot版本:2.3.1.RELEASE
dynamic-datasource版本:3.5.1
官方文档:https://www.kancloud.cn/tracy5546/dynamic-datasource/2264611
实现源代码地址

3、动态实时数据源

pom.xml配置

	<dependency>
        <groupId>com.baomidougroupId>
        <artifactId>dynamic-datasource-spring-boot-starterartifactId>
        <version>3.5.1version>
    dependency>

application.yml配置


spring:
  application:
    name: microservice-boot-common
  # Jackson 配置项
  jackson:
    #    serialization:
    #      write-dates-as-timestamps: true # 设置 Date 的格式,使用时间戳
    #      write-date-timestamps-as-nanoseconds: false # 设置不使用 nanoseconds 的格式。例如说 1611460870.401,而是直接 1611460870401
    #      write-durations-as-timestamps: true # 设置 Duration 的格式,使用时间戳
    #      fail-on-empty-beans: false # 允许序列化无属性的 Bean
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8

  datasource:
    dynamic:
      primary: master #设置默认的数据源或者数据源组,默认值即为master
      strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
      datasource:
        druid: # Druid 【监控】相关的全局配置
          web-stat-filter:
            enabled: true
          stat-view-servlet:
            enabled: true
            allow: # 设置白名单,不填则允许所有访问
            url-pattern: /druid/*
            login-username: test
            login-password: test
          filter:
            stat:
              enabled: true
              log-slow-sql: true # 慢 SQL 记录
              slow-sql-millis: 1000
              merge-sql: true
            wall:
              config:
                multi-statement-allow: true
          initial-size: 4 # 初始连接数
          min-idle: 4 # 最小连接池数量
          max-active: 50 # 最大连接池数量
          max-wait: 600000 # 配置获取连接等待超时的时间,单位:毫秒
          time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒
          min-evictable-idle-time-millis: 300000 # 配置一个连接在池中最小生存的时间,单位:毫秒
          max-evictable-idle-time-millis: 900000 # 配置一个连接在池中最大生存的时间,单位:毫秒
          validation-query: SELECT 1 # 配置检测连接是否有效
          test-while-idle: true
          test-on-borrow: false
          test-on-return: false
          name: master
        master:
          driver-class-name: com.mysql.jdbc.Driver # 3.2.0开始支持SPI可省略此配置
          url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&allowPublicKeyRetrieval=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&allowMultiQueries=true&nullCatalogMeansCurrent=true
          username: root
          password: 123456
        slave_1:
          driver-class-name: com.mysql.jdbc.Driver
          url: jdbc:mysql://127.0.0.1:3306/test2?useUnicode=true&allowPublicKeyRetrieval=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&allowMultiQueries=true&nullCatalogMeansCurrent=true
          username: root
          password: 123456




# MyBatis Plus 的配置项
mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true # 虽然默认为 true ,但是还是显示去指定下。
  global-config:
    db-config:
      #      id-type: NONE # “智能”模式,基于 IdTypeEnvironmentPostProcessor + 数据源的类型,自动适配成 AUTO、INPUT 模式。
      id-type: AUTO # 自增 ID,适合 MySQL 等直接自增的数据库
      #      id-type: INPUT # 用户输入 ID,适合 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库
      #      id-type: ASSIGN_ID # 分配 ID,默认使用雪花算法。注意,Oracle、PostgreSQL、Kingbase、DB2、H2 数据库时,需要去除实体类上的 @KeySequence 注解
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
      logic-delete-field: deleted_tag
      table-prefix: t_
    banner: false
  type-aliases-package: org.lwd.microservice.boot.common.dao
  mapper-locations: classpath*:/mapper/**/*.xml

表结构

/**
 *租户数据源
 */
CREATE TABLE `t_tenant_data_source`
(
    `id`              int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '数据源id',
    `tenant_id`       int(11) unsigned NOT NULL COMMENT '租户id',
    `tenant_name`     varchar(100) DEFAULT NULL COMMENT '租户名',
    `microservice_db` tinyint(3) DEFAULT '1' COMMENT '微服务业务db 1:基础库 2:平台库 3:财务库...',
    `jdbc_type`       tinyint(1) DEFAULT '1' COMMENT 'db类型 1:mysql ',
    `jdbc_uri`        varchar(255) NOT NULL COMMENT '连接URL',
    `jdbc_username`   varchar(255) NOT NULL COMMENT '连接用户',
    `jdbc_password`   varchar(255) NOT NULL COMMENT '连接密码',
    `status`          tinyint(1) NOT NULL DEFAULT '1' COMMENT '类型状态 -1删除 0禁用 1启用 ',
    `deleted_tag`     tinyint(4) DEFAULT '0' COMMENT '删除标识 0:未删除 1:已删除',
    `create_by_id`    int(11) NOT NULL DEFAULT '0' COMMENT '创建人id',
    `create_time`     datetime     NOT NULL COMMENT '创建时间',
    `create_by_name`  varchar(40)  DEFAULT NULL COMMENT '创建人名',
    `update_by_id`    int(11) DEFAULT NULL COMMENT '更新人id',
    `update_time`     datetime     DEFAULT NULL COMMENT '更新人时间',
    `update_by_name`  varchar(40)  DEFAULT NULL COMMENT '更新人名',
    PRIMARY KEY (`id`),
    KEY               `idx_tenant_tenant_id` (`tenant_id`) COMMENT '租户id'
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='租户数据源';

动态操作数据源

package org.lwd.microservice.boot.common.aop;

import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.creator.DefaultDataSourceCreator;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty;
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import org.apache.dubbo.common.utils.CollectionUtils;
import org.lwd.microservice.boot.common.entity.dto.TenantDataSourceDTO;
import org.lwd.microservice.boot.common.service.TenantDataSourceService;
import org.lwd.microservice.boot.core.constant.DataSourceConstant;
import org.lwd.microservice.boot.core.entity.BaseResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.sql.DataSource;
import java.util.List;

/**
 * 加载数据库所有数据源
 *
 * @author weidong
 * @version V1.0.0
 * @since 2023/6/13
 */
@Component
public class DataSourceChangeProvider {

    @Autowired
    private DataSource dataSource;

    //微服务模式下,替换成TenantDataSourceDubboService或者其他
    @Autowired
    private TenantDataSourceService tenantDataSourceService;

    @Autowired
    @SuppressWarnings("all")
    private DefaultDataSourceCreator dataSourceCreator;

    /**
     * 增加数据源
     *
     * @param dto 租户数据源信息
     */
    public void addDataSource(TenantDataSourceDTO dto) {
        DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;

        DataSourceProperty dataSourceProperty = new DataSourceProperty();
        int tenantId = dto.getTenantId();
        String dsName = DataSourceConstant.TENANT_SOURCE_HEADER + tenantId;
        dataSourceProperty.setPoolName(dsName);
        dataSourceProperty.setDriverClassName("com.mysql.jdbc.Driver");
        dataSourceProperty.setUrl(dto.getJdbcUri() + "?useUnicode=true&allowPublicKeyRetrieval=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&allowMultiQueries=true&nullCatalogMeansCurrent=true");
        dataSourceProperty.setUsername(dto.getJdbcUsername());
        dataSourceProperty.setPassword(dto.getJdbcPassword());
        DataSource dataSource = dataSourceCreator.createDataSource(dataSourceProperty);
        ds.addDataSource(dsName, dataSource);
    }

    /**
     * 移除数据源
     *
     * @param tenantId 租户ID
     */
    public void delDataSource(Integer tenantId) {
        DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
        ds.removeDataSource(DataSourceConstant.TENANT_SOURCE_HEADER + tenantId);
    }

    /**
     * 切换数据源
     *
     * @param dsName 租户ID
     */
    public void changeDataSource(String dsName) {
        //清空当前线程数据源
        //如果当前线程是连续切换数据源 只会移除掉当前线程的数据源名称
        DynamicDataSourceContextHolder.poll();
        DynamicDataSourceContextHolder.push(dsName);
    }

    @PostConstruct
    public void initDataSourceAll() {
        BaseResult<List<TenantDataSourceDTO>> listBaseResult = tenantDataSourceService.getTenantDataSourceList();
        if (listBaseResult.isSuccess()) {
            List<TenantDataSourceDTO> tenantDataSourceDTOList = listBaseResult.getData();
            if (CollectionUtils.isNotEmpty(tenantDataSourceDTOList)) {
                //数据源操作,也可以加入不同的数据源类型
                for (TenantDataSourceDTO dto : tenantDataSourceDTOList) {
                    this.addDataSource(dto);
                }
            }
        }
    }
}

4、动态切换数据源

aop模式切换

package org.lwd.microservice.boot.common.aop;

import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;

/**
 * 数据源拦截-aop模式
 *
 * @author weidong
 * @version V1.0.0
 * @since 2023/6/13
 */
@Order(1)
@Aspect
@EnableAspectJAutoProxy(proxyTargetClass = true)
@Component
@Slf4j
public class DataSourceChangeAdvisor {
    /**
     * 按需设置需要切换的模式
     */
    @Pointcut("@within(org.springframework.web.bind.annotation.RestController)")
    public void pointDataSource() {
    }

    /**
     * 需要定义模块的规则
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around("pointDataSource()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            //获取当前请求对象
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            if (attributes != null) {
                HttpServletRequest request = attributes.getRequest();
                String uri = request.getRequestURI();
                //String url = request.getRequestURL().toString();
                String[] uriArr = uri.split("/");
                if (uriArr[uriArr.length - 1].equals("detail")) {
                    log.info("datasource1------");
                    DynamicDataSourceContextHolder.push("master");
                } else if (uriArr[uriArr.length - 1].equals("detail2")) {
                    log.info("datasource2------");
                    DynamicDataSourceContextHolder.push("slave_1");
                } else {
                    log.info("datasource dynamic 3------");
                    DynamicDataSourceContextHolder.push("tenant_1");
                }
            }
        } catch (Exception e) {
            log.error("日志拦截异常", e);
        } finally {
            return joinPoint.proceed();
        }
    }

}

拦截器切换

package org.lwd.microservice.boot.common.interceptor;

import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 动态数据源-拦截器模式-需要自定义规则
 * @author weidong
 * @version V1.0.0
 * @since 2023/6/16
 */
@Slf4j
public class DynamicDataSourceChangeInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String uri = request.getRequestURI();
        //String url = request.getRequestURL().toString();
        String[] uriArr = uri.split("/");
        if (uriArr[uriArr.length - 1].equals("detail")) {
            log.info("interceptor datasource1------");
            DynamicDataSourceContextHolder.push("master");
        } else if (uriArr[uriArr.length - 1].equals("detail2")) {
            log.info("interceptor datasource2------");
            DynamicDataSourceContextHolder.push("slave_1");
        } else {
            log.info("interceptor datasource dynamic 3------");
            DynamicDataSourceContextHolder.push("tenant_1");
        }
        return true;
    }
}

package org.lwd.microservice.boot.common.interceptor;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * mvc拦截器-自定义拦截路径
 * @author weidong
 * @version V1.0.0
 * @since 2023/6/16
 */
@Configuration
public class DynamicDataSourceChangeInterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //如果拦截全部可以设置为 /**
        String [] path = {"/tenantDataSource/**"};
        //不需要拦截的接口路径
        String [] excludePath = {};
        DynamicDataSourceChangeInterceptor dynamicDataSourceChangeInterceptor = new DynamicDataSourceChangeInterceptor();
        registry.addInterceptor(dynamicDataSourceChangeInterceptor).addPathPatterns(path).excludePathPatterns(excludePath);
    }
}


测试

TestDataSource.http

### 详情

GET http://localhost:8021/tenantDataSource/detail?pk = 1

### 详情2-动态切换

GET http://localhost:8021/tenantDataSource/detail2?pk = 1

### 详情3-加载数据库数据源

GET http://localhost:8021/tenantDataSource/detail3?pk = 1


外传

 原创不易,如若本文能够帮助到您的同学
 支持我:关注我+点赞+收藏⭐️
 留言:探讨问题,看到立马回复
 格言:己所不欲勿施于人 扬帆起航、游历人生、永不言弃!

你可能感兴趣的:(分布式微服务,数据库,spring,boot,数据库,java)