2021-07-13

随着应用用户数量的增加,单个数据库已经没有办法满足频繁的数据库操作请求了,所以需要配置多个数据源来满足需求。
要求需要在项目运行中可以通过后台管理来增加数据源,所以我把数据源信息存在了主数据库, 从主数据库查询到数据库列表信息,在用户请求中通过参数来切换指定数据源。
为了满足不同的场景,切换数据源有两种方式,一种是在方法中切换数据源,一种是利用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);
    }
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值