SpringBoot -- 整合Druid实现多数据源动态切换方案

多数据源解决方案有很多种类,包括中间件mycat、sharding-jdbc,spring内置的多数据源方案dynamic,以及使用AOP实现的自定义多数据源动态切换方案。
网上对mycat和shrding-jdbc的使用介绍比较多多,这里主要了解一下dynamic的简单使用,以及整合Druid通过AOP切片是实现数据源切换的方案实现。

dynamic

依赖

<dependency>
	<groupId>com.baomidou</groupId>
    <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
    <version>3.4.1</version>
</dependency>

配置文件

spring:
  datasource:
    dynamic:
      datasource:
        base:
          username: root
          password: **********
          url: jdbc:mysql://127.0.0.1:3306/cloudalibaba?serverTimezone=UTC&autoReconnect=true&useUnicode=true&characterEncoding=UTF8&useSSL=false
          driver-class-name: com.mysql.jdbc.Driver
        oauth:
          username: root
          password: **********
          url: jdbc:mysql://127.0.0.1:3306/oauth2?serverTimezone=UTC&autoReconnect=true&useUnicode=true&characterEncoding=UTF8&useSSL=false
          driver-class-name: com.mysql.jdbc.Driver

使用方法

@Override
@DS("base")
public ResultVo<Object> multiDbTest() {
    
    
    try {
    
    
        List<String> tableName = testDao.getCurrentDbTable();
        return ResultVo.success(tableName);
    }catch (Exception ex){
    
    
        return ResultVo.failure("500",ex.getMessage());
    }
}

该模式适合业务简单,数据源比较少的情况,并且不是我们想要的动态切换效果

基于AOP实现,继承AbstractRoutingDataSource实现动态切换、

pom

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.3.9</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.22</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.47</version>
</dependency>
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.1.1</version>
</dependency>

配置文件

custom:
  datasource:
    enable: true
    # druid配置
    druid:
      type: com.alibaba.druid.pool.DruidDataSource
      initialSize: 50
      minIdle: 100
      maxActive: 2000
      maxWait: 60000
      timeBetweenEvictionRunsMillis: 60000
      minEvictableIdleTimeMillis: 30000
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      poolPreparedStatements: true
      maxPoolPreparedStatementPerConnectionSize: 20
      filters: stat,wall,slf4j
      connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
    # 数据源
    cloud_base:
      isBase: true
      url: jdbc:mysql://127.0.0.1:3306/cloudalibaba?serverTimezone=UTC&autoReconnect=true&useUnicode=true&characterEncoding=UTF8&useSSL=false
      username: root
      password: 888888888
      driverClassName: com.mysql.cj.jdbc.Driver
      validationQuery: select 'x'
    oauth:
      url: jdbc:mysql://127.0.0.1:3306/oauth2?serverTimezone=UTC&autoReconnect=true&useUnicode=true&characterEncoding=UTF8&useSSL=false
      username: root
      password: 8888888888
      driverClassName: com.mysql.cj.jdbc.Driver
      validationQuery: select 'x'

DruidProperties类

接受配置文件中的druid配置


@Data
@ConfigurationProperties(prefix = "custom.datasource.druid")
public class DruidProperties {
    
    

    private int initialSize;//启动程序时,在连接池中初始化多少个连接

    private int minIdle;//回收空闲连接时,将保证至少有minIdle个连接.

    private int maxActive;//连接池中最多支持多少个活动会话

    private int maxWait;//链接等待时长,连接超时,认为本次请求失败 单位毫秒,设置-1时表示无限等待

    private int timeBetweenEvictionRunsMillis;//检查空闲连接的频率,单位毫秒, 非正整数时表示不进行检查

    private int minEvictableIdleTimeMillis;//池中某个连接的空闲时长达到 timeBetweenEvictionRunsMillis 毫秒后, 连接池在下次检查空闲连接时,将回收该连接

    private boolean testWhileIdle;//当程序请求连接,池在分配连接时,是否先检查该连接是否有效。

    private boolean testOnBorrow;//程序申请连接时,检查连接有效性

    private boolean testOnReturn;//程序返还链接时,检查链接有效性

    private boolean poolPreparedStatements;

    private int maxPoolPreparedStatementPerConnectionSize;//每个连接最多缓存多少个SQL

    private String filters;//插件配置

    private String connectionProperties;//连接属性
}

JdbcContextHolder类

动态数据源持有类,包含一个本地线程共享对象,并提供set get remove 方法,切换数据源时,数据库别名信息从共享对象获取

/**
 * 记录动态数据源
 * 由切片调用putDataSource方法,确认每次local中的值,AbstractRoutingDataSource根据这里的值实现切换
 */
