SpringBoot动态数据源的使用

@TOCSpringBoot动态数据的使用

SpringBoot动态数据的使用

场景:系统运维工具,运维工具是运维工程师维护系统数据、功能正常的工具,常见的有:消息队列补偿工具、数据库同步失败稽查工具以及一些其他跟业务系统相关的工具。由于跟多个业务系统的打交道,在微服务的场景下,每一个微服务都有一个独立的数据库(地址及数据库名不同),这就需要运维工具在使用过程中能动态切换到正确的数据源上,对指定的微服务的数据库进行操作,当然使用Openfeign替代动态数据源也可以完成此类操作,具体可以平衡工作量及效率再做选择。

SpringBoot AbstractRoutingDataSource

AbstractRoutingDataSource是Spring Boot实现动态数据源的抽象类,通过继承AbstractRoutingDataSource类,重写determineCurrentLookupKey()和setTargetDataSources()方法,determineCurrentLookupKey方法用来决定当前使用哪一个数据源,其返回一个key,这个key从存储数据源的Map中找到对应的数据源,setTargetDataSources()用于设置备选数据源。

/** 
     * Retrieve the current target DataSource. Determines the 
     * {@link #determineCurrentLookupKey() current lookup key}, performs 
     * a lookup in the {@link #setTargetDataSources targetDataSources} map, 
     * falls back to the specified 
     * {@link #setDefaultTargetDataSource default target DataSource} if necessary. 
     * @see #determineCurrentLookupKey() 
     */  
    protected DataSource determineTargetDataSource() {  
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");  
        Object lookupKey = determineCurrentLookupKey();  
        DataSource dataSource = this.resolvedDataSources.get(lookupKey);  
        if (dataSource == null && (this.lenientFallback || lookupKey == null)) {  
            dataSource = this.resolvedDefaultDataSource;  
        }  
        if (dataSource == null) {  
            throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");  
        }  
        return dataSource;  
    }
	备选数据源采用map存储,键为生成数据源设定的键,与determineCurrentLookupKey返回的key对应,值则为数据源实例对象。
    public void setTargetDataSources(Map<Object, Object> targetDataSources) {
        this.targetDataSources = targetDataSources;
    }

使用示例

1、DbContextHolder 类,一个标识类,用来存储当前数据源的key及状态判断

public class DbContextHolder {

    private static final ThreadLocal contextHolder = new ThreadLocal<>();

    /**
     * 设置数据源
     *
     * @param dbTypeEnum
     */
    public static void setDbType(final String dbTypeEnum) {

        contextHolder.set(dbTypeEnum);
    }

    /**
     * 取得当前数据源
     *
     * @return
     */
    public static String getDbType() {

        return (String) contextHolder.get();
    }

    /**
     * 清除上下文数据,只是清除ThreadLocal,并不是真正清除数据源,后面判断当前是否存在数据源时,避免错误
     */
    public static void clearDbType() {

        contextHolder.remove();
    }
}

2、新建DynamicDataSource继承AbstractRoutingDataSource

public class DynamicDataSource extends AbstractRoutingDataSource {
    private final Logger log = LoggerFactory.getLogger(getClass());
	//用于本地存储key及数据源
    private  Map<Object, Object> dynamicTargetDataSourcesLocal;
	//数据库连接时加密用户名与密码
    @Value("${spring.datasource.privatekey}")
    private String privatekey;

	//从DbContextHolder与getDynamicTargetDataSourcesLocal中取出key做比较,只是校验的作用
    @Override
    protected Object determineCurrentLookupKey() {
        String baseKey = DbContextHolder.getDbType();
        if (!StringUtils.isEmpty(baseKey)) {
            Map<Object, Object> dynamicTargetDataSources2 = this.getDynamicTargetDataSourcesLocal();
            if (dynamicTargetDataSources2.containsKey(baseKey)) {
                log.info("---当前数据源:" + baseKey + "---");
            } else {
                log.info("不存在的数据源:");
                return null;
//                    throw new ADIException("不存在的数据源:"+datasource,500);
            }
        } else {
            log.info("---当前数据源:默认数据源---");
        }

        return baseKey;
    }
	
	//设置数据源的map集合
    @Override
    public void setTargetDataSources(Map<Object, Object> targetDataSources) {

        super.setTargetDataSources(targetDataSources);

    }


