Springboot+Druid动态切换数据源

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
简介: 关于数据源的切换,在实际使用中经常出现,本文主要是使用Druid,最近经常使用到,根据以往项目的使用,调整后进行记录,方便自己以后查看,也便于大家一起学习

一、特点:

  • 项目启动时已制定一个默认数据源。
  • 系统不固定数据源,需要在访问时通过自定义的方式去指定或获取。

二、实现
2.1 maven引入jar
只提供关键jar包,主要就是数据库与连接池的。

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
             <groupId>tk.mybatis</groupId>
             <artifactId>mapper-spring-boot-starter</artifactId>
             <version>2.1.5</version>
         </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.16</version>
        </dependency>

2.2 数据源上下文DataSourceContextHolder
避免线程的安全问题,设置当前数据源时添加同步锁synchronized 。

package net.cnki.common.datasource;

public class DataSourceContextHolder {
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
//设置当前线程持有的数据源
    public static synchronized void setDBType(String dbType){
        contextHolder.set(dbType);
    }
//获取当前线程持有的数据源
    public static String getDBType(){
        return contextHolder.get();
    }

    public static void clearDBType(){
        contextHolder.remove();
    }
}

2.3 DynamicDataSource继承AbstractRoutingDataSource抽象类
重写了determineCurrentLookupKey()方法,在多个数据源中确定当前所需要使用的那一个。其实DynamicDataSource本身就是一个线程安全下的单例(单例本想用枚举,但是不可以继承,所以放弃了),dataSourceMap用于存储数据源信息。

package net.cnki.common.datasource;

import java.util.HashMap;
import java.util.Map;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class DynamicDataSource extends AbstractRoutingDataSource {
    private static DynamicDataSource instance;
    private static byte[] lock = new byte[0];
    private static Map<Object, Object> dataSourceMap = new HashMap<Object, Object>();

    @Override
    public void setTargetDataSources(Map<Object, Object> targetDataSources) {
        super.setTargetDataSources(targetDataSources);
        dataSourceMap.putAll(targetDataSources);
        super.afterPropertiesSet();
    }
    public Map<Object, Object> getDataSourceMap() {
        return dataSourceMap;
    }
    public static synchronized DynamicDataSource getInstance() {
        if (instance == null) {
            synchronized (lock) {
                if (instance == null) {
                    instance = new DynamicDataSource();
                }
            }
        }
        return instance;
    }
    // 必须实现其方法
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDBType();
    }
}

2.4 Druid配置类
与直接使用Druid的差距不大,只是将注入数据源的 @bean 更换成自定义的方式。此处参数可以不在配置文件获取,毕竟参数不少,根据需要就可以。不想有默认数据源去掉参数的配置就行,但还是需要去掉注入。

package net.cnki.common.datasource;

import java.util.HashMap;
import java.util.Map;

import javax.servlet.Filter;
import javax.servlet.Servlet;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;

@Configuration
public class DruidConfig {
    
    @Value("${spring.datasource.type}")
    private String db_type;
    
    @Value("${spring.datasource.driver-class-name}")
    private String db_driver_name;
    
    @Value("${spring.datasource.url}")
    private String db_url;
    
    @Value("${spring.datasource.username}")
    private String db_user;
    
    @Value("${spring.datasource.password}")
    private String db_pwd;
    
    // 连接池初始化大小 
    @Value("${spring.datasource.initialSize}")
    private int initialSize;

    // 连接池最小值
    @Value("${spring.datasource.minIdle}")
    private int minIdle;

    // 连接池最大 值
    @Value("${spring.datasource.maxActive}")
    private int maxActive;

    // 配置获取连接等待超时的时间 
    @Value("${spring.datasource.maxWait}")
    private int maxWait;

    // 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
    @Value("${spring.datasource.timeBetweenEvictionRunsMillis}")
    private int timeBetweenEvictionRunsMillis;

    // 配置一个连接在池中最小生存的时间,单位是毫秒
    @Value("${spring.datasource.minEvictableIdleTimeMillis}")
    private int minEvictableIdleTimeMillis;

    // 用来验证数据库连接的查询语句,这个查询语句必须是至少返回一条数据的SELECT语句
    @Value("${spring.datasource.validationQuery}")
    private String validationQuery;

    // 检测连接是否有效
    @Value("${spring.datasource.testWhileIdle}")
    private boolean testWhileIdle;

    // 申请连接时执行validationQuery检测连接是否有效。做了这个配置会降低性能。
    @Value("${spring.datasource.testOnBorrow}")
    private boolean testOnBorrow;

    // 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能
    @Value("${spring.datasource.testOnReturn}")
    private boolean testOnReturn;