public class JdbcContextHolder {
    
    

    //本地线程共享对象
    private final static ThreadLocal<String> local = new ThreadLocal<>();

    public static void putDataSource(String name){
    
    
        local.set(name);
    }

    public static String getDataSource(){
    
    
        return local.get();
    }

    public static void removeDataSource(){
    
    
        local.remove();
    }
}

DataSourceConfig类

这里是获取数据库配置信息的类,

@Log4j2
@Configuration
@ConditionalOnProperty(prefix = "custom.datasource", name = "enable", havingValue = "true")
@EnableConfigurationProperties(DruidProperties.class)
public class DataSourceConfig {
    
    

	private SpringContextUtils springContextUtils;//spring上下文

	private DruidProperties druidProperties;//Durid属性配置

	private List<String> dataNames = new ArrayList<>();//数据源名称列表

	private Map<Object,Object> dataSources = new HashMap<>();//数据源列表

	private DataSource defaultDataSouce ;//默认数据库

	/**
	 * 构造器
	 * 通过构造器注入springContextUtils和druidProperties
	 * @param springContextUtils
	 * @param druidProperties
	 */
	public DataSourceConfig(SpringContextUtils springContextUtils,DruidProperties druidProperties){
    
    
		this.springContextUtils =springContextUtils ;
		this.druidProperties = druidProperties;
	}

	/**
	 * 配置多数据源和默认数据源
	 * @return
	 */
	@Bean(name = "dynamicDataSource")
	@Primary
	public DataSource dataSource(){
    
    
	//获取配置文件中custom.datasource开头的所有属性
		List<String> dataStrs = springContextUtils.getPropertyList("custom.datasource");

		//解析配置 排除关键字段
		dataNames = dataStrs.stream()
				.map(e->e.split("custom\\.datasource\\.")[1])//截取custom.datasource.后面的字符串
				.filter(e->!e.startsWith("enable"))//排除enable “是否启用多数据源”配置
				.filter(e->!e.startsWith("druid."))//排除druid配置
				.map(e->e.split("\\.")[0])//根据“.”分割获取数据库的名称属性
				.distinct()//去重
				.collect(Collectors.toList());

		//初始化数据源信息
		for (String dataName : dataNames) {
    
    
			String cloudBaseUrl =getDataProp(dataName,"url");
			String cloudBasesername =getDataProp(dataName,"username");
			String cloudBasePassword =getDataProp(dataName,"password");
			String cloudBaseDriverClassName =getDataProp(dataName,"driverClassName");
			String cloudBaseValidationQuery =getDataProp(dataName,"validationQuery");

			DataSource dataSource =initDruidDataSource(cloudBaseUrl,cloudBasesername,cloudBasePassword,cloudBaseDriverClassName,cloudBaseValidationQuery);

			if(springContextUtils.containProperty("custom.datasource."+dataName+".isBase",true)
					&& "true".equals(springContextUtils.getProperty("custom.datasource."+dataName+".isBase").toString())){
    
    
				defaultDataSouce = dataSource;
			}

			dataSources.put(dataName,dataSource);
		}
		
		DynamicDataSource dynamicDataSource = new DynamicDataSource();
		//设置默认数据源
		dynamicDataSource.setDefaultTargetDataSource(defaultDataSouce);
		//配置多个数据源
		dynamicDataSource.setTargetDataSources(dataSources);
		return dynamicDataSource;
	}

	/**
	 * Druid属性设置
	 * @param datasource
	 */
	private void setDruidOptions(DruidDataSource datasource){
    
    
		datasource.setInitialSize(druidProperties.getInitialSize());
		datasource.setMinIdle(druidProperties.getMinIdle());
		datasource.setMaxActive(druidProperties.getMaxActive());
		datasource.setMaxWait(druidProperties.getMaxWait());
		datasource.setTimeBetweenEvictionRunsMillis(druidProperties.getTimeBetweenEvictionRunsMillis());
		datasource.setMinEvictableIdleTimeMillis(druidProperties.getMinEvictableIdleTimeMillis());
		datasource.setTestWhileIdle(druidProperties.isTestWhileIdle());
		datasource.setTestOnBorrow(druidProperties.isTestOnBorrow());
		datasource.setTestOnReturn(druidProperties.isTestOnReturn());
		datasource.setPoolPreparedStatements(druidProperties.isPoolPreparedStatements());
		datasource.setMaxPoolPreparedStatementPerConnectionSize(druidProperties.getMaxPoolPreparedStatementPerConnectionSize());
		try {
    
    
			datasource.setFilters(druidProperties.getFilters());
		} catch (SQLException e) {
    
    
			log.error("druid configuration initialization filter Exception", e);
		}
		datasource.setConnectionProperties(druidProperties.getConnectionProperties());
	}

