多数据源切换(一)

多数据源切换(一)

业务背景

​ 当前SpringBoot应用(SpringBoot + Mybatis)需要连接使用两个数据库源,其中一个是master数据源,另一个是external数据源,在应用中需要实现两个数据源的切换。

功能实现方案

​ 一种方案是在指定文件夹下使用指定数据源:使用Mybatis的 SqlSessionFactory 注入指定数据源,这样在使用指定文件夹下面的Mapper.xml时会切换为指定数据源,就完成了多数据源的切换。

​ 另一种方案是自定义注解:根据自定义注解上的value使用对应的数据源,使用AOP动态的切换当前使用的数据源。

​ 本文介绍实现的就是第二种方案:AOP + 自定义注解实现数据源动态切换

功能实现思路

​ 首先了解Spring对于数据库动态切换提供了哪些支持,通过搜索很容易了解到Spring提供AbstractRoutingDataSource类来实现数据源的动态切换。下面是官方对于AbstractRoutingDataSource的说明:

​ 根据各种数据源的lookup key调用指定数据源,look key通常(但不是必须)通过一些线程绑定的事务上下文来确定。在这里插入图片描述

​ 这样就可以根据实现AbstractRoutingDataSource这个虚拟类:设置当前线程中的look key来实现切换数据源。

功能实现

创建数据库及表结构

​ 创建两个数据库 master_db 和 external_db ,分别在两个数据库中创建表t_master_user和t_external_user,建表语句如下:

