SpringBoot+AOP实现多数据源动态切换

SpringBoot+AOP实现多数据源动态切换

背景

系统后端需要访问多个数据库,现有的数据库连接配置写入配置文件中。后端需要从一个数据库的配置表里动态的读取其它mysql数据库的链接配置信息,并根据链接信息动态创建数据库链接,发起请求,而且还要能使用现在的一些连接池

设计总体思路

SpringBoot+AOP方式实现多数据源切换,继承AbstractRoutingDataSource实现数据源动态的获取,在service层使用注解指定数据源

步骤

  1. 对数据源库表进行设计,相关SQL语句如下所示:
    CREATE TABLE "YUDB"."DOM_DATABASE" (
    	"DB_ID" NUMBER NOT NULL ENABLE,
    	"SRC_ID" NUMBER,
    	"DB_NAME" VARCHAR2 ( 30 ),
    	"DB_C_NAME" VARCHAR2 ( 60 ),
    	"NOTE" VARCHAR2 ( 60 ),
    	"TAB_NUM" NUMBER,
    	"DB_SIZE" NUMBER,
    	"UD_TIME" DATE NOT NULL ENABLE,
    	"MD_FILE_ID" VARCHAR2 ( 32 ),
    	 CONSTRAINT "PK_DATABASE" PRIMARY KEY ( "DB_ID" ) USING INDEX PCTFREE 10 INITRANS 2 MAXTRANS 255 COMPUTE STATISTICS STORAGE ( INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645 PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT FLASH_CACHE DEFAULT CELL_FLASH_CACHE DEFAULT ) TABLESPACE "USERS" ENABLE,
    	 CONSTRAINT "SYS_C0011295" CHECK ( "DB_ID" IS NOT NULL ) ENABLE,
    	 CONSTRAINT "SYS_C0011296" CHECK ( "UD_TIME" IS NOT NULL ) ENABLE,
         CONSTRAINT "FK_DATABASE" FOREIGN KEY ( "SRC_ID" ) REFERENCES "YUDB"."DOM_DATASOURCE" ( "SRC_ID" ) ON DELETE CASCADE ENABLE 
    ) SEGMENT CREATION IMMEDIATE PCTFREE 10 PCTUSED 40 INITRANS 1 MAXTRANS 255 NOCOMPRESS LOGGING STORAGE ( INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645 PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT FLASH_CACHE DEFAULT CELL_FLASH_CACHE DEFAULT ) TABLESPACE "YU"
    
    CREATE TABLE "YUDB"."DOM_DATASOURCE" (
    	"SRC_ID" NUMBER NOT NULL ENABLE,
    	"CHG_ID" NUMBER,
    	"SRC_SID" VARCHAR2 ( 20 ),
    	"TABLE_SPACE" VARCHAR2 ( 20 ),
    	"IP_ADDR" VARCHAR2 ( 30 ),
    	"SRC_PORT" VARCHAR2 ( 10 ),
    	"SRC_USER" VARCHAR2 ( 20 ),
    	"SRC_PSW" VARCHAR2 ( 20 ),
    	"ORGID" NUMBER,
    	"DBTYPE" VARCHAR2 ( 100 ),
    	CONSTRAINT "PK_DATASOURCE" PRIMARY KEY ( "SRC_ID" ) USING INDEX PCTFREE 10 INITRANS 2 MAXTRANS 255 COMPUTE STATISTICS STORAGE ( INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645 PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT FLASH_CACHE DEFAULT CELL_FLASH_CACHE DEFAULT ) TABLESPACE "USERS" ENABLE,
    CONSTRAINT "SYS_C0011298" CHECK ( "SRC_ID" IS NOT NULL ) ENABLE 
    ) SEGMENT CREATION IMMEDIATE PCTFREE 10 PCTUSED 40 INITRANS 1 MAXTRANS 255 NOCOMPRESS LOGGING STORAGE ( INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645 PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT FLASH_CACHE DEFAULT CELL_FLASH_CACHE DEFAULT ) TABLESPACE "YU"
    
    DataBase库表设计
    DataBase库表设计
    DataSource库表设计:
    数据库表设计
    ER图如下图所示:
    ER图

    往yu数据库中的表dataSource里增加数据库的相关配置信息。

  2. 在配置文件application-dev.yml中,进行多数据源配置
    spring:
      devtools:
        restart:
          enabled: true  #设置开启热部署
          additional-paths: src/main/java #重启目录
          exclude: WEB-INF/**
      datasource:
        main:
          username: YUDB
          password: YUDB_HH
          url: jdbc:oracle:thin:@xxx.xxx.xxx.xxx:1521/RAC1
          driver-class-name: oracle.jdbc.driver.OracleDriver
        zyml:
          username: xxxxxx
          password: xxxxxx
          url: jdbc:oracle:thin:@xxx.xxx.xxx.xxx:1521/HHU
          driver-class-name: oracle.jdbc.driver.OracleDriver
        bjobj:
          username: xxxxxx
          password: xxxxxx
          url: jdbc:oracle:thin:@xxx.xxx.xxx.xxx:1521/BJOBJ
          driver-class-name: oracle.jdbc.driver.OracleDriver
        #####DruidDataSource配置#####################
        type: com.alibaba.druid.pool.DruidDataSource
        initialSize: 5
        minIdle: 5
        maxActive: 20
        # 配置获取连接等待超时的时间
        maxWait: 60000
        # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
        timeBetweenEvictionRunsMillis: 60000
        # 配置一个连接在池中最小生存的时间,单位是毫秒
        minEvictableIdleTimeMillis: 300000
        validationQuery: SELECT 1 FROM DUAL
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        # 打开PSCache,并且指定每个连接上PSCache的大小
        poolPreparedStatements: true
        maxPoolPreparedStatementPerConnectionSize: 20
        # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
        filters: stat,wall,log4j
        # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
        connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
        #合并多个DruidDataSource的监控数据
        useGlobalDataSourceStat: true
      ###############以上为配置druid添加的配置########################################
    
  3. 创建实体类DataSource和DataBase
    DataBase表和DataSource表通过外键src_id链接,先创建DataBase实体类
    package org.hhu.yu.system.rdbms.entity;
    
    import lombok.Data;
    
    import javax.persistence.*;
    import java.util.Date;
    
    @Data
    public class DataBase {
        
        private Long db_id;
    
        @JoinColumn(name="SRC_ID")
        @ManyToOne(cascade = CascadeType.ALL)
        //数据库名
        private String db_name;
        //数据库中文名
        private String db_c_nname;
        //数据库描述
        private String db_desc;
        //表数量
        private Long table_num;
        //数据库大小
        private Long data_size;
        //数据库更新时间
        private Date update_time;
        private String metaId;
    }
    
    然后创建DataSource实体类
    import io.swagger.annotations.ApiModel;
    import io.swagger.annotations.ApiModelProperty;
    import lombok.*;
    
    @ApiModel(description = "数据源对象Model")
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @RequiredArgsConstructor
    public class DataSource{
    
        @NonNull
        @ApiModelProperty(value = "数据源id",name ="src_id" )
        private Long src_id;
    
        @NonNull
        @ApiModelProperty(value= "数据源实例名",name="src_sid",required=true)
        private String src_sid;
        //数据源表空间
        private String table_space;
    
        @ApiModelProperty(value= "数据源ip地址",name="ip_addr",required=true)
        private String ip_addr;
    
        @ApiModelProperty(value= "数据源端口",name="src_port",required=true)
        private String src_port;
    
        @ApiModelProperty(value= "数据源用户名",name="src_user",required=true)
        private String src_user;
    
        @ApiModelProperty(value= "数据源密码",name="src_pwd",required=true)
        private String src_pwd;
    
        @ApiModelProperty(value= "数据库类型",name="db_type",required=true)
        private String db_type;
    
        @NonNull
        private DataBase dataBase;
    }
    
    
  4. 创建DruidDBConfig.java,配置Druid数据库连接池
@Configuration
public class DruidDBConfig {

    @Bean
    @Qualifier("mainDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.main")
    DataSource mainConfig() throws SQLException{
        DruidDataSource build = DruidDataSourceBuilder.create().build();
        List<Filter> filters = new ArrayList<>();
        filters.add(statFilter());
        filters.add(logFilter());
        build.setProxyFilters(filters);
        return build;
    }

    @Bean
    @Qualifier("zymlDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.zyml")
    DataSource zymlConfig(){
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @Qualifier("bjobjDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.bjobj")
    DataSource bjobjConfig(){
        return DruidDataSourceBuilder.create().build();
    }

    @Bean(name = "dynamicDataSource")
    @Primary  //优先使用,多数据源
    @Qualifier("dynamicDataSource")
    public DynamicDataSource dynamicDataSource() throws SQLException {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        dynamicDataSource.setDebug(false);
        // 默认数据源配置 DefaultTargetDataSource
        dynamicDataSource.setDefaultTargetDataSource(mainConfig());
        Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
        //多数据源配置 TargetDataSources
        targetDataSources.put("mainDataSource", mainConfig());
        targetDataSources.put("zymlDataSource", zymlConfig());
        targetDataSources.put("bjobjDataSource", mainConfig());
        dynamicDataSource.setTargetDataSources(targetDataSources);
        return dynamicDataSource;
    }
}
  1. 创建DBContextHolder类,实现数据源的切换服务

    @Slf4j
    public class DBContextHolder {
        // 对当前线程的操作-线程安全的
        private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
    
        // 调用此方法,切换数据源
        public static void setDataSource(String dataSource) {
            contextHolder.set(dataSource);
            log.info("已切换到数据源:{}",dataSource);
        }
    
        // 获取数据源
        public static String getDataSource() {
            return contextHolder.get();
        }
    
        // 删除数据源
        public static void clearDataSource() {
            contextHolder.remove();
            log.info("已切换到主数据源");
        }
    }
    
  2. 创建核心的动态数据源配置类,该类继承AbstractRoutingDataSource实现数据源动态的获取。

@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {

    private boolean debug = true;
    private Map<Object, Object> dynamicTargetDataSources;
    private Object dynamicDefaultTargetDataSource;

    @Override
    protected Object determineCurrentLookupKey() {
        String datasource = DBContextHolder.getDataSource();
        if (!StringUtils.isEmpty(datasource)) {
            Map<Object, Object> dynamicTargetDataSources2 = this.dynamicTargetDataSources;
            if (dynamicTargetDataSources2.containsKey(datasource)) {
                log.info("---当前数据源:" + datasource + "---");
            } else {
                log.info("不存在的数据源:");
                throw new ADIException("不存在的数据源:"+datasource,500);
                return null;               
                
            }
        } else {
            log.info("---当前数据源:默认数据源---");
        }
        return datasource;
    }

    @Override
    public void setTargetDataSources(Map<Object, Object> targetDataSources) {
        super.setTargetDataSources(targetDataSources);
        this.dynamicTargetDataSources = targetDataSources;
    }


    @Override
    public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        this.dynamicDefaultTargetDataSource = defaultTargetDataSource;
    }

    /**
     * @param debug
     * @description the debug to set
     */
    public void setDebug(boolean debug) {
        this.debug = debug;
    }

    /**
     * @return the debug
     */
    public boolean isDebug() {
        return debug;
    }

    /**
     * @return the dynamicTargetDataSources
     */
    public Map<Object, Object> getDynamicTargetDataSources() {
        return dynamicTargetDataSources;
    }

    /**
     * @param dynamicTargetDataSources
     *            the dynamicTargetDataSources to set
     */
    public void setDynamicTargetDataSources(Map<Object, Object> dynamicTargetDataSources) {
        this.dynamicTargetDataSources = dynamicTargetDataSources;
    }

    /**
     * @return the dynamicDefaultTargetDataSource
     */
    public Object getDynamicDefaultTargetDataSource() {
        return dynamicDefaultTargetDataSource;
    }

    /**
     * @param dynamicDefaultTargetDataSource
     * the dynamicDefaultTargetDataSource to set
     */
    public void setDynamicDefaultTargetDataSource(Object dynamicDefaultTargetDataSource) {
        this.dynamicDefaultTargetDataSource = dynamicDefaultTargetDataSource;
    }

    /**
     * @param dataSource
     * @throws Exception
     * @description 创建新数据源时检查数据源是否存在
     */
    public void createDataSourceWithCheck(DataSource dataSource) throws Exception {
        @NonNull Long src_id = dataSource.getSrc_id();
        log.info("正在检查数据源:"+src_id);
        Map<Object, Object> currentDynamicTargetDataSources = this.dynamicTargetDataSources;
        if (currentDynamicTargetDataSources.containsKey(src_id)) {
            log.info("数据源"+src_id+"之前已经创建,准备测试数据源是否正常...");
            DruidDataSource druidDataSource = (DruidDataSource) currentDynamicTargetDataSources.get(src_id);
            boolean rightFlag = true;
            Connection connection = null;
            try {
                log.info(src_id+"数据源的概况->当前闲置连接数:"+druidDataSource.getPoolingCount());
                long activeCount = druidDataSource.getActiveCount();
                log.info(src_id+"数据源的概况->当前活动连接数:"+activeCount);
                if(activeCount > 0) {
                    log.info(src_id+"数据源的概况->活跃连接堆栈信息:"+druidDataSource.getActiveConnectionStackTrace());
                }
                log.info("准备获取数据库连接...");
                connection = druidDataSource.getConnection();
                log.info("数据源"+src_id+"正常");
            } catch (Exception e) {
                log.error(e.getMessage(),e); //把异常信息打印到日志文件
                rightFlag = false;
                log.info("缓存数据源"+src_id+"已失效,准备删除...");
                if(delDatasources(src_id)) {
                    log.info("缓存数据源删除成功");
                } else {
                    log.info("缓存数据源删除失败");
                }
            } finally {
                if(null != connection) {
                    connection.close();
                }
            }
            if(rightFlag) {
                log.info("不需要重新创建数据源");
                return;
            } else {
                log.info("准备重新创建数据源...");
                createDataSource(dataSource);
                log.info("重新创建数据源完成");
            }
        } else {
            createDataSource(dataSource);
        }

    }


    private  void createDataSource(DataSource dataSource) throws Exception {
        DBUtils dbUtils = SpringUtils.getBean(DBUtils.class);
        @NonNull Long src_id = dataSource.getSrc_id();
        log.info("准备创建数据源"+src_id);
        String db_type = dataSource.getDb_type();
        String username = dataSource.getSrc_user();
        String password = dataSource.getSrc_pwd();
        String ip_addr = dataSource.getIp_addr();
        String src_port = dataSource.getSrc_port();
        @NonNull String src_sid = dataSource.getSrc_sid();
        String url = null;
        String driveClass = null;
        if("mysql".equalsIgnoreCase(db_type)) {
            driveClass = DBUtils.MYSQL_DRIVER;
            dbUtils.setMySQLYUrl(ip_addr,src_port,src_sid);
            url = dbUtils.getMySQLYUrl();
        } else if("oracle".equalsIgnoreCase(db_type)){
            driveClass = DBUtils.ORACLE_DRIVER;
            dbUtils.setOracleUrl(ip_addr,src_port,src_sid);
            url = dbUtils.getOracleUrl();
        } else if("dm".equalsIgnoreCase(db_type)){
            driveClass = DBUtils.DM_DRIVER;
            dbUtils.setDMUrl(ip_addr,src_port,src_sid);
            url = dbUtils.getDMUrl();
        } else if("sqlserver".equalsIgnoreCase(db_type)){
            driveClass = DBUtils.SQLSERVER_DRIVER;
            dbUtils.setSQLserverUrl(ip_addr,src_port,src_sid);
            url = dbUtils.getSQLserverUrl();
        }
        if(testDatasource(src_id.toString(),driveClass,url,username,password)) {
            boolean result = this.createDataSource(src_id.toString(), driveClass, url, username, password, db_type);
            if(!result) {
                log.error("数据源"+src_id+"配置正确,但是创建失败");
                throw new ADIException("数据源"+src_id+"配置正确,但是创建失败",500);
            }
        } else {
            log.error("数据源配置有错误");
            throw new ADIException("数据源配置有错误",500);
        }
    }

    /**
     * @description 自定义创建数据源
     * @param key
     * @param driveClass
     * @param url
     * @param username
     * @param password
     * @param db_type
     * @return
     */
    public boolean createDataSource(String key, String driveClass, String url, String username, String password, String db_type) {
        try {
            try { // 排除连接不上的错误
                Class.forName(driveClass);
                DriverManager.getConnection(url, username, password);//连接数据库
            } catch (Exception e) {
                return false;
            }
            @SuppressWarnings("resource")
//            HikariDataSource druidDataSource = new HikariDataSource();
            DruidDataSource druidDataSource = new DruidDataSource();
            druidDataSource.setName(key);
            druidDataSource.setDriverClassName(driveClass);
            druidDataSource.setUrl(url);
            druidDataSource.setUsername(username);
            druidDataSource.setPassword(password);
            druidDataSource.setInitialSize(1); //初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时
            druidDataSource.setMaxActive(20); //最大连接池数量
            druidDataSource.setMaxWait(60000); //获取连接时最大等待时间,单位毫秒。当链接数已经达到了最大链接数的时候,应用如果还要获取链接就会出现等待的现象,等待链接释放并回到链接池,如果等待的时间过长就应该踢掉这个等待,不然应用很可能出现雪崩现象
            druidDataSource.setMinIdle(5); //最小连接池数量
            String validationQuery = "select 1 from dual";
            if("mysql".equalsIgnoreCase(db_type)) {
                driveClass = DBUtils.MYSQL_DRIVER;
                validationQuery = "select 1";
            } else if("oracle".equalsIgnoreCase(db_type)){
                driveClass = DBUtils.ORACLE_DRIVER;
                druidDataSource.setPoolPreparedStatements(true); //是否缓存preparedStatement,PSCache对支持游标的ORACLE数据库性能提升巨大,在mysql下建议关闭。
                druidDataSource.setMaxPoolPreparedStatementPerConnectionSize(50);
                druidDataSource.setConnectionProperties("oracle.net.CONNECT_TIMEOUT=6000;oracle.jdbc.ReadTimeout=300000");//对于耗时长的查询sql,会受限于ReadTimeout的控制,单位毫秒
            } else if("dm".equalsIgnoreCase(db_type)){
                driveClass = DBUtils.DM_DRIVER;
                validationQuery = "select 1";
            } else if("sqlserver".equalsIgnoreCase(db_type)){
                driveClass = DBUtils.SQLSERVER_DRIVER;
                validationQuery = "select 1";
            }
            //申请连接时执行validationQuery检测连接是否有效,这里建议配置为TRUE,防止取到的连接不可用
            druidDataSource.setTestOnBorrow(true);
            //申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
            druidDataSource.setTestWhileIdle(true);
            //用来检测连接是否有效的sql,要求是一个查询语句。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
            druidDataSource.setValidationQuery(validationQuery);
            //属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有:监控统计用的filter:stat日志用的filter:log4j防御sql注入的filter:wall
            druidDataSource.setFilters("stat");
            //配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
            druidDataSource.setTimeBetweenEvictionRunsMillis(60000);
            //配置一个连接在池中最小生存的时间,单位是毫秒,这里配置为3分钟180000
            druidDataSource.setMinEvictableIdleTimeMillis(180000);
            //打开druid.keepAlive之后,当连接池空闲时,池中的minIdle数量以内的连接,空闲时间超过minEvictableIdleTimeMillis,
            // 则会执行keepAlive操作,即执行druid.validationQuery指定的查询SQL,一般为select * from dual,只要
            // minEvictableIdleTimeMillis设置的小于防火墙切断连接时间,就可以保证当连接空闲时自动做保活检测,不会被防火墙切断
            druidDataSource.setKeepAlive(true);
            //是否移除泄露的连接/超过时间限制是否回收。
            druidDataSource.setRemoveAbandoned(true);
            //泄露连接的定义时间(要超过最大事务的处理时间);单位为秒。这里配置为1小时
            druidDataSource.setRemoveAbandonedTimeout(3600);
            //移除泄露连接发生是是否记录日志
            druidDataSource.setLogAbandoned(true);
            druidDataSource.init();
            this.dynamicTargetDataSources.put(key, druidDataSource);
            // 将map赋值给父类的TargetDataSources
            setTargetDataSources(this.dynamicTargetDataSources);
            super.afterPropertiesSet();// 将TargetDataSources中的连接信息放入resolvedDataSources管理
            log.info(key+"数据源初始化成功");
            //log.info(key+"数据源的概况:"+druidDataSource.dump());
            return true;
        } catch (Exception e) {
            log.error(e + "");
            return false;
        }
    }

    /**
     * 删除数据源
     * @param db_id
     * @return
     */
    public boolean delDatasources(Long db_id) {
        Map<Object, Object> currentDynamicTargetDataSources = this.dynamicTargetDataSources;
        if (currentDynamicTargetDataSources.containsKey(db_id)) {
            Set<DruidDataSource> druidDataSourceInstances = DruidDataSourceStatManager.getDruidDataSourceInstances();
            for (DruidDataSource l : druidDataSourceInstances) {
                if (db_id.equals(l.getName())) {
                    currentDynamicTargetDataSources.remove(db_id);
                    DruidDataSourceStatManager.removeDataSource(l);
                    // 将map赋值给父类的TargetDataSources
                    setTargetDataSources(currentDynamicTargetDataSources);
                    // 将TargetDataSources中的连接信息放入resolvedDataSources管理
                    super.afterPropertiesSet();
                    return true;
                }
            }
            return false;
        } else {
            return false;
        }
    }

    /**
     * 测试数据源连接
     * @param driveClass
     * @param url
     * @param username
     * @param password
     * @return
     */
    public boolean testDatasource(String key,String driveClass, String url, String username, String password) {
        try {
            Class.forName(driveClass);
            DriverManager.getConnection(url, username, password);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值