    // 是否缓存preparedStatement,也就是PSCache。
    @Value("${spring.datasource.poolPreparedStatements}")
    private boolean poolPreparedStatements;
    
    // 指定每个连接上PSCache的大小。
    @Value("${spring.datasource.maxPoolPreparedStatementPerConnectionSize}")
    private int maxPoolPreparedStatementPerConnectionSize;
    
    // 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙  
    @Value("${spring.datasource.filters}")
    private String filters;
    
    // 通过connectProperties属性来打开mergeSql功能;慢SQL记录    
    @Value("${spring.datasource.connectionProperties}")
    private String connectionProperties;
    
    // Druid控制台配置:记录慢SQL 
    @Value("${spring.datasource.logSlowSql}")
    private String logSlowSql;
    
    @Value("${spring.datasource.removeAbandoned}")
    private boolean removeAbandoned;
    
    @Value("${spring.datasource.removeAbandonedTimeout}")
    private int removeAbandonedTimeout;
    
    @Value("${spring.datasource.logAbandoned}")
    private boolean logAbandoned;
    
    @Bean
    public DynamicDataSource druidDataSource() {
        Map<Object,Object> map = new HashMap<>();
        DynamicDataSource dynamicDataSource = DynamicDataSource.getInstance();
        DruidDataSource defaultDataSource = new DruidDataSource();
        
        defaultDataSource.setDriverClassName(db_driver_name);
        defaultDataSource.setUrl(db_url);
        defaultDataSource.setUsername(db_user);
        defaultDataSource.setPassword(db_pwd);
        defaultDataSource.setInitialSize(initialSize);
        defaultDataSource.setMinIdle(minIdle);
        defaultDataSource.setMaxActive(maxActive);
        defaultDataSource.setMaxWait(maxWait);
        defaultDataSource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
        defaultDataSource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
        defaultDataSource.setValidationQuery(validationQuery);
        defaultDataSource.setTestWhileIdle(testWhileIdle);
        defaultDataSource.setTestOnBorrow(testOnBorrow);
        defaultDataSource.setTestOnReturn(testOnReturn);
        defaultDataSource.setPoolPreparedStatements(poolPreparedStatements);
        defaultDataSource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize);
        defaultDataSource.setRemoveAbandoned(removeAbandoned);
        defaultDataSource.setRemoveAbandonedTimeout(removeAbandonedTimeout);
        defaultDataSource.setLogAbandoned(logAbandoned);
        dynamicDataSource.setDefaultTargetDataSource(defaultDataSource);
        