CREATE TABLE `t_master_user` (
  `id` bigint NOT NULL COMMENT '主键',
  `name` varchar(50) DEFAULT NULL COMMENT '姓名',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;

CREATE TABLE `t_external_user` (
  `id` bigint NOT NULL COMMENT '主键',
  `name` varchar(50) DEFAULT NULL COMMENT '姓名',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;

切换数据源代码

  1. 设置当前线程的数据源上下文,动态切换数据源

    package com.zhjw.config.db;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.Stack;
    
    /**
     * Created by zhjw on 2023/3/6
     * <p>
     * 设置当前线程的数据源上下文,动态切换数据源
     * <p>
     * {@link DynamicRoutingDataSource} 实现Spring的AbstractRoutingDataSource接口,设置当前线程使用的数据源
     *
     * @author zhjw
     * @date 2023/3/6
     */
    @Slf4j
    public class DataSourceContextHolderWrapper {
    
        /**
         * 默认数据源
         */
        public static final String DEFAULT_DS = "master";
    
        private static final ThreadLocal<Stack<String>> contextHolder = new ThreadLocal<>();
    
        /**
         * 设置数据源名
         *
         * @param dbType DB名
         */
        public static void setDB(String dbType) {
            if (contextHolder.get() == null) {
                contextHolder.set(new Stack<>());
            }
            contextHolder.get().push(dbType);
            log.debug("Set db to {}", dbType);
        }
    
        /**
         * 获取数据源名
         *
         * @return DB名
         */
        public static String getDB() {
            if (contextHolder.get() != null && !contextHolder.get().empty()) {
                log.debug("Get db as {}", contextHolder.get().peek());
                return contextHolder.get().peek();
            }
            return DEFAULT_DS;
        }
    
        /**
         * 清除数据源名
         */
        public static void clearDB() {
            if (contextHolder.get() != null && !contextHolder.get().empty()) {
                log.debug("Pop db {}", contextHolder.get().pop());
            }
            if (contextHolder.get() != null && contextHolder.get().empty()) {
                contextHolder.remove();
            }
        }
    
    }
    
    1. 实现Spring的AbstractRoutingDataSource接口,设置当前线程使用的数据源
    package com.zhjw.config.db;
    
    import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
    
    /**
     * Created by zhjw on 2023/3/6
     * <p>
     * 实现Spring的AbstractRoutingDataSource接口,设置当前线程使用的数据源
     * </p>
     *
     * @author zhjw
     * @date 2023/3/6
     */
    public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
    
        @Override
        protected Object determineCurrentLookupKey() {
            return DataSourceContextHolderWrapper.getDB();
        }
    }
    
  2. 读取多数据源配置并封装为Bean

    package com.zhjw.config.db;
    
    import com.alibaba.druid.pool.DruidDataSource;
    import com.alibaba.druid.pool.DruidDataSourceFactory;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Primary;
    import org.springframework.context.annotation.PropertySource;
    import org.springframework.jdbc.datasource.DataSourceTransactionManager;
    import org.springframework.transaction.PlatformTransactionManager;
    import org.springframework.util.CollectionUtils;
    
    import javax.annotation.Resource;
    import javax.sql.DataSource;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.Properties;
    
    /**
     * Created by zhjw on 2023/3/6
     * <p>
     * 多数据源配置类
     * </p>
     *
     * @author zhjw
     * @date 2023/3/6
     */
    @Slf4j
    @Configuration
    @PropertySource(value = "classpath:application.properties")
    public class MultipleDataSourceConfig {
    
        /**
         * 读取properties配的数据源信息
         *
         * @return
         */
        @Bean("dataSourcePropertiesMap")
        @ConfigurationProperties(prefix = "spring.datasource")
        public Map<String, Properties> dataSourcePropertiesMap() {
            return new HashMap(8);
        }
    
        /**
         * 读取properties配的数据源信息,装配为Bean
         *
         * @param dataSourcePropertiesMap 读取properties配的数据源信息
         */
        @Bean("dataSourceBeanMap")
        public Map dataSourceBeanMap(@Qualifier("dataSourcePropertiesMap") Map<String, Properties> dataSourcePropertiesMap) throws Exception {
            Map dataSourceBeanMap = new HashMap(8);
            if (CollectionUtils.isEmpty(dataSourcePropertiesMap)) {
                return dataSourceBeanMap;
            }
    
            //Properties装配Bean
            for (String key : dataSourcePropertiesMap.keySet()) {
                Properties properties = dataSourcePropertiesMap.get(key);
                DruidDataSource druidDataSource = new DruidDataSource();
                DruidDataSourceFactory.config(druidDataSource,properties);
                dataSourceBeanMap.put(key, druidDataSource);
            }
    
            return dataSourceBeanMap;
        }
    
    
        @Resource
        private Map dataSourceBeanMap;
    
        /**
         * 动态数据源: 通过AOP在不同数据源之间动态切换
         *
         * @return 数据源
         */
        @Primary
        @Bean(name = "dynamicRoutingDataSource")
        public DataSource dynamicRoutingDataSource() {
            //实现Spring的AbstractRoutingDataSource接口,设置当前线程使用的数据源
            DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource();
    
            // 默认数据源:不指定使用数据源,则使用默认数据源
            if (dataSourceBeanMap.get(DataSourceContextHolderWrapper.DEFAULT_DS) == null) {
                throw new IllegalArgumentException("Multiple datasource must define default master datasource!");
            }
            dynamicRoutingDataSource.setDefaultTargetDataSource(dataSourceBeanMap.get(DataSourceContextHolderWrapper.DEFAULT_DS));
    
            // 配置多数据源
            dynamicRoutingDataSource.setTargetDataSources(dataSourceBeanMap);
            return dynamicRoutingDataSource;
        }
    
    
        /**
         * 配置@Transactional注解事务
         *
         * @return 平台事务管理器
         */
        @Bean
        public PlatformTransactionManager transactionManager() {
            return new DataSourceTransactionManager(dynamicRoutingDataSource());
        }
    
    
    }
    
  3. 自定义切换数据库注解@DS

    package com.zhjw.annotation;
    
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * Created by zhjw on 2023/3/6
     * <p>
     * 切换数据库注解
     * </p>
     *
     * @author zhjw
     * @date 2023/3/6
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE, ElementType.METHOD})
    public @interface DS {
    
        /**
         * 数据库名称
         */
        String value() default "master";
    }
    
  4. 实现@DS注解切面(这里添加**@Order**注解为下一节内容)

    package com.zhjw.aspect;
    
    import com.zhjw.annotation.DS;
    import com.zhjw.config.db.DataSourceContextHolderWrapper;
    import lombok.extern.slf4j.Slf4j;
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.After;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.aspectj.lang.annotation.Pointcut;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.core.annotation.Order;
    import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
    import org.springframework.stereotype.Component;
    
    import java.lang.reflect.Method;
    
    /**
     * Created by zhjw on 2023/3/6
     *
     * 实现@DS注解切面
     *
     * <p>
     * 提升该切面实例化的优先级是为了不影响@Transactional注解生效:
     * 不指定优先级的话{@link AbstractRoutingDataSource#determineTargetDataSource()}方法执行在本切面之前
     * 在{@link AbstractRoutingDataSource#determineTargetDataSource()}方法的194行
     * 会导致该事务一直使用默认的datasource,而不会切换
     * </p>
     *
     * @author zhjw
     * @date 2023/3/6
     */
    @Component
    @Aspect
    @Slf4j
    //@Order(100)
    public class DynamicDataSourceAspect {
    
        /**
         * 加载数据源的切入点
         */
        @Pointcut("@annotation(com.zhjw.annotation.DS) || @within(com.zhjw.annotation.DS)")
        public void pointCut() {
        }
    
        /**
         * 加载当前线程使用数据源
         *
         * @param point
         */
        @Before("pointCut()")
        public void beforeSwitchDS(JoinPoint point) {
            //获得当前访问的class
            Class<?> clazz = point.getTarget().getClass();
            //获得访问的方法名
            String methodName = point.getSignature().getName();
            //得到方法的参数的类型
            Class[] argClass = ((MethodSignature) point.getSignature()).getParameterTypes();
    
    
            String dataSource = DataSourceContextHolderWrapper.DEFAULT_DS;
            try {
                // 得到访问的方法对象
                Method method = clazz.getMethod(methodName, argClass);
    
                // 就近原则:方法上注解大于类上注解
                /** 判断类上是否存在@DS注解 **/
                if (clazz.isAnnotationPresent(DS.class)) {
                    DS annotation = clazz.getAnnotation(DS.class);
                    // 取出注解中的数据源名
                    dataSource = annotation.value();
                }
                /**判断方法上是否存在@DS注解 **/
                if (method.isAnnotationPresent(DS.class)) {
                    DS annotation = method.getAnnotation(DS.class);
                    // 取出注解中的数据源名
                    dataSource = annotation.value();
                }
            } catch (Exception e) {
                log.error("@DS AOP failed to dynamic data source ,cause by :{}", e);
            }
    
            // 切换数据源
            DataSourceContextHolderWrapper.setDB(dataSource);
        }
    
        /**
         * 清除当前线程使用数据源
         *
         * @param point
         */
        @After("pointCut()")
        public void afterSwitchDS(JoinPoint point) {
            DataSourceContextHolderWrapper.clearDB();
        }
    
    }
    

