当前位置: 首页 > news >正文

数据源切换之道

大家好,我是 Mr.Sun,一名热爱技术和分享的程序员。
​📖 个人博客​:Mr.Sun的博客
​​✨ 微信公众号​:「Java技术宇宙」
期待与你交流,让我们一起在技术道路上成长。
Mr.Sun的个人博客

一、相关的POM文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.mrsun</groupId><artifactId>example</artifactId><version>1.0-SNAPSHOT</version><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.3.RELEASE</version><relativePath/></parent><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><java.version>1.8</java.version><lombok.version>1.18.10</lombok.version><commons-pool2.version>2.6.2</commons-pool2.version><mybatis-spring-boot-starter.version>2.0.0</mybatis-spring-boot-starter.version><druid.version>1.1.12</druid.version><mongodb-spring-boot-starter.version>2.2.4.RELEASE</mongodb-spring-boot-starter.version><rocketmq.version>4.3.0</rocketmq.version><jackson-databind.version>2.10.1</jackson-databind.version><druid-spring-boot-starter.version>1.2.1</druid-spring-boot-starter.version></properties><dependencies><!--Mybatis--><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>${mybatis-spring-boot-starter.version}</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><!--Druid--><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>${druid.version}</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>${druid-spring-boot-starter.version}</version></dependency><!--lombok--><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>${lombok.version}</version><scope>provided</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><dependency><groupId>log4j</groupId><artifactId>log4j</artifactId><version>1.2.17</version></dependency></dependencies></project>

二、Properties配置

这里配置了两个数据源,一个是default,一个是salve,两个数据源公用同一套Druid连接池

spring.dynamic.datasource.default.url=jdbc:mysql://localhost:3306/ry-vue?autoReconnect=true&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai&allowMultiQueries=true
spring.dynamic.datasource.default.username=root
spring.dynamic.datasource.default.password=123456
spring.dynamic.datasource.default.driver-class-name=com.mysql.cj.jdbc.Driver
spring.dynamic.datasource.default.type=com.alibaba.druid.pool.DruidDataSourcespring.dynamic.datasource.salve.url=jdbc:mysql://localhost:3306/example?autoReconnect=true&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai&allowMultiQueries=true
spring.dynamic.datasource.salve.username=root
spring.dynamic.datasource.salve.password=123456
spring.dynamic.datasource.salve.driver-class-name=com.mysql.cj.jdbc.Driver
spring.dynamic.datasource.salve.type=com.alibaba.druid.pool.DruidDataSourcespring.datasource.druid.initialSize=5
spring.datasource.druid.min-idle=5
spring.datasource.druid.max-active=30
spring.datasource.druid.max-wait=60000
spring.datasource.druid.time-between-eviction-runs-millis=60000
spring.datasource.druid.min-evictable-idle-time-millis=300000
spring.datasource.druid.validation-query=SELECT 1
spring.datasource.druid.test-while-idle=true
spring.datasource.druid.test-on-borrow=false
spring.datasource.druid.test-on-return=false
spring.datasource.druid.max-pool-prepared-statement-per-connection-size=20
spring.datasource.druid.filters=stat,wall,log4j

三、Druid连接池

