ITPub博客

首页 > 数据库 > 数据库开发技术 > SpringBoot动态数据源切换

SpringBoot动态数据源切换

原创 数据库开发技术 作者:毛毛小妖的笔记 时间:2023-05-05 17:27:16 0 删除 编辑
需求:项目中要求从多个源库中读取元数据信息,需要用到哪个库的信息,就加载哪个库的配置信息。比如,项目中使用的是自己搭建的数据库,但是有些数据需要从阿里云数据库中查询,这就需要能够在使用过程中动态切换数据源。

为了实现以上需求,我们可以将数据源信息存储在关系型数据库中,如下表:

当需要切换到需要的数据源时,传入相应的数据源ID或名称即可切换。

一、引入依赖

<!-- druid数据库连接池 --><dependency>    <groupId>com.alibaba</groupId>    <artifactId>druid-spring-boot-starter</artifactId>    <version>1.2.8</version></dependency>

二、yml配置文件

这个文件的目的是为了配置项目的基础数据源,当然了,你也可以在此文件中添加新的数据源信息
spring:  datasource:    type: com.alibaba.druid.pool.DruidDataSource    driverClassName: com.mysql.cj.jdbc.Driver    druid:      # 主库数据源      master:        url: jdbc:mysql://ip1:3306/demo?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8        username: root        password: 123456      # 从库数据源      slave:        # 从数据源开关/默认关闭 false/true        enabled: true        url: jdbc:mysql://ip2:3306/test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8        username: root        password: 123456      # 初始连接数      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
三、自定义切换数据源注解
为了方便的使用以上文件配置的数据源,我们可以自定义数据源注解来使用不同的数据源
import com.lacus.core.enums.DataSourceType;import java.lang.annotation.Documented;import java.lang.annotation.ElementType;import java.lang.annotation.Inherited;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;
/** * 自定义多数据源切换注解 * * 优先级:先方法,后类,如果方法覆盖了类上的数据源类型,以方法的为准,否则以类上的为准 */@Target({ElementType.METHOD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documented@Inheritedpublic @interface DataSource {
// 切换数据源名称 DataSourceType value() default DataSourceType.MASTER;}
四、定义AOP切面
为了配合以上定义的自定义数据源注解,需要通过切面去实现这个自定义的注解
import com.lacus.core.annotations.DataSource;import com.lacus.core.datasource.DynamicDataSourceContextHolder;import java.util.Objects;import lombok.extern.slf4j.Slf4j;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Pointcut;import org.aspectj.lang.reflect.MethodSignature;import org.springframework.core.annotation.AnnotationUtils;import org.springframework.core.annotation.Order;import org.springframework.stereotype.Component;
/** * 多数据源处理切面 */@Aspect@Order(1)@Component@Slf4jpublic class DataSourceAspect {
@Pointcut("@annotation(com.lacus.core.annotations.DataSource)" + "|| @within(com.lacus.core.annotations.DataSource)") public void dsPointCut() {}
@Around("dsPointCut()") public Object around(ProceedingJoinPoint point) throws Throwable { DataSource dataSource = getDataSource(point); if (dataSource != null) { DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name()); }
try { return point.proceed(); } finally { // 在执行方法之后,销毁数据源 DynamicDataSourceContextHolder.clearDataSourceType(); } }
/** * 获取需要切换的数据源 */ public DataSource getDataSource(ProceedingJoinPoint point) { MethodSignature signature = (MethodSignature) point.getSignature(); DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class); if (Objects.nonNull(dataSource)) { return dataSource; }
return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class); }}
五、Druid连接池属性配置
import com.alibaba.druid.pool.DruidDataSource;import org.springframework.beans.factory.annotation.Value;import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;import org.springframework.context.annotation.Configuration;
/** * druid 配置属性 */@ConditionalOnExpression("'${lacus.embedded.mysql}' != 'true'")@Configurationpublic class DruidProperties {
@Value("${spring.datasource.druid.initialSize}") private int initialSize;
@Value("${spring.datasource.druid.minIdle}") private int minIdle;
@Value("${spring.datasource.druid.maxActive}") private int maxActive;
@Value("${spring.datasource.druid.maxWait}") private int maxWait;
@Value("${spring.datasource.druid.timeBetweenEvictionRunsMillis}") private int timeBetweenEvictionRunsMillis;
@Value("${spring.datasource.druid.minEvictableIdleTimeMillis}") private int minEvictableIdleTimeMillis;
@Value("${spring.datasource.druid.maxEvictableIdleTimeMillis}") private int maxEvictableIdleTimeMillis;
@Value("${spring.datasource.druid.validationQuery}") private String validationQuery;
@Value("${spring.datasource.druid.testWhileIdle}") private boolean testWhileIdle;
@Value("${spring.datasource.druid.testOnBorrow}") private boolean testOnBorrow;
@Value("${spring.datasource.druid.testOnReturn}") private boolean testOnReturn;
public DruidDataSource dataSource(DruidDataSource datasource) { // 配置初始化大小、最小、最大 datasource.setInitialSize(initialSize); datasource.setMaxActive(maxActive); datasource.setMinIdle(minIdle);
// 配置获取连接等待超时的时间 datasource.setMaxWait(maxWait);
// 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
// 配置一个连接在池中最小、最大生存的时间,单位是毫秒 datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis); datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis);
//用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null, // testOnBorrow、testOnReturn、testWhileIdle都不会起作用。 datasource.setValidationQuery(validationQuery); // 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis, // 执行validationQuery检测连接是否有效。 datasource.setTestWhileIdle(testWhileIdle); // 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 datasource.setTestOnBorrow(testOnBorrow); // 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 datasource.setTestOnReturn(testOnReturn); return datasource; }}
六、数据源枚举类
public enum DataSourceType {    // 主库    MASTER,    // 从库    SLAVE}
七、Druid多数据源配置类
import cn.hutool.extra.spring.SpringUtil;import com.lacus.core.enums.DataSourceType;import com.lacus.core.datasource.DynamicDataSource;import com.alibaba.druid.pool.DruidDataSource;import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;import com.alibaba.druid.util.Utils;import java.io.IOException;import java.util.HashMap;import java.util.Map;import javax.servlet.Filter;import javax.servlet.FilterChain;import javax.servlet.ServletException;import javax.servlet.ServletRequest;import javax.servlet.ServletResponse;import javax.sql.DataSource;import lombok.extern.slf4j.Slf4j;import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.boot.web.servlet.FilterRegistrationBean;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Primary;
/** * druid 配置多数据源 */@Configuration@Slf4jpublic class DruidConfig {
@Bean @ConfigurationProperties("spring.datasource.druid.master") @ConditionalOnExpression("'${lacus.embedded.mysql}' != 'true'") public DataSource masterDataSource(DruidProperties druidProperties) { DruidDataSource dataSource = DruidDataSourceBuilder.create().build(); return druidProperties.dataSource(dataSource); }
@Bean @ConfigurationProperties("spring.datasource.druid.slave") @ConditionalOnExpression("'${lacus.embedded.mysql}' != 'true'") @ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true") public DataSource slaveDataSource(DruidProperties druidProperties) { DruidDataSource dataSource = DruidDataSourceBuilder.create().build(); return druidProperties.dataSource(dataSource); }
@Bean(name = "dynamicDataSource") @Primary public DynamicDataSource dataSource(DataSource masterDataSource) { Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource); targetDataSources.put(DataSourceType.SLAVE.name(), slaveDataSource); return new DynamicDataSource(masterDataSource, targetDataSources); }
/** * 设置数据源 * * @param targetDataSources 备选数据源集合 * @param sourceName 数据源名称 * @param beanName bean名称 */ public void setDataSource(Map<Object, Object> targetDataSources, String sourceName, String beanName) { try { DataSource dataSource = SpringUtil.getBean(beanName); targetDataSources.put(sourceName, dataSource); } catch (Exception e) { log.error("设置数据库失败", e); } }}
八、动态数据源配置类
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;import javax.sql.DataSource;import java.util.Map;
/** * 动态数据源 */public class DynamicDataSource extends AbstractRoutingDataSource {
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) { super.setDefaultTargetDataSource(defaultTargetDataSource); super.setTargetDataSources(targetDataSources); super.afterPropertiesSet(); }
@Override protected Object determineCurrentLookupKey() { return DynamicDataSourceContextHolder.getDataSourceType(); }}
重点在于determineCurrentLookupKey()方法,它的返回值是你所要用的数据源dataSource的key值。通过继承AbstractRoutingDataSource类,并重写其中的determineCurrentLookupKey()`方法,来实现数据源的切换。

九、动态数据源切换工具类

import com.alibaba.druid.pool.DruidDataSource;import com.lacus.common.exception.CustomException;import com.lacus.common.utils.spring.SpringUtils;import com.lacus.core.enums.DataSourceType;import com.lacus.dao.metadata.entity.MetaDatasourceEntity;import com.lacus.dao.metadata.enums.DatasourceTypeEnum;import com.lacus.dao.metadata.mapper.MetaDatasourceMapper;import com.lacus.service.metadata.IMetaDataSourceService;import lombok.extern.slf4j.Slf4j;import org.apache.commons.lang3.StringUtils;import java.util.HashMap;import java.util.Map;import java.util.Objects;
/** * 数据源切换处理 */@Slf4jpublic class DynamicDataSourceContextHolder {
/** * 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本, * 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。 */ private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
/** * 设置数据源的变量 */ public static void setDataSourceType(String dsType) { log.info("切换到{}数据源", dsType); CONTEXT_HOLDER.set(dsType); }
private static String convertUrl(MetaDatasourceEntity metaDatasource, String protocol) { return String.format("jdbc:%s://%s:%s/", protocol, metaDatasource.getIp(), metaDatasource.getPort()); }
/** * 获得数据源的变量 */ public static String getDataSourceType() { return CONTEXT_HOLDER.get(); }
/** * 清空数据源变量 */ public static void clearDataSourceType() { CONTEXT_HOLDER.remove(); }
/** * 传入datasourceId来切换数据源 */ public static void setDataSourceId(Long datasourceId) { // 获取MetaDatasourceMapper类实例,根据datasourceId查询数据库中的数据源连接信息 MetaDatasourceMapper metaDatasourceMapper = SpringUtils.getBean(MetaDatasourceMapper.class); MetaDatasourceEntity metaDatasource = metaDatasourceMapper.selectById(datasourceId); if (Objects.isNull(metaDatasource)) { throw new CustomException("无效数据源ID:" + datasourceId); } DruidDataSource druidDataSource = new DruidDataSource(); DatasourceTypeEnum datasourceTypeEnum = DatasourceTypeEnum.getByType(metaDatasource.getType()); if (Objects.nonNull(datasourceTypeEnum)) { druidDataSource.setDriverClassName(datasourceTypeEnum.getDriverName()); druidDataSource.setUrl(convertUrl(metaDatasource, datasourceTypeEnum.getProtocol())); } druidDataSource.setUsername(metaDatasource.getUsername()); druidDataSource.setBreakAfterAcquireFailure(true); druidDataSource.setConnectionErrorRetryAttempts(1); druidDataSource.setMaxWait(2000); druidDataSource.setFailFast(true); if(StringUtils.isNotEmpty(metaDatasource.getPassword())) { druidDataSource.setPassword(metaDatasource.getPassword()); }
DynamicDataSource dynamicDataSource = SpringUtils.getBean(DynamicDataSource.class); Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put(metaDatasource.getDatasourceName(), druidDataSource); dynamicDataSource.setTargetDataSources(targetDataSources); dynamicDataSource.afterPropertiesSet();
log.info("切换到{}数据源", metaDatasource.getDatasourceName()); CONTEXT_HOLDER.set(metaDatasource.getDatasourceName()); }}

十、使用示例

1. 使用注解切换数据源
@Service@DataSource(DataSourceType.MASTER)public class DemoService{}
@Mapper@DataSource(DataSourceType.SLAVE)public class DemoMapper{}
2. 使用datasourceId切换数据源
public List<SchemaDbDTO> getSchemaDbList(Long datasourceId) {    try {        // 使用数据源切换工具类切换数据源        DynamicDataSourceContextHolder.setDataSourceId(datasourceId);        List<SchemaDbEntity> schemaDbList = schemaMapper.listAllSchemaDb();        return schemaDbList.stream().map(entity -> {            SchemaDbDTO schemaDbDTO = new SchemaDbDTO(entity);            schemaDbDTO.setDatasourceId(datasourceId);            return schemaDbDTO;        }).collect(Collectors.toList());    } finally {        // 使用完数据源后销毁数据源连接        DynamicDataSourceContextHolder.clearDataSourceType();    }}

原文作者:毛毛小妖的笔记;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2019-04-04

  • 博文量
    70
  • 访问量
    46581