@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,最后拿当前的数据源对象,实现数据源的切换。