	/**
	 * 事务管理
	 * @return
	 */
	@Bean
	public PlatformTransactionManager txManager() {
    
    
		return new DataSourceTransactionManager(dataSource());
	}

	/**
	 * 配置Druid监控 http://localhost:15998/druid/datasource.html
	 * @return
	 */
	@Bean(name="druidServlet")
	public ServletRegistrationBean druidServlet() {
    
    
		ServletRegistrationBean reg = new ServletRegistrationBean();
		reg.setServlet(new StatViewServlet());

		reg.addUrlMappings("/druid/*");
		reg.addInitParameter("allow", ""); // 白名单
		return reg;
	}

	/**
	 * 设置过滤器
	 * @return
	 */
	@Bean(name = "filterRegistrationBean")
	public FilterRegistrationBean filterRegistrationBean() {
    
    
		FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
		filterRegistrationBean.setFilter(new WebStatFilter());
		filterRegistrationBean.addUrlPatterns("/*");
		filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
		filterRegistrationBean.addInitParameter("profileEnable", "true");
		filterRegistrationBean.addInitParameter("DruidWebStatFilter","/*");
		return filterRegistrationBean;
	}

	/**
	 * 初始化Druid信息
	 * @param url
	 * @param username
	 * @param password
	 * @param driverName
	 * @param validationQuery
	 * @return
	 */
	private DruidDataSource initDruidDataSource(String url,String username,String password,String driverName,String validationQuery){
    
    
		DruidDataSource datasource = new DruidDataSource();
		datasource.setUrl(url);//数据库地址
		datasource.setUsername(username);//用户名
		datasource.setPassword(password);//密码
		datasource.setDriverClassName(driverName);//设置驱动
		datasource.setValidationQuery(validationQuery);//检查语句
		setDruidOptions(datasource); // 设置druid数据源的属性
		return datasource;
	}

	/**
	 * 获取配置信息
	 * @param dataName
	 * @param propName
	 * @return
	 */
	private String getDataProp(String dataName,String propName){
    
    
		try {
    
    
			//查找指定配置信息
			if (springContextUtils.containProperty("custom.datasource."+dataName+"."+propName,true)){
    
    
				return springContextUtils.getProperty("custom.datasource."+dataName+"."+propName).toString();
			}else{
    
    
				throw new RuntimeException(String.format("数据库 %s 缺少属性 %s ",dataName,propName));
			}
		}catch (Exception ex){
    
    
			throw new RuntimeException(String.format("数据库 %s 配置信息获取失败 :%s",dataName,ex.getMessage()));
		}
	}
}

DataSourceAspect切片

@Aspect
@Order(2)
@Component
@Log4j2
public class DataSourceAspect {
    
    

    // 切入点 这里在Service层切入 如果ServiceImpl实现需要切换多个数据源,可以将切点设置在Dao层
    @Pointcut("execution(* com.dm.cloud.service.*Service..*(..))")
    public void dataSourcePointCut(){
    
    
        //引入切点
    }

    /**
     * 方法前执行切换
     * @param joinPoint
     */
    @Before("dataSourcePointCut()")
    private void before(JoinPoint joinPoint){
    
    

        Object[] args = joinPoint.getArgs(); // 参数值
        String[] argNames = ((MethodSignature)joinPoint.getSignature()).getParameterNames(); // 参数名
        //参数名和参数值的位置是一一对应的
        int targetPos = -1 ;
        for(int i=0;i<argNames.length;i++){
    
    
            /**
             * 这里主要是通过获取方法中的 db参数来获取目标数据源
             * 因此在如果想要切换数据源就必须在切片方法上 增加该参数 否则使用默认数据源
             */
            if("db".equals(argNames[i])){
    
    
                targetPos=i;
                break;
            }
        }
        try {
    
    
            if(targetPos>=0){
    
    
                //设置当前线程的目标数据库
                JdbcContextHolder.putDataSource(args[targetPos].toString());
            }else{
    
    
                //没设置数据源信息 使用默认数据源
                log.info(">>> current thread " + Thread.currentThread().getName() + " is working with default database");
            }
        }catch (Exception e){
    
    
            log.error("change database error: "+e.getMessage());
        }
    }

    /**
     * 执行完切面后,将线程共享中的数据源名称清空
     * @param joinPoint
     */
    @After("dataSourcePointCut()")
    public void after(JoinPoint joinPoint){
    
    
        log.info(">>> datasource dispose");
        JdbcContextHolder.removeDataSource();
    }

}

DynamicDataSource类 获取实际目标数据源关键类

