spring boot 动态配置数据源应用

**

实现目标

**
每个用户所使用的数据源连接不一样,这就需要系统能依据用户登录信息动态切换到不同的数据源,新用户使用时可能会增加新的数据源连接,这就需要系统支持动态增加数据源;
1、已有多数据源的初始化加载;
2、动态添加数据源;
3、无效数据源的移除;

**

前言

**
spring boot 提供了动态选择数据源的抽象类:AbstractRoutingDataSource ;使用它可以获取当前线程所持有的数据源;具体方式如下:

BlockChainDynamicDataSource.java

@Slf4j
public class BlockChainDynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
    	// 获取当前线程所持有的key值
        return BlockDataSourcePool.getLocalDataSourceKey();
    }
}

BlockDataSourcePool.java

/**
 * 数据源连接池
 * @author bobch
 */
@Slf4j
@Data
public class BlockDataSourcePool {

    /**
     * 记录当前线程池的数据源key 重点标记 ThreadLocal
     */
    private static final ThreadLocal<String> DATA_SOURCE_KEY = new ThreadLocal<>();
    public static void putDataSourceKey(String localKey){
        log.info("putDataSourceKey--->{}", localKey);
        DATA_SOURCE_KEY.set(localKey);
    }

    public static String getLocalDataSourceKey(){
        return DATA_SOURCE_KEY.get();
    }

    public static void clearDataSource(){
        DATA_SOURCE_KEY.remove();
    }
}

通过spring的切面编程,指定相应路径下的mapper使用特定的数据源配置:

@Slf4j
@Aspect
@Component
public class BlockChainDataSourceAspect {
    @Resource
    private HttpServletRequest httpServletRequest;
	/** 可切换数据源切面配置 */
    @Pointcut("execution(public * com.*.*.bdao..*(..))")
    private void changeDataSource() { }
    /** 主数据源切面配置 */
    @Pointcut("execution(public * com.*.*.dao..*(..))")
    private void masterDataSource() { }

    @Before("changeDataSource()")
    public void changeDataSource(JoinPoint point) throws Throwable {
        String localDataSourceKey = BlockDataSourcePool.getLocalDataSourceKey();
        if (StringUtils.isEmpty(localDataSourceKey)) {
        	// 获取请求标识,通过标识(用户身份)动态获取匹配的数据源
            String nodeId = httpServletRequest.getHeader("nodeId");
            if (StringUtils.isNotEmpty(nodeId)) {
                Map<Object, Object> targetDataSources = BlockChainDBRegistryPostProcessor.getTargetDataSources();
                if (!targetDataSources.containsKey("DB_" + nodeId)) {
                    throw new BusinessException("未配置数据源!");
                } else {
                    BlockDataSourcePool.putDataSourceKey("DB_" + nodeId);
                }
            } else {
            	// 默认数据源信息
                BlockDataSourcePool.putDataSourceKey("DB_0");
            }
        }
    }

	/** 方法执行完毕后 线程与数据源取消关联 */
    @After("changeDataSource()")
    public void restoreDataSource(JoinPoint point) {
        BlockDataSourcePool.clearDataSource();
    }

    @Before("masterDataSource()")
    public void masterDataSource(JoinPoint point) throws Throwable {
        BlockDataSourcePool.putDataSourceKey("master");
    }

	/** 理论上master是不需要释放的 有待考究 */
    @After("masterDataSource()")
    public void restoreMasterDataSource(JoinPoint point) {
        BlockDataSourcePool.clearDataSource();
    }
}

多数据源的参数配置
数据源的配置信息-数据库

/**
 * 浏览器数据源动态注册
 * @author bobch
 */
@Slf4j
@Configuration
public class BlockChainDBRegistryPostProcessor {

    private static final int DB_REDIS = DataConstant.REDIS_DB.DB7;

    @Resource
    private JedisUtil jedisUtil;
    @Value("${spring.datasource.url}")
    private String url;
    @Value("${spring.datasource.username}")
    private String username;
    @Value("${spring.datasource.password}")
    private String password;
    private BlockChainDynamicDataSource dataSource = new BlockChainDynamicDataSource();
    private static Map<Object, Object> targetDataSources = new HashMap<>();

	/** 用于动态添加数据源的添加检测 */
    public static void addTargetDataSourcesForTest(DBSettingModel dbSettingModel) throws Exception {
    	targetDataSources.put(dbSettingModel.getDbKey(), createDataSource(dbSettingModel.getUrl(), dbSettingModel.getUsername(), dbSettingModel.getPassword()));
    }
	
	/** 已存在多数据源的添加 */
    public static void addTargetDataSources(DBSettingModel dbSettingModel) {
        try {
            targetDataSources.put(dbSettingModel.getDbKey(), createDataSource(dbSettingModel.getUrl(), dbSettingModel.getUsername(), dbSettingModel.getPassword()));
        } catch (Exception e) {
            log.error("添加数据源{}失败!{}", JsonUtil.bean2Json(dbSettingModel) ,ExceptionUtils.getStackTrace(e));
        }
    }

    public static Map<Object, Object> getTargetDataSources() {
        return targetDataSources;
    }

    @Bean(name = "dataSource")
    public DataSource dataSource() throws Exception {
        DataSource baasDataSource = createDataSource(url, username, password);
        //按照目标数据源名称和目标数据源对象的映射存放在Map中
        targetDataSources.put("master", baasDataSource);
        //采用是AbstractRoutingDataSource的对象包装多数据源
        dataSource.setTargetDataSources(targetDataSources);
        //设置当前使用的数据源为master
        BlockDataSourcePool.putDataSourceKey("master");
        //设置默认的数据源,当拿不到数据源时,使用此配置
        dataSource.setDefaultTargetDataSource(targetDataSources.get("master"));
        return dataSource;
    }

