首页 > 数据库 > 数据库开发技术 > SpringBoot动态数据源切换
需求:项目中要求从多个源库中读取元数据信息,需要用到哪个库的信息,就加载哪个库的配置信息。比如,项目中使用的是自己搭建的数据库,但是有些数据需要从阿里云数据库中查询,这就需要能够在使用过程中动态切换数据源。
为了实现以上需求,我们可以将数据源信息存储在关系型数据库中,如下表:
当需要切换到需要的数据源时,传入相应的数据源ID或名称即可切换。
<!-- druid数据库连接池 --><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.2.8</version></dependency>
spring:datasource:type: com.alibaba.druid.pool.DruidDataSourcedriverClassName: com.mysql.cj.jdbc.Driverdruid:# 主库数据源master:url: jdbc:mysql://ip1:3306/demo?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8username: rootpassword: 123456# 从库数据源slave:# 从数据源开关/默认关闭 false/trueenabled: trueurl: jdbc:mysql://ip2:3306/test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8username: rootpassword: 123456# 初始连接数initialSize: 5# 最小连接池数量minIdle: 10# 最大连接池数量maxActive: 20# 配置获取连接等待超时的时间maxWait: 60000# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒timeBetweenEvictionRunsMillis: 60000# 配置一个连接在池中最小生存的时间,单位是毫秒minEvictableIdleTimeMillis: 300000# 配置一个连接在池中最大生存的时间,单位是毫秒maxEvictableIdleTimeMillis: 900000# 配置检测连接是否有效validationQuery: SELECT 1 FROM DUALtestWhileIdle: truetestOnBorrow: falsetestOnReturn: 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;/*** 自定义多数据源切换注解** 优先级:先方法,后类,如果方法覆盖了类上的数据源类型,以方法的为准,否则以类上的为准*/public DataSource {// 切换数据源名称DataSourceType value() default DataSourceType.MASTER;}
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;/*** 多数据源处理切面*/public class DataSourceAspect {+ )public void 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);}}
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 配置属性*/public class DruidProperties {private int initialSize;private int minIdle;private int maxActive;private int maxWait;private int timeBetweenEvictionRunsMillis;private int minEvictableIdleTimeMillis;private int maxEvictableIdleTimeMillis;private String validationQuery;private boolean testWhileIdle;private boolean testOnBorrow;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}
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 配置多数据源*/public class DruidConfig {public DataSource masterDataSource(DruidProperties druidProperties) {DruidDataSource dataSource = DruidDataSourceBuilder.create().build();return druidProperties.dataSource(dataSource);}public DataSource slaveDataSource(DruidProperties druidProperties) {DruidDataSource dataSource = DruidDataSourceBuilder.create().build();return druidProperties.dataSource(dataSource);}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();}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());}}
public class DemoService{}public class DemoMapper{}
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();}}
原文作者:毛毛小妖的笔记;如有侵权,请联系管理员删除。