@Data
@ConfigurationProperties(prefix = "spring.datasource.druid")
public class DruidDataSourceProperties {private int initialSize;private int minIdle;private int maxActive;private int maxWait;private int timeBetweenEvictionRunsMillis;private String validationQuery;private boolean testWhileIdle;private boolean testOnBorrow;private boolean testOnReturn;private String filters;public DruidDataSource dataSource(DruidDataSource datasource) {/** 配置初始化大小、最小、最大 */datasource.setInitialSize(initialSize);datasource.setMaxActive(maxActive);datasource.setMinIdle(minIdle);/** 配置获取连接等待超时的时间 */datasource.setMaxWait(maxWait);/** 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);/*** 用来检测连接是否有效的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);try {datasource.setFilters(filters);} catch (SQLException e) {throw new RuntimeException(e);}return datasource;}
}

四、动态数据源配置

定义多数据源枚举

public enum DsType {DEFAULT("default"),SALVE("salve");private final String name;DsType(String name) {this.name = name;}public static DsType getDsType(String name) {for (DsType value : DsType.values()) {if (value.name.equals(name)) {return value;}}throw new RuntimeException("没有匹配到数据源类型");}
}

切换数据源注解@DS

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface DS {public DsType value() default DsType.DEFAULT;}

动态数据源配置

核心就是在AbstractRoutingDataSource这个类,设置好多数据源配置和默认数据源,在执行SQL的时候设置数据源,然后拿到配置的数据源来执行,其中多线程隔离使用ThreadLocal来实现

Druid监控:http://localhost:8080/druid/index.html admin/123456(代码里配置的)

@Data
@Configuration
@ConfigurationProperties(prefix = "spring.dynamic")
@EnableConfigurationProperties(value = DruidDataSourceProperties.class)
public class DynamicDataSourceConfig {@Autowiredprivate DruidDataSourceProperties druidDataSourceProperties;private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceConfig.class);// spring.dynamic.datasource依赖注入Map<String, DataSourceProperties> datasource;@Bean("dynamicDataSource")public DynamicDataSource dynamicDataSource() {Map<Object, Object> targetDataSource = new HashMap<>();datasource.forEach((k, v) -> {DsType dsType = DsType.getDsType(k);targetDataSource.put(dsType, initDruidDataSource(v));});DynamicDataSource dynamicDataSource = new DynamicDataSource();dynamicDataSource.setTargetDataSources(targetDataSource);dynamicDataSource.setDefaultTargetDataSource(targetDataSource.get(DsType.DEFAULT));logger.info("初始化默认数据源:{}", DsType.DEFAULT);return dynamicDataSource;}private DruidDataSource initDruidDataSource(DataSourceProperties properties) {DruidDataSource druidDataSource = druidDataSourceProperties.dataSource(DruidDataSourceBuilder.create().build());druidDataSource.setUrl(properties.getUrl());druidDataSource.setUsername(properties.getUsername());druidDataSource.setPassword(properties.getPassword());druidDataSource.setDriverClassName(properties.getDriverClassName());return druidDataSource;}public static class DynamicDataSource extends AbstractRoutingDataSource {@Overrideprotected Object determineCurrentLookupKey() {return DsTypeContainer.getDataSource();}}public static class DsTypeContainer {private static final ThreadLocal<DsType> DS_TYPE = new ThreadLocal<>();public static void setDataSource(DsType dataSource) {if (dataSource == null) {dataSource = DsType.DEFAULT;}logger.info("设置数据源:{}", dataSource);DS_TYPE.set(dataSource);}public static DsType getDataSource() {DsType dataSource = DS_TYPE.get();if (dataSource == null) {dataSource = DsType.DEFAULT;}logger.info("获取数据源:{}", dataSource);return dataSource;}public static void clearDataSource() {DS_TYPE.remove();}}/*** 配置Druid的监控* http://localhost:8080/druid/index.html* @return*/@Beanpublic ServletRegistrationBean statViewServlet(){ServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");Map<String,String> initParams = new HashMap<>();initParams.put("loginUsername","admin");initParams.put("loginPassword","123456");initParams.put("allow","");//initParams.put("deny","127.0.0.1");bean.setInitParameters(initParams);return bean;}
}

五、(方式一) AOP切换

扫描接口类和方法上存在@DS注解,其中方法上的优先级大于类上的

@Aspect
@Component
public class DataSourceAop {/*** 扫描所有与这个注解有关的* :@within:用于匹配所有持有指定注解类型内的方法和类;* 也就是说只要有一个类上的有这个,使用@within这个注解,就能拿到下面所有的方法*:@annotation:用于匹配当前执行方法持有指定注解的方法,而这个注解只针对方法** 不添加扫描路径,应该是根据启动类的扫描范围执行的*/@Pointcut("@annotation(com.mrsun.aspectj.DS) " +"|| @within(com.mrsun.aspectj.DS)")public void doPointCut() {}@Around("doPointCut()")public Object around(ProceedingJoinPoint joinPoint) throws Throwable {DS dataSource = getDataSource(joinPoint);if (dataSource != null) {DynamicDataSourceConfig.DsTypeContainer.setDataSource(dataSource.value());}try {return joinPoint.proceed();} finally {//关闭线程资源 在执行方法之后DynamicDataSourceConfig.DsTypeContainer.clearDataSource();}}/*** 获取类或者方法上的注解* 先获取方法上的注解,然后在获取类上的注解,这就实现了方法上数据源切换优先于类上的* @param joinPoint 正在执行的连接点* @return 注解*/private DS getDataSource(ProceedingJoinPoint joinPoint) {MethodSignature method = (MethodSignature) joinPoint.getSignature();// 检查方法上的注解DS methodAnnotation = method.getMethod().getAnnotation(DS.class);if (methodAnnotation != null) {return methodAnnotation;}// 检查目标类的注解Class<?> targetClass = joinPoint.getTarget().getClass();DS classAnnotation = targetClass.getAnnotation(DS.class);if (classAnnotation != null) {return classAnnotation;}// 检查所有接口上的注解for (Class<?> interfaceClass : targetClass.getInterfaces()) {DS interfaceAnnotation = interfaceClass.getAnnotation(DS.class);if (interfaceAnnotation != null) {return interfaceAnnotation;}}// 如果仍未找到,返回默认值或抛出异常return null;}}

六、测试

@Mapper
public interface IDefaultMapper {@Select("select * from sys_user")List<Map<String,String>> selectDefaultUser();
}
@Mapper
@DS(value = DsType.SALVE)
public interface ISalveMapper {@Select("select * from example_user")List<Map<String,String>> selectSalveUser();}

这样的话,IDefaultMapper里执行的都是default数据源的,ISalveMapper里所有方法都是执行salve数据源的,如果在这里面的其中一个方法加上@DS(value=DsType.DEFAULT),那么方法上的数据源优先级大于类上的

@SpringBootTest
@RunWith(SpringRunner.class)
public class ExampleTest {@Resourceprivate IDefaultMapper defaultMapper;@Resourceprivate ISalveMapper salveMapper;@Testpublic void test() {System.out.println(defaultMapper.selectDefaultUser());System.out.println("==========================");System.out.println(salveMapper.selectSalveUser());System.out.println("==========================");System.out.println(defaultMapper.selectDefaultUser());}}

这样就可以观察日志数据源切换和查询出来的数据是否正确

七、(方式二) Mybatis插件切换

除了使用AOP的方式来实现数据源切换,也可以用Mybatis的插件拦截器,在每次执行SQL的时候,都会判断下当前方法或者类上是否存在@DS注解,然后根据类型切换数据源

如果使用这种方式,请把AOP拦截器相关代码给注释了,也即是DataSourceAop.class类

实现拦截器

/*** @author hwsun3* @date 2025/3/27* @desc*/
@Slf4j
@Intercepts({// 这里应该就是重载Executor.class类的方法,参数参考里面的@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),})
public class DataSourceInterceptor implements Interceptor {private static final Map<String, Method> METHOD_CACHE = new ConcurrentHashMap<>(256);@Overridepublic Object intercept(Invocation invocation) throws Throwable {MappedStatement ms = (MappedStatement) invocation.getArgs()[0];try {handleDataSourceSwitch(ms);return invocation.proceed();} finally {DynamicDataSourceConfig.DsTypeContainer.clearDataSource();}}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {System.out.println("mybatis-plugin-properties:===========");System.out.println(properties);}private void handleDataSourceSwitch(MappedStatement ms) {try {// 1. 解析Mapper接口信息String mapperClassName = ms.getId().substring(0, ms.getId().lastIndexOf("."));String methodName = ms.getId().substring(ms.getId().lastIndexOf(".") + 1);// 2. 获取原始接口类(处理动态代理)Class<?> mapperInterface = ClassUtils.resolveClassName(mapperClassName, ClassUtils.getDefaultClassLoader());Class<?> userClass = ClassUtils.getUserClass(mapperInterface);// 3. 获取方法对象(带缓存)Method method = METHOD_CACHE.computeIfAbsent(ms.getId(), k -> {try {// 通过MappedStatement获取实际参数类型Class<?>[] paramTypes = ms.getParameterMap().getParameterMappings().stream().map(ParameterMapping::getJavaType).toArray(Class[]::new);return userClass.getMethod(methodName, paramTypes);} catch (NoSuchMethodException e) {log.warn("Method not found: {}.{}", mapperClassName, methodName);return null;}});if (method == null) {return;}// 4. 注解查找(支持接口继承)DS dsAnnotation = AnnotationUtils.findAnnotation(method, DS.class);if (dsAnnotation == null) {dsAnnotation = AnnotationUtils.findAnnotation(userClass, DS.class);}// 5. 设置数据源if (dsAnnotation != null) {DynamicDataSourceConfig.DsTypeContainer.setDataSource(dsAnnotation.value());log.debug("Switched to datasource: {} for {}.{}",dsAnnotation.value(), userClass.getSimpleName(), method.getName());}} catch (Exception e) {log.error("DataSourceInterceptor error: {}", e.getMessage(), e);}}
}

MyBatisConfig配置拦截器

@Configuration
public class MyBatisConfig {@Beanpublic ConfigurationCustomizer mybatisConfigurationCustomizer() {return configuration -> {// 添加自定义拦截器configuration.addInterceptor(new DataSourceInterceptor());};}
}

作者:Mr.Sun | 「Java技术宇宙」主理人
专注分享硬核技术干货与编程实践,让编程之路更简单。
​📖 深度文章​:个人博客「Mr.Sun的博客」 ​
🚀 最新推送​:微信公众号「Java技术宇宙
加我为好友(sunhw0305),备注“加群”免费加入技术交流群

http://www.hskmm.com/?act=detail&tid=25675

相关文章:

  • 完整教程:tryhackme——Abusing Windows Internals(进程注入)
  • 向量存储vs知识图谱:LLM记忆系统技术选型
  • QBXT2025S刷题 Day5
  • FFT 学习笔记
  • Ai元人文系列:领域协同深耕:构建人机价值共生的文明实践框架
  • NFL统一数据生态系统技术架构解析
  • 复习题集
  • 实用指南:SCDN如何同时保障网站加速与DDoS防御?
  • 二分查找模板:基础二分与进阶二分
  • 【设计模式-4.5】行为型——迭代器模式 - 教程
  • 循环结构
  • SP6950 CTOI10D3 - A HUGE TOWER 题解
  • 浅谈并查集
  • 16_AiAgentMCP简单教程
  • 17_AiAgentMCP实现技术选型
  • JVM_XMS 和 java_opts哪种写法对?如何在JVM中设置JVM_XMS和java_opts?
  • POLIR-Society-Philosophy-mind: 思想/精神
  • 鸿蒙编译ffmpeg库 - 详解
  • 知道却做不到
  • 题解:loj154 集合划分计数
  • 为什么 Java 中打印Object类型的变量无需强转,而从Object类型的数组中取元素却要强转?
  • WinReanimator恶意软件清除指南:详细步骤与工具使用
  • 251006
  • 2025国庆Day5
  • 字节跳动开源图标库:2000+图标一键换肤的魔法 - 教程
  • 换根DP学习笔记
  • 自动化数据操作平台获3000万美元融资
  • 模块
  • 实用指南:【相机基础知识与物体检测】更新中
  • AtCoder Beginner Contest 422 游记(VP)