        map.put("default", defaultDataSource);
        dynamicDataSource.setTargetDataSources(map);
        dynamicDataSource.setDefaultTargetDataSource(defaultDataSource);
        
       
        return dynamicDataSource;
    }
    
    @Bean
    public ServletRegistrationBean<Servlet>  druid(){
        // 现在要进行druid监控的配置处理操作
        ServletRegistrationBean<Servlet> servletRegistrationBean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*");
        // 白名单,多个用逗号分割, 如果allow没有配置或者为空,则允许所有访问
        servletRegistrationBean.addInitParameter("allow", "127.0.0.1");
        // 黑名单,多个用逗号分割 (共同存在时,deny优先于allow)
        //servletRegistrationBean.addInitParameter("deny", "192.168.1.110");
        // 控制台管理用户名
        servletRegistrationBean.addInitParameter("loginUsername", "admin");
        // 控制台管理密码
        servletRegistrationBean.addInitParameter("loginPassword", "admin");
        // 是否可以重置数据源,禁用HTML页面上的“Reset All”功能
        servletRegistrationBean.addInitParameter("resetEnable", "false");

        return servletRegistrationBean;
    }
    
    @Bean
    public FilterRegistrationBean<Filter> filterRegistrationBean() {
        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>() ;
        filterRegistrationBean.setFilter(new WebStatFilter());
        //所有请求进行监控处理
        filterRegistrationBean.addUrlPatterns("/*"); 
        //添加不需要忽略的格式信息
        filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.css,/druid/*");
        return filterRegistrationBean ;
    }
    
}

2.5 DruidDataSourceUtil实现切换入口
需要进行切换时调用此方法即可,就会查看毛重是否存在,没有的新建,有的直接使用。

package net.cnki.common.datasource;

import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.alibaba.druid.pool.DruidDataSource;

/**
 * 
 * @author ZhiPengyu
 * @ClassName:    [DruidDataSourceUtil.java]
 * @Description:  [用于查找并切换数据源]
 */
public class DruidDataSourceUtil {
    private static Logger logger = LoggerFactory.getLogger(DruidDataSourceUtil.class);
    
    public static void addOrChangeDataSource(String key,String dbip,String dbname,String dbuser,String dbpwd){
        DataSourceContextHolder.setDBType("default");

        /**
         * 创建动态数据源
         */
        Map<Object, Object> dataSourceMap = DynamicDataSource.getInstance().getDataSourceMap();
        if(!dataSourceMap.containsKey(key+"master") && null != key){
            logger.info("插入新数据库连接信息为:jdbc:mysql://"+dbip+":3306/"+dbname+"?serverTimezone=Hongkong&characterEncoding=UTF-8&useSSL=false");
            DruidDataSource dynamicDataSource = new DruidDataSource();
            dynamicDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
            
            dynamicDataSource.setUsername(dbuser);
            dynamicDataSource.setUrl("jdbc:mysql://"+dbip+":3306/"+dbname+"?serverTimezone=Hongkong&characterEncoding=UTF-8&useSSL=false&nullCatalogMeansCurrent=true&allowMultiQueries=true"
                    );
            dynamicDataSource.setPassword(dbpwd);

            dynamicDataSource.setInitialSize(50);
            dynamicDataSource.setMinIdle(5);
            dynamicDataSource.setMaxActive(1000);
            dynamicDataSource.setMaxWait(5000);
            dynamicDataSource.setTimeBetweenEvictionRunsMillis(60000);
            dynamicDataSource.setMinEvictableIdleTimeMillis(300000);
            dynamicDataSource.setValidationQuery("SELECT 1 FROM DUAL");
            dynamicDataSource.setTestWhileIdle(true);
            dynamicDataSource.setTestOnBorrow(false);
            dynamicDataSource.setTestOnReturn(false);
            dynamicDataSource.setPoolPreparedStatements(true);
            dynamicDataSource.setMaxPoolPreparedStatementPerConnectionSize(20);
            
            dynamicDataSource.setRemoveAbandoned(true);
            dynamicDataSource.setRemoveAbandonedTimeout(180);
            dynamicDataSource.setLogAbandoned(true);

            dataSourceMap.put(key+"master", dynamicDataSource);
            DynamicDataSource.getInstance().setTargetDataSources(dataSourceMap);
            //切换为动态数据源实例
            DataSourceContextHolder.setDBType(key+"master");
        }else{
            //切换为动态数据源实例
            DataSourceContextHolder.setDBType(key+"master");
        }
    }
    
}

三、切换
实现切换代码:

DruidDataSourceUtil.addOrChangeDataSource(key,dbip,dbname,dbuser,dbpwd);

我这里目前用到的实现有两种,原理一样,都是事先拦截,切换后执行。拦截有两种,用于不同情况。

3.1 全局切换
全局切换也就是每次访问都会切换数据源,不需要考虑到底是哪些接口。
继承OncePerRequestFilter,进行访问拦截
访问过滤-用于访问校验jwt、过滤url、切换数据源等等,我的项目使用的springsecurity并前后端分离处理,使用jwt替换了原session会话,所以处理内容较多,参考就可。至于用到的工具包全都是自己写的,没啥特殊的都是一些封装的,实在不知道的在留言吧

package net.cnki.security.filter;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.PathMatcher;
import org.springframework.web.filter.OncePerRequestFilter;

import com.alibaba.fastjson.JSONObject;

import net.cnki.api.cnki.bean.CasDbBean;
import net.cnki.common.returned.ResponseUtil;
import net.cnki.common.returned.ResultCode;
import net.cnki.common.returned.ResultGenerator;
import net.cnki.common.datasource.DruidDataSourceUtil;
import net.cnki.common.redis.JedisUtils;
import net.cnki.common.redis.RedisConstants;
import net.cnki.security.jwt.JwtTokenUtil;
import net.cnki.util.AESUtil;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    Logger logger = LoggerFactory.getLogger(this.getClass());
    
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    ResultGenerator resultGenerator;
    @Autowired
    private PathMatcher pathMatcher;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    @Autowired
    JedisUtils jedisUtils;
    
    @Value("${jwt.token.header}")
    private String token_header;
    @Value("${jwt.token.type}")
    private String token_type;
    @Value("${jwt.token.passUrl}")
    private List<String> passUrl;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) 
            throws ServletException, IOException {
        String requestUrl = request.getRequestURI();
        logger.info("["+requestUrl+"]访问校验jwt,并将用户角色信息写入内存!");
        //判断URL是否需要验证
        Boolean flag = true;
        for(String url : passUrl){
            if(pathMatcher.match(url, requestUrl)){
                flag = false;
                break;
            }
        }
        
        //根据判断结果执行校验
        if (flag) {
            String authHeader = request.getHeader(this.token_header);
            if (authHeader != null && authHeader.startsWith(this.token_type)) {
                //获取token
                String authToken = authHeader.substring(this.token_type.length());
                if (!jwtTokenUtil.isTokenExpired(authToken)) {//无效token去更新
                    //根据token获取用户名
                    String username = jwtTokenUtil.getUserNameFromToken(authToken);
                    if (username != null) {
                        String retoken = jedisUtils.get(username, RedisConstants.datebase1);
                        if (StringUtils.isEmpty(retoken)) {
                            logger.error("用户:"+username+" 访问url:["+requestUrl+"]校验失败,未登录!");
                            ResponseUtil.out(response, 402, resultGenerator.getFreeResult(ResultCode.LOGIN_NO).toString());
                            return;
                        }
                        //获取用户对应数据源
                        String dbStr = jedisUtils.get(username,RedisConstants.datebase2);
                        if (dbStr != null) {
                            String dbinfo = AESUtil.decryptPwd(dbStr);
                            CasDbBean casDbBean = JSONObject.parseObject(dbinfo, CasDbBean.class);
                            DruidDataSourceUtil.addOrChangeDataSource(casDbBean.getSchoolId(),casDbBean.getDbIp(),casDbBean.getDbName(),casDbBean.getDbUser(),casDbBean.getDbPassword());
                        }
                        
                        UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
                        if (jwtTokenUtil.validateToken(authToken, userDetails) && !StringUtils.isEmpty(retoken)) {
                            //验证token是否有效
                            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                            SecurityContextHolder.getContext().setAuthentication(authentication);
                            
                            chain.doFilter(request, response);
                            return;
                        }
                    }
                } 
            }
        }else {//无需校验直接通过
            chain.doFilter(request, response);
            return;
        }
        logger.error("访问url:["+requestUrl+"]校验失败,无权访问!");
        ResponseUtil.out(response, 403, resultGenerator.getFreeResult(ResultCode.NO_PERMISSION).toString());
    }
    
}