代码很简单,继承AbstractRoutingDataSource 并实现determineCurrentLookupKey()方法。
getResolvedDataSources()方法可以获取到已经记录起来的datasSource信息,判断传进来的数据库标志是不是已经存在,并打印切换信息。

@Log4j2
public class DynamicDataSource extends AbstractRoutingDataSource {
    
    

    @Override
    protected Object determineCurrentLookupKey() {
    
    
        if (getResolvedDataSources().containsKey(JdbcContextHolder.getDataSource())) {
    
    
            log.info(">>> current thread " + Thread.currentThread().getName() + " add database【 " + JdbcContextHolder.getDataSource() + " 】 to ThreadLocal");
        } else {
    
    
            log.info(">>> current thread " + Thread.currentThread().getName() + " is working with default database");
        }
        //从共享线程中获取数据源名称
        return JdbcContextHolder.getDataSource();
    }
}
  • 根据AbstractRoutingDataSource 源码
    public Connection getConnection() throws SQLException {
          
          
       return this.determineTargetDataSource().getConnection();
    }
    
    public Connection getConnection(String username, String password) throws SQLException {
          
          
       return this.determineTargetDataSource().getConnection(username, password);
    } 
       
    
    我们可以看到,在获得链接的时候,会先调用determineTargetDataSource()方法获得DataSource
    protected DataSource determineTargetDataSource() {
          
          
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        Object lookupKey = this.determineCurrentLookupKey();
        DataSource 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 + "]");
        } else {
          
          
            return dataSource;
        }
    }
    @Nullable
    protected abstract Object determineCurrentLookupKey();
    

在determineTargetDataSource()方法中,调用了抽象方法**determineCurrentLookupKey()**来获取要切换的数据库标志。这里就是我们子类要实现的内容,我们在这里直接返回ThreadLocal中的值,添加简单的日志。

测试

Dao

@Mapper
public interface TestDao extends BaseMapper<Object> {
    
    
	//查询数据库中的表名
    @Select("SELECT TABLE_NAME AS tableName FROM information_schema.TABLES WHERE TABLE_SCHEMA = (SELECT DATABASE ())  ")
    List<String> getCurrentDbTable();
}

接口

public interface ITestService {
    
    
    /* 动态切换数据源测试 */
    ResultVo<Object> multiDbTest(String db);
}

实现类

@Service
public class TestServiceImpl implements ITestService {
    
    

    @Autowired
    TestDao testDao;
    
    @Override
    public ResultVo<Object> multiDbTest(String db) {
    
    
        try {
    
    
            List<String> tableName = testDao.getCurrentDbTable();
            return ResultVo.success(tableName);
        }catch (Exception ex){
    
    
            return ResultVo.failure("500",ex.getMessage());
        }
    }
}

控制器

@Log4j2
@RestController
@RequestMapping("/test")
public class TestController {
    
    
    @Autowired
    ITestService testService;
    
    @GetMapping("/Db/{db}")
    public ResultVo<Object> search(@PathVariable("db") String db){
    
    
        ResultVo<Object>  o= testService.multiDbTest("","",db);
        log.info("tables {}",db,o.getResult());
        return o;
    }
}

打印结果:

传入参数 oauth
2021-11-16 14:43:31.622 [http-nio-15998-exec-1] INFO com.dm.cloud.datasource.DynamicDataSource - >>> current thread http-nio-15998-exec-1 add database【 oauth 】 to ThreadLocal
2021-11-16 14:43:32.065 [http-nio-15998-exec-1] INFO com.dm.cloud.datasource.DataSourceAspect - >>> datasource dispose
2021-11-16 14:43:32.067 [http-nio-15998-exec-1] INFO com.dm.cloud.controller.TestController - tables [oauth_client_details, users]

传入参数oooo
2021-11-16 14:43:38.818 [http-nio-15998-exec-3] INFO com.dm.cloud.datasource.DynamicDataSource - >>> current thread http-nio-15998-exec-3 is working with default database
2021-11-16 14:43:38.824 [http-nio-15998-exec-3] INFO com.dm.cloud.datasource.DataSourceAspect - >>> datasource dispose
2021-11-16 14:43:38.824 [http-nio-15998-exec-3] INFO com.dm.cloud.controller.TestController - tables [account, branch_table, config_info, config_info_aggr, config_info_beta, config_info_tag, config_tags_relation, distributed_lock, global_table, group_capacity, his_config_info, lock_table, orders, product, roles, tenant_capacity, tenant_info, undo_log, users]

可以通过地址 http://localhost:xxxx/druid/datasource.html 查看连接状态、sql执行记录等信息。

猜你喜欢

转载自blog.csdn.net/qq_40096897/article/details/121338659
今日推荐