    // 创建数据源
    public synchronized boolean  createDataSource(String key, String driveClass, String url, String username, String password, String databasetype,String baseKey) {
        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";
            druidDataSource.setTestOnBorrow(true); //申请连接时执行validationQuery检测连接是否有效,这里建议配置为TRUE,防止取到的连接不可用
            druidDataSource.setTestWhileIdle(true);//建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
            druidDataSource.setValidationQuery(validationQuery); //用来检测连接是否有效的sql,要求是一个查询语句。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
            druidDataSource.setFilters("stat");//属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有:监控统计用的filter:stat日志用的filter:log4j防御sql注入的filter:wall
            druidDataSource.setTimeBetweenEvictionRunsMillis(60000); //配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
            druidDataSource.setMinEvictableIdleTimeMillis(180000); //配置一个连接在池中最小生存的时间,单位是毫秒,这里配置为3分钟180000
            druidDataSource.setKeepAlive(true); //打开druid.keepAlive之后,当连接池空闲时,池中的minIdle数量以内的连接,空闲时间超过minEvictableIdleTimeMillis,则会执行keepAlive操作,即执行druid.validationQuery指定的查询SQL,一般为select * from dual,只要minEvictableIdleTimeMillis设置的小于防火墙切断连接时间,就可以保证当连接空闲时自动做保活检测,不会被防火墙切断
            druidDataSource.setRemoveAbandoned(true); //是否移除泄露的连接/超过时间限制是否回收。
            druidDataSource.setRemoveAbandonedTimeout(3600); //泄露连接的定义时间(要超过最大事务的处理时间);单位为秒。这里配置为1小时
            druidDataSource.setLogAbandoned(true); //移除泄露连接发生是是否记录日志
            druidDataSource.init();
            //数据源创建成功后,会放到本地的Map集合中,设置DbContextHolder,调用setTargetDataSources,完成备选数据源的添加。
            Map<Object,Object> targetDataSources=this.getDynamicTargetDataSourcesLocal();
            targetDataSources.put(baseKey,druidDataSource);
            DbContextHolder.setDbType(baseKey);
            this.setTargetDataSources(targetDataSources);
            this.setDynamicTargetDataSourcesLocal(targetDataSources);
            super.afterPropertiesSet();// 将TargetDataSources中的连接信息放入resolvedDataSources管理
            log.info(key+"数据源初始化成功");
            //log.info(key+"数据源的概况:"+druidDataSource.dump());
            return true;
        } catch (Exception e) {
            log.error(e + "");
            return false;
        }
    }
    // 删除数据源
    public boolean delDatasources(String baseKey) {
        Map<Object, Object> dynamicTargetDataSources2 = this.getDynamicTargetDataSourcesLocal();
        if (dynamicTargetDataSources2.containsKey(baseKey)) {
            Set<DruidDataSource> druidDataSourceInstances = DruidDataSourceStatManager.getDruidDataSourceInstances();
            for (DruidDataSource l : druidDataSourceInstances) {
                if (baseKey.equals(l.getName())) {
                    dynamicTargetDataSources2.remove(baseKey);
                    DruidDataSourceStatManager.removeDataSource(l);
                    setTargetDataSources(dynamicTargetDataSources2);// 将map赋值给父类的TargetDataSources
                    super.afterPropertiesSet();// 将TargetDataSources中的连接信息放入resolvedDataSources管理
                    return true;
                }
            }
            return false;
        } else {
            return false;
        }
    }

    // 测试数据源连接是否有效
    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;
        }
    }

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

    }



    private  void createDataSource(DataBaseSource dataSource,String baseKey) throws Exception {
        String datasourceId = dataSource.getDataSourceId();
        log.info("准备创建数据源"+datasourceId);
        String databasetype = dataSource.getDatabasetype();
        String username = AESUtil.decryptString(dataSource.getUserName(),privatekey);
        String password = AESUtil.decryptString(dataSource.getPassWord(),privatekey);
        String url = dataSource.getUrl();
        if(testDatasource(datasourceId,dataSource.getCode(),url,username,password)) {
            boolean result = this.createDataSource(datasourceId, dataSource.getCode(), url, username, password, databasetype,baseKey);
            if(!result) {
                log.error("数据源"+datasourceId+"配置正确,但是创建失败");
//                throw new ADIException("数据源"+datasourceId+"配置正确,但是创建失败",500);
            }
        } else {
            log.error("数据源配置有错误");
//            throw new ADIException("数据源配置有错误",500);
        }
    }

    public Map<Object, Object> getDynamicTargetDataSourcesLocal() {
        return dynamicTargetDataSourcesLocal;
    }

    public void setDynamicTargetDataSourcesLocal(Map<Object, Object> dynamicTargetDataSourcesLocal) {
        this.dynamicTargetDataSourcesLocal = dynamicTargetDataSourcesLocal;
    }
}

3、定义创建数据源DataSourceCreateAspect和切换数据源的切面DataSourceSwitchAspect

@Component
@Aspect
@Order(-100) /** 这是为了保证AOP在事务注解之前生效,Order的值越小,优先级越高 **/
@Slf4j
@SuppressWarnings({ "unchecked", "unused", "PMD" })
public class DataSourceSwitchAspect {

    @Pointcut("execution(* com.chinamobile.cmss.cpms.mops.manage.service..*.*(..)) ||execution(* com.chinamobile.cmss.cpms.mops.common.db.service..*.*(..)) ||execution(* com.chinamobile.cmss.cpms.mops.tools.mopsOperationRequest.service..*.*(..)) ")
    private void dbMtoolsAspect() {

    }