    /**
     * 刷新数据源链接池,多数据源动态添加后的刷新
     */
    public void flushDB(){
        dataSource.setTargetDataSources(BlockChainDBRegistryPostProcessor.getTargetDataSources());
        dataSource.afterPropertiesSet();
    }
	
	/** 根据链接信息创建数据源实例 */
    public static DruidDataSource createDataSource(String url, String username, String password) throws Exception {
        Map<String, String> map = new HashMap<>();
        map.put(DruidDataSourceFactory.PROP_DRIVERCLASSNAME, "com.mysql.cj.jdbc.Driver");
        map.put(DruidDataSourceFactory.PROP_URL, url);
        map.put(DruidDataSourceFactory.PROP_USERNAME, username);
        map.put(DruidDataSourceFactory.PROP_PASSWORD, password);
        DruidDataSource dataSource = (DruidDataSource)DruidDataSourceFactory.createDataSource(map);
        dataSource.setBreakAfterAcquireFailure(true);
        dataSource.setConnectionErrorRetryAttempts(0);
        dataSource.setKillWhenSocketReadTimeout(true);
        dataSource.getConnection(1000);
        return dataSource;
    }

多数据源的初始化

/** 系统启动后,初始化已经配置的数据源信息 */
@Component
@Slf4j
public class SystemLineRunner implements CommandLineRunner {

    @Resource
    private DBSettingDao dbSettingDao;
    @Resource
    private BlockChainDBRegistryPostProcessor blockChainDBRegistryPostProcessor;

    @Override
    public void run(String... args) {
        List<DBSettingModel> allDB = dbSettingDao.findAllDB();
        log.info("所有数据源配置信息--{}", JsonUtil.bean2Json(allDB));
        try {
        	// 需要连接的数据源放入集合中
            allDB.forEach(BlockChainDBRegistryPostProcessor::addTargetDataSources);
            // 添加完成后的数据源刷新,后面依据切面即可使用
            blockChainDBRegistryPostProcessor.flushDB();
        } catch (Exception e) {
            log.error("数据源配置失败!{}", ExceptionUtils.getStackTrace(e));
        }
    }
  }

用户在前端手动添加数据源

    @Auth
    @PostMapping("configChainBrowserDB")
    @ApiOperation("配置数据源")
    public ResultDto configChainBrowser(@RequestBody @NotNull DBSettingModel dbSettingModel, @RequestHeader("token") String token) {
        try {
        	// 在主数据源添加配置信息
            BlockDataSourcePool.putDataSourceKey("master");
            leagueChainService.addToDBInfo(dbSettingModel, token);
            BlockDataSourcePool.clearDataSource();
            // 切换线程连接 并测试该数据源是否连接成功
            BlockDataSourcePool.putDataSourceKey(dbSettingModel.getDbKey());
            leagueChainService.checkDB(dbSettingModel);
            BlockDataSourcePool.clearDataSource();
            return ResultUtil.success();
        } catch (BusinessException e) {
            return ResultUtil.error(e.getMessage());
        } catch (Exception e) {
            return ResultUtil.error(e);
        }
    }
/**
     * 添加区块链浏览器的数据源信息
     * @param dbSettingModel
     */
    public void addToDBInfo(DBSettingModel dbSettingModel, String token) throws Exception {
        log.info("添加浏览器的数据源信息:{}", JsonUtil.bean2Json(dbSettingModel));
        Long chainIdByToken = tokenParser.getChainIdByToken(token);
        dbSettingModel.setDbKey("DB_" + chainIdByToken);
        try {
            // 移除原有连接
            BlockChainDBRegistryPostProcessor.getTargetDataSources().remove(dbSettingModel.getDbKey());
            // 添加新连接
            BlockChainDBRegistryPostProcessor.addTargetDataSourcesForTest(dbSettingModel);
        } catch (Exception e) {
            throw new BusinessException("数据源添加失败,请检查连接信息!");
        }
        blockChainDBRegistryPostProcessor.flushDB();
        // 持久化到数据库
        leagueChainDao.addDbInfo(dbSettingModel);

    }

    /**
     * 检查数据库连接
     * @param dbSettingModel
     */
    public void checkDB(DBSettingModel dbSettingModel){
        Object dataSource = BlockChainDBRegistryPostProcessor.getTargetDataSources().get(dbSettingModel.getDbKey());
        if (Objects.isNull(dataSource)) {
            throw new BusinessException("数据源未连接!");
        }
        try {
            DruidDataSource druidDataSource = (DruidDataSource) dataSource;
            druidDataSource.getConnection(10000);
            blockChainDBRegistryPostProcessor.dbStatusChange(dbSettingModel.getDbKey(), BlockChainDBRegistryPostProcessor.ON_LINE);
        } catch (Exception e) {
            log.error("数据源[{}]连接失败{}", dbSettingModel.getDbKey(), ExceptionUtils.getStackTrace(e));
            blockChainDBRegistryPostProcessor.dbStatusChange(dbSettingModel.getDbKey(), BlockChainDBRegistryPostProcessor.OFF_LINE);
            throw new BusinessException("数据源连接失败,请检查连接信息!");
        }
    }
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值