3.2 局部切换
局部切换就是多数接口采用默认数据源或全局切换下的数据源,但是仍有部分接口或部分功能模块处理不同数据源的数据。
其实现也是有三种:

  1. 一种就像全局切换那样,配置指定的url路径咋子单独配置,适用功能模块的处理,接口较多,统一处理了。
  2. 其次就是,直接在接口里使用,旨在需要切换的时候执行就可以。但是需要注意,如果controller接口配置事务,不可采用此方法。
  3. 还有一种,也是此处要说的,也就是自定义注解,只需要在接口添加就行的。
相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
2月前
|
SQL 监控 druid
springboot-druid数据源的配置方式及配置后台监控-自定义和导入stater(推荐-简单方便使用)两种方式配置druid数据源
这篇文章介绍了如何在Spring Boot项目中配置和监控Druid数据源,包括自定义配置和使用Spring Boot Starter两种方法。
|
6月前
|
Java
Springboot 导出word,动态填充表格数据
Springboot 导出word,动态填充表格数据
|
6月前
|
Java 调度 Spring
SpringBoot实现多线程定时任务动态定时任务配置文件配置定时任务
SpringBoot实现多线程定时任务动态定时任务配置文件配置定时任务
651 0
|
6月前
|
安全 数据安全/隐私保护
Springboot+Spring security +jwt认证+动态授权
Springboot+Spring security +jwt认证+动态授权
209 0
|
1月前
|
druid Java Maven
|
4月前
|
监控 druid Java
spring boot 集成配置阿里 Druid监控配置
spring boot 集成配置阿里 Druid监控配置
293 6
|
3月前
|
druid Java 数据库连接
SpringBoot项目整合MybatisPlus持久层框架+Druid数据库连接池,以及实现增删改查功能
SpringBoot项目整合MybatisPlus和Druid数据库连接池,实现基本的增删改查功能。
341 0
|
5月前
|
druid Java 关系型数据库
在Spring Boot中集成Druid实现多数据源有两种常用的方式:使用Spring Boot的自动配置和手动配置。
在Spring Boot中集成Druid实现多数据源有两种常用的方式:使用Spring Boot的自动配置和手动配置。
804 5
|
5月前
|
JavaScript Java 测试技术
基于SpringBoot+Vue的宝鸡文理学院学生成绩动态追踪系统的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue的宝鸡文理学院学生成绩动态追踪系统的详细设计和实现(源码+lw+部署文档+讲解等)
31 0
|
5月前
|
Java 应用服务中间件 容器
手写SpringBoot(二)之动态切换Servlet容器
我们在切换serlvet容器的时候,会将SpringBoot默认的tomcat jar包给排除掉,换上我们需要的jar包,比如jetty。
40 0

热门文章

最新文章

下一篇
无影云桌面