• SpringBoot实现动态数据源配置


    场景描述:

    前一阵子接手的新项目中需要使用2个数据源。

    一个叫行云数据库,一个叫OceanBase数据库。

    就是说,我有时候查询要查行云的数据,有时候查询要查 OceanBase 的数据,咋办?

    废话不多说, 下面以mysql为例,开整。

    一、环境依赖

    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-testartifactId>
        <scope>testscope>
    dependency>
    <dependency>
        <groupId>org.mybatis.spring.bootgroupId>
        <artifactId>mybatis-spring-boot-starterartifactId>
        <version>2.3.0version>
    dependency>
    <dependency>
        <groupId>mysqlgroupId>
        <artifactId>mysql-connector-javaartifactId>
        <version>8.0.28version>
    dependency>
    

    二、实现思路

    在进行下一步之前,我们必须要知道 SpringBoot 自动配置的相关原理,因为之前,我们大多是单数据源。

    # 配置mysql数据库
    spring:
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/blog?serverTimezone=Asia/Shanghai&allowMultiQueries=true
        username: root
        password: root
    

    从前只是会用,会配置,甚至都是别人给配置的自己直接拿来用。

    但是要搞懂动态数据源就必须要先搞懂自动配置

    0.o?让我看看怎么个事儿之SpringBoot自动配置

    现在我们要实现多数据源,并且可以自动切换

    也就是我 A 查询连接的是行云数据库。

    而我 B 查询却连接的是 OceanBase 数据库。

    怎么办?

    那第一个肯定就不能再使用DataSourceAutoConfigurtation了。

    我直接反手一个 exclude 。

    @EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
    

    然后呢?

    Spring boot想得很周到,它提供了AbstractRoutingDataSource 抽象类。

    这个类能根据用户定义的规则选择当前的数据源


    有同学要问了:

    AbstractRoutingDataSource 是什么东西?

    AbstractRoutingDataSource 是一个抽象类。

    它继承了 AbstractDataSource 抽象类。

    而 AbstractDataSource 实现了 DataSource 接口。

    也就是说:AbstractRoutingDataSource 他实际上就是一个DataSource 。

    AbstractRoutingDataSource 中有两个关键方法。

    setTargetDataSources(Map<Object, Object> targetDataSources)
    

    第一个方法见名知意,设置目标数据源(复数也就是多个)。

    protected abstract Object determineCurrentLookupKey();
    

    第二个方法是仅有的一个抽象方法,需要开发者具体实现,也可以见名知意,决定当前使用(目标数据源中的)哪个

    我们要做的是什么?

    我们准备 2 个数据源,全部配置好放进 Map targetDataSources 里备用。

    我们继承 AbstractRoutingDataSource 并实现抽象方法 determineCurrentLookupKey() 。

    当我们继承AbstractRoutingDataSource时我们自身也是一个数据源。

    对于数据源必然有连接数据库的动作。

    只是AbstractRoutingDataSource的getConnection()方法里实际是调用determineTargetDataSource()返回的数据源的getConnection()方法。

    这样我们可以在执行查询之前,设置使用的数据源。

    实现可动态路由的数据源,在每次数据库查询操作前执行。

    它的抽象方法 determineCurrentLookupKey() 决定使用哪个数据源。

    我知道肯定有人看不懂要上嘴脸了:

    Talk is cheap, show me the FUCKING code !!!

    2.1 配置文件

    spring:
      datasource:
        dynamic:
          db1:
            driverClassName: com.mysql.cj.jdbc.Driver
            url: jdbc:mysql://localhost:3306/db1?serverTimezone=Asia/Shanghai&allowMultiQueries=true
            username: root
            password: root
          db2:
            driverClassName: com.mysql.cj.jdbc.Driver
            url: jdbc:mysql://localhost:3306/db2?serverTimezone=Asia/Shanghai&allowMultiQueries=true
            username: root
            password: root
    

    2.2 自定义动态数据源

    DynamicDataSource继承AbstractRoutingDataSource。

    public class DynamicDataSource extends AbstractRoutingDataSource {
        @Override
        protected Object determineCurrentLookupKey() {
            return DynamicDataSourceContextHolder.getDataSource();
        }
    }
    

    这里的determineCurrentLookupKey方法,需要返回一个数据源。

    又有同学问了:

    DynamicDataSourceContextHolder 又是什么东西?

    public class DynamicDataSourceContextHolder {
        public static final ThreadLocal CONTEXT_HOLDER = new ThreadLocal<>();
    
        public static String getDataSource() {
            return CONTEXT_HOLDER.get();
        }
    
        public static void setDataSource(String dataSource) {
            CONTEXT_HOLDER.set(dataSource);
        }
    
        public static void clearDataSource() {
            CONTEXT_HOLDER.remove();
        }
    }
    

    看到 Context 应该很熟悉了,跟程序上下文有关。

    它的作用就是你查询数据库的时候用哪个数据源,就 setDataSource 哪个。

    还有点懵?没事,继续往下看。

    @Configuration
    @EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
    public class DynamicDataSourceConfig {
        @Bean("db1")
        @ConfigurationProperties(prefix = "spring.datasource.dynamic.db1")
        public DataSource db1() {
            return DataSourceBuilder.create().build();
        }
        @Bean("db2")
        @ConfigurationProperties(prefix = "spring.datasource.dynamic.db2")
        public DataSource db2() {
            return DataSourceBuilder.create().build();
        }
    
        @Bean
        @Primary
        public DataSource dataSource() {
            Map dataSourceMap = new HashMap<>(2);
            dataSourceMap.put("db1", db1());
            dataSourceMap.put("db2", db2());
    
            DynamicDataSource dynamicDataSource = new DynamicDataSource();
            // 需要设置的多数据源
            dynamicDataSource.setTargetDataSources(dataSourceMap);
            // 主数据源/默认数据源
            dynamicDataSource.setDefaultTargetDataSource(db1());
            return dynamicDataSource;
        }
    }
    

    这是比较常见的自定义数据源配置了。

    可以看到一共注册了3个数据源。

    但是最后一个DynamicDataSource有 @Primary 注解,它表明这个数据源优先级更高。

    DynamicDataSource中设置了dataSourceMap,也就是保存了 db1 和 db2。

    以上我们动态数据源配置的工作就做完了。

    我们以实际查询中的操作完整捋一遍这当中到底发生了什么!

    @Service
    public class UserService {
        @Autowired
        private UserMapper userMapper;
        /**
         *使用db2数据源
         */
        public void saveUser(UserDto userDto) {
            DynamicDatasourceHolder.setDataSource("db2");
            User user = new User();
            user.setUName(userDto.getName());
            userMapper.insert(user);
            DynamicDatasourceHolder.removeDataSource("db2");
        }
    }
    

    首先,DynamicDatasourceHolder 设置了数据源 db2 。

    CONTEXT_HOLDER 中就保存了一个 “db2” 字符串。

    userMapper 进行数据库操作之前,MyBatis 框架替我们做了一些事。

    其中一件事是获取数据库连接

    MyBatis 就在想:我得找个 DataSource ,因为DataSource 有getConnection() 方法。

    谁是 DataSource ?

    继承了 AbstractRoutingDataSource 的 DynamicDataSource 大声喊到:我是 !

    开始连接数据库!

    @Override
    public Connection getConnection() throws SQLException {
    	return determineTargetDataSource().getConnection();
    }
    

    连接哪个?

    protected DataSource determineTargetDataSource() {
        // 哥,看这一行!
       Object lookupKey = determineCurrentLookupKey();
       DataSource dataSource = this.resolvedDataSources.get(lookupKey);
       return dataSource;
    }
    

    连接这个!

    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSource();
    }
    

    连接完成!

    insert 完成!

    removeDataSource("db2") !

    每次这样用嫌麻烦?

    办他!

    三、动态数据源注解@DS

    看这部分之前需要一些前置知识点:

    Java注解,看完就会用

    师爷,翻译翻译什么叫AOP

    @Target({ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Component
    public @interface DS {
        String value() default "db1";
    }
    
    @Component
    @Aspect
    public class DynamicDataSourceAspect {
    
        @Pointcut("@annotation(com.example.demo.annotation.DS)")
        public void dynamicDataSourcePointCut() {
    
        }
    
        @Around("dynamicDataSourcePointCut()")
        public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
            String dataSourceKey = "db1";
            // 类上的注解
            Class aClass = joinPoint.getTarget().getClass();
            DS annotation = aClass.getAnnotation(DS.class);
    
            // 方法上的注解
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            DS annotationMethod = signature.getMethod().getAnnotation(DS.class);
    
            if (Objects.nonNull(annotationMethod)) {
                dataSourceKey = annotationMethod.value();
            } else {
                dataSourceKey = annotation.value();
            }
            // 设置数据源
            DynamicDataSourceContextHolder.setDataSource(dataSourceKey);
            try {
                 return joinPoint.proceed();
            }finally {
                DynamicDataSourceContextHolder.clearDataSource();
            }
        }
    }
    

    具体我就不讲解了,自己对照前两篇文章研究一下吧。


    最后再补充一下,动态数据源实际上有现成的依赖包可以用,可以参考使用,地址如下:

    https://github.com/baomidou/dynamic-datasource


    往期推荐:

    师爷,翻译翻译什么叫AOP

    0.o?让我看看怎么个事儿之SpringBoot自动配置

    终于搞懂动态代理了!

    学会@ConfigurationProperties月薪过三千

    不是银趴~是@Import!

    @Conditional+@Configuration有没有搞头?

  • 相关阅读:
    强化学习 导论
    UG7.5三四五轴编程+后处理讲解视频教程
    动静图结合详解: 归并排序 ,计数排序
    SpringMVC-2-Controller和RestFul风格
    [论文笔记]MacBERT
    Ubuntu:Arduino IDE 开发环境配置【保姆级】
    Keras CIFAR-10分类 SVM 分类器篇
    SpringBoot 如何进行参数校验
    向量数据库X云计算驱动大模型落地电商行业,Zilliz联合AWS探索并贡献成熟解决方案
    圣杯布局的两种实现
  • 原文地址:https://www.cnblogs.com/cosimo/p/17994801