数据源切换之道

大家好,我是 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),备注"加群"免费加入技术交流群

相关推荐
Keepreal4962 小时前
1小时快速上手SpringBoot,熟练掌握CRUD
spring boot·后端
豆浆Whisky2 小时前
Go interface性能调优指南:避免常见陷阱的实用技巧|Go语言进阶(10)
后端·go
IT_陈寒3 小时前
「Redis性能翻倍的5个核心优化策略:从数据结构选择到持久化配置全解析」
前端·人工智能·后端
风象南3 小时前
SpringBoot安全进阶:利用门限算法加固密钥与敏感配置
后端
数据知道4 小时前
Go语言:用Go操作SQLite详解
开发语言·后端·golang·sqlite·go语言
你的人类朋友11 小时前
【Node】单线程的Node.js为什么可以实现多线程?
前端·后端·node.js
iナナ12 小时前
Spring Web MVC入门
java·前端·网络·后端·spring·mvc
CoderYanger12 小时前
优选算法-双指针:2.复写零
java·后端·算法·leetcode·职场和发展
数据知道13 小时前
Go基础:用Go语言操作MongoDB详解
服务器·开发语言·数据库·后端·mongodb·golang·go语言