    @Pointcut("execution(* com.chinamobile.cmss.cpms.mops.tools.personal.service..*.*(..)) || execution(* com.chinamobile.cmss.cpms.mops.tools.dataCheck.service..*.*(..))")
    private void dbSwitchAspect() {

    }
    /**
     * 默认数据源切换,在管理端都是用的cpms_mops数据源
     */
    @Before("dbMtoolsAspect()")
    public void dbMtools() {
        //        log.info("切换到cpms_mops数据源...");
        DbContextHolder.setDbType("MOPS");
    }

    @Before("dbSwitchAspect()")  //目标方法第一个参数是  String model,....
    private void dbSwitchByParam(JoinPoint joinPoint) {
        Object[] args=joinPoint.getArgs();
        if(args[0]!=null){
            if(args[1]!=null){
                DbContextHolder.setDbType(args[0].toString()+"_"+args[1].toString());//切换到指定modelCode_provinceCode数据库
            }
            DbContextHolder.setDbType(args[0].toString());//切换到指定modelCode数据库
        }else{
            DbContextHolder.setDbType("MOPS");//切换到默认数据库 cpms_mops
        }
    }

}

@Component
@Aspect
@Order(-150) /** 这是为了保证AOP在事务注解之前生效,Order的值越小,优先级越高 **/
@Slf4j
@SuppressWarnings({ "unchecked", "unused", "PMD" })
public class DataSourceCreateAspect {

    @Autowired
    private DBChangeService dbChangeService;

    @Autowired
    private DynamicDataSource dynamicDataSource;

    @Pointcut("execution(* com.chinamobile.cmss.cpms.mops.tools.personal.service..*.*(..)) || execution(* com.chinamobile.cmss.cpms.mops.tools.dataCheck.service..*.*(..))")
    private void dbCreateAspect() {

    }



    @Before("dbCreateAspect()")  //目标方法第一个参数是  String model,....
    private void dbSwitchByParam(JoinPoint joinPoint) {
        Object[] args=joinPoint.getArgs();
        String baseKey;
        if(args[0]!=null){
            Map<Object, Object> dynamicTargetDataSources2 = this.dynamicDataSource.getDynamicTargetDataSourcesLocal();
            if(args[1]!=null){
                baseKey=args[0].toString()+"_"+args[1].toString();//切换到指定modelCode_provinceCode数据库
            }
            baseKey=args[0].toString();//切换到指定modelCode数据库
            //当前数据源在本地存在,则不进行创建
            if(!dynamicTargetDataSources2.containsKey(baseKey)){
                try{
                    dbChangeService.changeDb(args[0].toString(),args[1]==null?null:args[1].toString());
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }

    }

}

4、此处数据源是放在数据库中的,所以获取相关数据源信息要访问数据库

@Service
public class DBChangeServiceImpl extends ServiceImpl<DataSourceMapper, DataBaseSource> implements DBChangeService {

    @Autowired
    DataSourceMapper dataSourceMapper;
    @Autowired
    private DynamicDataSource dynamicDataSource;
    @Override
    public DataBaseSource getOne(String moduleCode, String provinceCode) {
        return dataSourceMapper.selectDataSourceOne(moduleCode,provinceCode);
    }

    @Override
    public boolean changeDb(String moduleCode, String provinceCode) throws Exception {
        //默认切换到主数据源,进行整体资源的查找
        DbContextHolder.clearDbType();

        DataBaseSource dataBaseSource = dataSourceMapper.selectDataSourceOne(moduleCode,provinceCode);
        if(dataBaseSource ==null|| dataBaseSource.getDataSourceId()==null){
            dataBaseSource = dataSourceMapper.selectDataSourceOne(moduleCode,null);
            if(dataBaseSource ==null|| dataBaseSource.getDataSourceId()==null){
                throw new BusinessException(ExceptionEnum.DATA_BASE_CHANGE_EXCEPTION.getCode(),ExceptionEnum.DATA_BASE_CHANGE_EXCEPTION.getMessage()+"||"+moduleCode+"||"+provinceCode);
            }
        }

        if (dataBaseSource.getModuleCode().equals(moduleCode)) {
                System.out.println("需要使用的的数据源已经找到,datasourceId是:" + dataBaseSource.getDataSourceId());
                //创建数据源连接&检查 若存在则不需重新创建
                final String baseKey=provinceCode==null?moduleCode:moduleCode+"_"+provinceCode;
                dynamicDataSource.createDataSourceWithCheck(dataBaseSource,baseKey);
                return true;
            }

        return false;
    }
}

在这里插入图片描述
5、使用代码看一下
在这里插入图片描述
大致思路:
1、定义切面,切入点为所有service层代码,在controller层调用service层代码时,创建数据源切面DataSourceCreateAspect判断当前数据源是否存在;
2、不存在则调用changeDb从数据库中获取数据源连接信息,并创建数据源,创建数据源的过程中,设置了DbContextHolder,并且调用了setTargetDataSources设置了当前数据源Map集合;
3、程序访问数据库时,会调用determineCurrentLookupKey,返回新生成的key,最后拿当前的数据源对象,实现数据源的切换。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值