SpringBoot实现数据源的动态切换(可在运行中增加和删除数据源)
随着应用用户数量的增加,单个数据库已经没有办法满足频繁的数据库操作请求了,所以需要配置多个数据源来满足需求。
要求需要在项目运行中可以通过后台管理来增加数据源,所以我把数据源信息存在了主数据库, 从主数据库查询到数据库列表信息,在用户请求中通过参数来切换指定数据源。
为了满足不同的场景,切换数据源有两种方式,一种是在方法中切换数据源,一种是利用AOP通过注解实现数据源的切换。
主数据源配置
主数据源中创建数据源表,用于数据源切换
# 数据源配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
druid:
# 主库数据源
master:
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: root
# 从库数据源
slave:
# 从数据源开关/默认关闭
enabled: false
url:
username:
password:
# 初始连接数
initialSize: 5
# 最小连接池数量
minIdle: 10
# 最大连接池数量
maxActive: 20
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
# 配置一个连接在池中最大生存的时间,单位是毫秒
maxEvictableIdleTimeMillis: 900000
# 配置检测连接是否有效
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
创建数据源实体类
public class DatabaseInfo{
private Long dbId;
/** 数据库类型:msyql、redis */
@Excel(name = "数据库类型:msyql、redis")
private String dbType;
/** 数据库URL或host */
@Excel(name = "数据库URL或host")
private String dbHosturl;
/** 数据库端口 */
@Excel(name = "数据库端口")
private Long dbPort;
/** 数据库名称或位置 */
@Excel(name = "数据库名称或位置")
private String dbName;
/** 数据库用户名 */
@Excel(name = "数据库用户名")
private String dbUsername;
/** 数据库密码 */
@Excel(name = "数据库密码")
private String dbPassword;
/** 增加时间 */
@JsonFormat(pattern = "yyyy-MM-dd")
@Excel(name = "增加时间", width = 30, dateFormat = "yyyy-MM-dd")
private Date addTime;
private Date updateTime;
public Long getDbId() {
return dbId;
}
public void setDbId(Long dbId) {
this.dbId = dbId;
}
public String getDbType() {
return dbType;
}
public void setDbType(String dbType) {
this.dbType = dbType;
}
public String getDbHosturl() {
return dbHosturl;
}
public void setDbHosturl(String dbHosturl) {
this.dbHosturl = dbHosturl;
}
public Long getDbPort() {
return dbPort;
}
public void setDbPort(Long dbPort) {
this.dbPort = dbPort;
}
public String getDbName() {
return dbName;
}
public void setDbName(String dbName) {
this.dbName = dbName;
}
public String getDbUsername() {
return dbUsername;
}
public void setDbUsername(String dbUsername) {
this.dbUsername = dbUsername;
}
public String getDbPassword() {
return dbPassword;
}
public void setDbPassword(String dbPassword) {
this.dbPassword = dbPassword;
}
public Date getAddTime() {
return addTime;
}
public void setAddTime(Date addTime) {
this.addTime = addTime;
}
public Date getUpdateTime() {
return updateTime;
}
public void setUpdateTime(Date updateTime) {
this.updateTime = updateTime;
}
}
编写AbstractRoutingDataSource的实现类,DynamicDataSource来动态操作数据源
public class DynamicDataSource extends AbstractRoutingDataSource
{
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources)
{
super.setDefaultTargetDataSource(defaultTargetDataSource);
super.setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}
private boolean debug = true;
private final Logger log = LoggerFactory.getLogger(getClass());
private Map<Object, Object> dynamicTargetDataSources = new HashMap<Object, Object>();
private Object dynamicDefaultTargetDataSource;
@Override
protected Object determineCurrentLookupKey() {
String datasource = DynamicDataSourceContextHolder.getDataSourceType();
if (!StringUtils.isEmpty(datasource)) {
Map<Object, Object> dynamicTargetDataSources2 = this.dynamicTargetDataSources;
if (dynamicTargetDataSources2.containsKey(datasource)) {
log.info("---当前数据源:" + datasource + "---");
} else {
log.info("不存在的数据源:");
return null;
// throw new ADIException("不存在的数据源:"+datasource,500);
}
} else {
log.info("---当前数据源:默认数据源---");
}
return datasource;
}
@Override
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
super.setTargetDataSources(targetDataSources);
this.dynamicTargetDataSources = targetDataSources;
}
}
检测数据源是否存在且可用
切换数据源之前需要先检测一下数据源是否存在,而且是正常可用。如果不存在则创建数据源,如果存在但是不可用则先删除数据源再重新创建。
/**
* @param DatabaseInfo数据源实体类
* @throws Exception
*/
public void createDataSourceWithCheck(DatabaseInfo databaseInfo) throws Exception {
String datasourceId = databaseInfo.dbId().toString();
log.info("正在检查数据源:"+datasourceId);
Map<Object, Object> dynamicTargetDataSources2 = this.dynamicTargetDataSources;
if (null!=dynamicTargetDataSources2&&dynamicTargetDataSources2.containsKey(datasourceId)) {
log.info("数据源"+datasourceId+"之前已经创建,准备测试数据源是否正常...");
//DataSource druidDataSource = (DataSource) dynamicTargetDataSources2.get(datasourceId);
DruidDataSource druidDataSource = (DruidDataSource) dynamicTargetDataSources2.get(datasourceId);
boolean rightFlag = true;
Connection connection = null;
try {
log.info(datasourceId+"数据源的概况->当前闲置连接数:"+druidDataSource.getPoolingCount());
long activeCount = druidDataSource.getActiveCount();
log.info(datasourceId+"数据源的概况->当前活动连接数:"+activeCount);
if(activeCount > 0) {
/*log.info(datasourceId+"数据源的概况->活跃连接堆栈信息:"+druidDataSource.getActiveConnectionStackTrace());*/
}
log.info("准备获取数据库连接...");
connection = druidDataSource.getConnection();
log.info("数据源"+datasourceId+"正常");
} catch (Exception e) {
log.error(e.getMessage(),e); //把异常信息打印到日志文件
rightFlag = false;
log.info("缓存数据源"+datasourceId+"已失效,准备删除...");
if(delDatasources(datasourceId)) {
log.info("缓存数据源删除成功");
} else {
log.info("缓存数据源删除失败");
}
} finally {
if(null != connection) {
connection.close();
}
}
if(rightFlag) {
log.info("不需要重新创建数据源");
return;
} else {
log.info("准备重新创建数据源...");
createDataSource(databaseInfo);
log.info("重新创建数据源完成");
}
} else {
createDataSource(databaseInfo);
}
}
创建数据源
数据源不存在创建数据源
/**
*
* @param DatabaseInfo
* @throws Exception
*/
private void createDataSource(DatabaseInfo databaseInfo ) throws Exception {
String datasourceId = databaseInfo.getDbId().toString();
log.info("准备创建数据源"+datasourceId);
String databasetype = databaseInfo.getDbtype();
String username = databaseInfo.getDbusername();
String password = databaseInfo.getDbpassword();
String url = databaseInfo.getDbhosturl();
String driveClass = "com.mysql.cj.jdbc.Driver";
// if("mysql".equalsIgnoreCase(databasetype)) {
// driveClass = DBUtil.mysqldriver;
// } else if("oracle".equalsIgnoreCase(databasetype)){
// driveClass = DBUtil.oracledriver;
// }
if(testDatasource(datasourceId,driveClass,url,username,password)) {
boolean result = this.createDataSource(datasourceId, driveClass, url, username, password, databasetype);
if(!result) {
log.error("数据源"+datasourceId+"配置正确,但是创建失败");
// throw new ADIException("数据源"+datasourceId+"配置正确,但是创建失败",500);
}
} else {
log.error("数据源配置有错误");
// throw new ADIException("数据源配置有错误",500);
}
}
创建数据源
private boolean debug = true;
private final Logger log = LoggerFactory.getLogger(getClass());
private Map<Object, Object> dynamicTargetDataSources = new HashMap<Object, Object>();
private Object dynamicDefaultTargetDataSource;
/**
*
* @param key 数据源id
* @param driveClass
* @param url 数据源url
* @param username 用户名
* @param password 密码
* @param databasetype 数据源类型
* @return
*/
public boolean createDataSource(String key, String driveClass, String url, String username, String password, String databasetype) {
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(databasetype)) {
// driveClass = DBUtil.mysqldriver;
// validationQuery = "select 1";
// } else if("oracle".equalsIgnoreCase(databasetype)){
// driveClass = DBUtil.oracledriver;
// druidDataSource.setPoolPreparedStatements(true); //是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。
// druidDataSource.setMaxPoolPreparedStatementPerConnectionSize(50);
// int sqlQueryTimeout = ADIPropUtil.sqlQueryTimeOut();
// druidDataSource.setConnectionProperties("oracle.net.CONNECT_TIMEOUT=6000;oracle.jdbc.ReadTimeout="+sqlQueryTimeout);//对于耗时长的查询sql,会受限于ReadTimeout的控制,单位毫秒
// }
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();
this.dynamicTargetDataSources.put(key, druidDataSource);
setTargetDataSources(this.dynamicTargetDataSources);// 将map赋值给父类的TargetDataSources
super.afterPropertiesSet();// 将TargetDataSources中的连接信息放入resolvedDataSources管理
log.info(key+"数据源初始化成功");
//log.info(key+"数据源的概况:"+druidDataSource.dump());
return true;
} catch (Exception e) {
log.error(e + "");
return false;
}
}
删除数据源
/**
*
* @param datasourceid 数据源id
* @return
*/
public boolean delDatasources(String datasourceid) {
Map<Object, Object> dynamicTargetDataSources2 = this.dynamicTargetDataSources;
if (dynamicTargetDataSources2.containsKey(datasourceid)) {
Set<DruidDataSource> druidDataSourceInstances = DruidDataSourceStatManager.getDruidDataSourceInstances();
for (DruidDataSource l : druidDataSourceInstances) {
if (datasourceid.equals(l.getName())) {
dynamicTargetDataSources2.remove(datasourceid);
DruidDataSourceStatManager.removeDataSource(l);
setTargetDataSources(dynamicTargetDataSources2);// 将map赋值给父类的TargetDataSources
super.afterPropertiesSet();// 将TargetDataSources中的连接信息放入resolvedDataSources管理
return true;
}
}
return false;
} else {
return false;
}
}
创建DynamicDataSourceContextHolder类实现数据源的切换
使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本, 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本
public class DynamicDataSourceContextHolder
{
public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);
/**
* 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
* 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
*/
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
/**
* 设置数据源的变量
*/
public static void setDataSourceType(String dsType)
{
log.info("切换到{}数据源", dsType);
CONTEXT_HOLDER.set(dsType);
}
/**
* 获得数据源的变量
*/
public static String getDataSourceType()
{
return CONTEXT_HOLDER.get();
}
/**
* 清空数据源变量
*/
public static void clearDataSourceType()
{
CONTEXT_HOLDER.remove();
}
}
查询数据源库中的数据实现切换数据源
通过changeDb方法查询库里的数据源信息,我把数据源信息存到了redis里,查到以后根据数据源id进行切换
public static boolean changeDb(String datasourceId){
DatabaseInfo databaseInfo = new DatabaseInfo();
databaseInfo.setCustomerid(datasourceId);
DatabaseInfo database = SpringUtils.getBean(ISysDatabaseInfoService.class).selectDbInfoListInCache(databaseInfo);
if (null!=database) {
log.info("需要使用的的数据源已经找到,datasourceId是:" + database.getDbId());
//创建数据源连接&检查 若存在则不需重新创建
try {
SpringUtils.getBean(DynamicDataSource.class).createDataSourceWithCheck(database);
//切换到该数据源
DynamicDataSourceContextHolder.setDataSourceType(database.getDbId().toString());
} catch (Exception e) {
throw new CustomException("切换数据源异常,datasourceId:" + database.getDbId(), HttpStatus.NOT_FOUND);
}
return true;
}
throw new CustomException("切换数据源未找到:" + datasourceId, HttpStatus.NOT_FOUND);
}
调用DBChangeService.changeDb(dbId);切换数据源,在操作完后,要切换回主数据源,调用DynamicDataSourceContextHolder.clearDataSourceType();
//切换数据源
DBChangeService.changeDb(dbId);
//执行数据库操作
sysLogininforService.insertLogininfor(logininfor);
//切换回主数据源
DynamicDataSourceContextHolder.clearDataSourceType();
使用AOP,用注解的方式实现切换数据源
定义注解
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface TenantDS
{
/**
* 切换数据源名称
*/
public String value() default "";
}
通过注解实现数据源切换
@Aspect
@Order(1)
@Component
public class TenantDataSourceAspect
{
protected Logger logger = LoggerFactory.getLogger(getClass());
@Pointcut("@annotation(com.test.TenantDS)"
+ "|| @within(com.test.TenantDS)")
public void dsPointCut()
{
}
@Around("dsPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable
{
TenantDS tenantDS = getDataSource(point);
//如果注解内有ISV参数,取方法的第一个参数值为数据源id
if (StringUtils.isNotBlank(tenantDS.value())&&"ISV".equals(tenantDS.value()))
{
Object params = point.getArgs()[0];
if(null!=params) {
DBChangeService.changeDb(params.toString());
}
} else if(null==DynamicDataSourceContextHolder.getDataSourceType())
{ //没有参数时,获取当前用户的数据源id
//当前是默认数据源才切换数据源
String tenanId = SecurityUtils.getTenant();
if (StringUtils.isNotBlank(tenanId)) {
DBChangeService.changeDb(tenanId);
}
}
try
{
return point.proceed();
}
finally
{
// 销毁数据源 在执行方法之后
DynamicDataSourceContextHolder.clearDataSourceType();
}
}
/**
* 获取需要切换的数据源
*/
public TenantDS getDataSource(ProceedingJoinPoint point)
{
MethodSignature signature = (MethodSignature) point.getSignature();
TenantDS tenantDS = AnnotationUtils.findAnnotation(signature.getMethod(), TenantDS.class);
if (Objects.nonNull(tenantDS))
{
return tenantDS;
}
return AnnotationUtils.findAnnotation(signature.getDeclaringType(), TenantDS.class);
}
}
使用方法
注解内有ISV参数,dbId参数就是要切换的数据源id
@TenantDS("ISV")
@GetMapping("/test")
public AjaxResult test(String dbId){
SysParame parame = parameService.selectSysParameById(1L);
return AjaxResult.success(parame);
}
注解没有参数时,获取当前用户对应的数据源id
@TenantDS
@GetMapping("/test")
public AjaxResult test(){
SysParame parame = parameService.selectSysParameById(1L);
return AjaxResult.success(parame);
}