数据源切换之道

大家好,我是 Mr.Sun,一名热爱技术和分享的程序员。

​📖 个人博客​:Mr.Sun的博客

​​✨ 微信公众号​:「Java技术宇宙」

期待与你交流,让我们一起在技术道路上成长。

一、相关的POM文件

java 复制代码
<?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连接池

properties 复制代码
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.DruidDataSource

spring.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.DruidDataSource

spring.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连接池

java 复制代码
@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;
    }
}

四、动态数据源配置

定义多数据源枚举

java 复制代码
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

java 复制代码
@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(代码里配置的)

java 复制代码
@Data
@Configuration
@ConfigurationProperties(prefix = "spring.dynamic")
@EnableConfigurationProperties(value = DruidDataSourceProperties.class)
public class DynamicDataSourceConfig {

    @Autowired
    private 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 {
        @Override
        protected 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
     */
    @Bean
    public 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注解,其中方法上的优先级大于类上的

java 复制代码
@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;
    }

}

六、测试

java 复制代码
@Mapper
public interface IDefaultMapper {
    @Select("select * from sys_user")
    List<Map<String,String>> selectDefaultUser();
}
java 复制代码
@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),那么方法上的数据源优先级大于类上的

java 复制代码
@SpringBootTest
@RunWith(SpringRunner.class)
public class ExampleTest {

    @Resource
    private IDefaultMapper defaultMapper;

    @Resource
    private ISalveMapper salveMapper;

    @Test
    public 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类

实现拦截器

java 复制代码
/**
 * @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);

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
        try {
            handleDataSourceSwitch(ms);
            return invocation.proceed();
        } finally {
            DynamicDataSourceConfig.DsTypeContainer.clearDataSource();
        }
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public 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配置拦截器

java 复制代码
@Configuration
public class MyBatisConfig {

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

作者:Mr.Sun | 「Java技术宇宙」主理人

专注分享硬核技术干货与编程实践,让编程之路更简单。

​📖 深度文章​:个人博客「Mr.Sun的博客」 ​

🚀 最新推送​:微信公众号「Java技术宇宙

加我为好友(sunhw0305),备注"加群"免费加入技术交流群

相关推荐
幽络源小助理35 分钟前
springboot校园车辆管理系统源码 – SpringBoot+Vue项目免费下载 | 幽络源
vue.js·spring boot·后端
刀法如飞37 分钟前
一款开箱即用的Spring Boot 4 DDD工程脚手架
java·后端·架构
uzong1 小时前
后端系统设计文档模板
后端
幽络源小助理1 小时前
SpringBoot+Vue车票管理系统源码下载 – 幽络源免费项目实战代码
vue.js·spring boot·后端
uzong1 小时前
软件架构指南 Software Architecture Guide
后端
又是忙碌的一天1 小时前
SpringBoot 创建及登录、拦截器
java·spring boot·后端
勇哥java实战分享2 小时前
短信平台 Pro 版本 ,比开源版本更强大
后端
学历真的很重要2 小时前
LangChain V1.0 Context Engineering(上下文工程)详细指南
人工智能·后端·学习·语言模型·面试·职场和发展·langchain
计算机毕设VX:Fegn08953 小时前
计算机毕业设计|基于springboot + vue二手家电管理系统(源码+数据库+文档)
vue.js·spring boot·后端·课程设计
上进小菜猪3 小时前
基于 YOLOv8 的智能杂草检测识别实战 [目标检测完整源码]
后端