**
实现目标
**
每个用户所使用的数据源连接不一样,这就需要系统能依据用户登录信息动态切换到不同的数据源,新用户使用时可能会增加新的数据源连接,这就需要系统支持动态增加数据源;
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("数据源连接失败,请检查连接信息!");
}
}