业务数据测试代码

  1. application.properties配置内容如下:

    #端口号
    server.port=8080
    
    spring.datasource.master.url=jdbc:mysql://127.0.0.1:3306/master_db?autoReconnect=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai
    spring.datasource.master.username=root
    spring.datasource.master.password=123456
    
    spring.datasource.external.url=jdbc:mysql://127.0.0.1:3306/external_db?autoReconnect=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai
    spring.datasource.external.username=root
    spring.datasource.external.password=123456
    
    #mybatis 配置
    mybatis.mapper-locations=classpath:mybatis/*/*.xml
    mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
     
    
  2. SpringBoot启动类添加==(exclude = DataSourceAutoConfiguration.class)==,去除自动配置信息
    在这里插入图片描述

  3. 在需要切换数据源的ServiceImpl类 或者 方法上加上@DS自定义注解,以及对应的数据库信息
    在这里插入图片描述

详细的业务测试代码已上传到我的Gitee上,如对您有帮助还请给项目一个Star,十分感谢!

调取localhost:8080//insert/union 接口

在这里插入图片描述

查看结果
在这里插入图片描述

切换数据源代码封装为boot-start的jar包

​ 为了代码的复用性,将切换数据源代码封装为boot-start的jar包,其他项目使用时,直接引入jar包

 <dependency>
      <groupId>com.zhjw</groupId>
      <artifactId>multiple-datasource-spring-boot-starter</artifactId>
      <version>1.0.0</version>
</dependency>

思考题?

​ 假如在externalServiceImpl#insert方法上加上@Transactional 事务注解,则无法切换数据源,只会使用主数据源,如下图所示:
在这里插入图片描述

探究原因及解决方案在:
多数据源切换(二)

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值