温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

SpringBoot怎么实现多数据源的切换

发布时间:2022-03-04 09:05:13 来源:亿速云 阅读:140 作者:iii 栏目:开发技术

这篇文章主要介绍“SpringBoot怎么实现多数据源的切换”,在日常操作中,相信很多人在SpringBoot怎么实现多数据源的切换问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”SpringBoot怎么实现多数据源的切换”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

    前言

    我们在进行软件开发的过程中,刚开始的时候因为无法估量系统后期的访问量和并发量,所以一开始会采用单体架构,后期如果网站流量变大, 并发量变大,那么就可能会将架构扩展为微服务架构,各个微服务对应一个数据库,不过这样的成本就有点大了,可能只是有些模块用的人比较多, 有些模块没什么人用,如果都进行服务拆分,其实也没那个必要,如果有些模块用的人比较多,那么我们可以采用读写分离来减轻压力,这样的话, 可以在一定程度上提升系统的用户体验,不过这只是在数据库的I/O上面做方案,如果系统的压力很大,那么肯定要做负载均衡,我们今天就先说 实现数据库的读写分离。我们要在代码层面实现数据库的读写分离,那么核心就是数据源的切换,本文基于AOP来实现数据源的切换。

    工程结构

    └─com
        └─steak
            └─transaction
                │  TransactionDemoApplication.java
                │  
                ├─datasource
                │  │  DatasourceChooser.java
                │  │  DatasourceConfiguration.java
                │  │  DatasourceContext.java
                │  │  DatasourceScope.java
                │  │  DynamicDatasourceAspect.java
                │  │  
                │  └─properties
                │          DynamicDatasourceProperties.java
                │          MainDatasourceProperties.java
                │          
                ├─execute
                │      PlaceOrderExecute.java
                │      
                ├─rest
                │      PlaceOrderApi.java
                │      
                ├─result
                │      R.java
                │      
                └─service
                        IntegralService.java
                        OrderService.java
                        StockService.java

    在下面的实现中,我们一个有三个数据源,其中有一个是默认的,如果不指定具体的数据源,那么就使用默认的,我们是基于申明式的方式来切换 数据源,只需要在具体的接口上加上注解便能实现数据源的切换。

    编码实现

    yml文件

    主数据源直接使用spring的配置,其他数据源采用自定义的方式,这里采用一个map结构来定义,方便进行解析,可以在yml文件里进行添加多个,在代码 逻辑层面不用改动。

    spring:
      datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        druid:
          username: root
          password: 123456
          url: jdbc:mysql://127.0.0.1:3306/db?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC
          driver-class-name: com.mysql.cj.jdbc.Driver
    
    dynamic:
      datasource: {
        slave1: {
          username: 'root',
          password: '123456',
          url: ' url: jdbc:mysql://127.0.0.1:3306/db?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC',
          driver-class-name: 'com.mysql.cj.jdbc.Driver'
        },
        slave2: {
          username: 'root',
          password: '123456',
          url: ' url: jdbc:mysql://127.0.0.1:3306/db?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC',
          driver-class-name: 'com.mysql.cj.jdbc.Driver'
        }
      }

    主数据源MainDatasourceProperties

    对于主数据源,我们单独拿出来放在一个类里面,不过其实也可以放到dynamic里面,只是需要做一定的处理,我们就简单的放几个连接属性。

    /**
     * @author 刘牌
     * @date 2022/3/220:14
     */
    @Component
    @ConfigurationProperties(prefix = "spring.datasource.druid")
    @Data
    public class MainDatasourceProperties {
        private String username;
        private String password;
        private String url;
        private String driverClassName;
    }

    其他数据源DynamicDatasourceProperties

    其他数据源使用一个Map来接受yml文件中的数据源配置

    /**
     * @author 刘牌
     * @date 2022/3/213:47
     */
    @Component
    @ConfigurationProperties(prefix = "dynamic")
    @Data
    public class DynamicDatasourceProperties {
        private Map<String,Map<String,String>> datasource;
    }

    数据源配置类DatasourceConfiguration

    配置类中主要对DataSource进行配置,主数据源我们按照正常的bean来定义连接属性,而其他数据数据源则使用反射的方式来进行连接属性的 配置,因为主数据源一般是不会变动的,但是其他数据源可能会发生变动,可能会添加,这时候如果通过硬编码取配置,那么每增加一个数据源,就需要 增加一个配置,显然不太行,所以就使用反射来进行赋值。

    /**
     * @author 刘牌
     * @date 2022/3/111:34
     */
    @Configuration
    @AllArgsConstructor
    public class DatasourceConfiguration {
    
        final MainDatasourceProperties mainDatasourceProperties;
        final DynamicDatasourceProperties dynamicDatasourceProperties;
    
        @Bean
        public DataSource datasource(){
            Map<Object,Object> datasourceMap = new HashMap<>();
            DatasourceChooser datasourceChooser = new DatasourceChooser();
            /**
             * main database
             */
            DruidDataSource mainDataSource = new DruidDataSource();
            mainDataSource.setUsername(mainDatasourceProperties.getUsername());
            mainDataSource.setPassword(mainDatasourceProperties.getPassword());
            mainDataSource.setUrl(mainDatasourceProperties.getUrl());
            mainDataSource.setDriverClassName(mainDatasourceProperties.getDriverClassName());
            datasourceMap.put("main",mainDataSource);
            /**
             * other database
             */
            Map<String, Map<String, String>> sourceMap = dynamicDatasourceProperties.getDatasource();
            sourceMap.forEach((datasourceName,datasourceMaps) -> {
                DruidDataSource dataSource = new DruidDataSource();
                datasourceMaps.forEach((K,V) -> {
                    String setField = "set" + K.substring(0, 1).toUpperCase() + K.substring(1);
                    //转换yml文件中带有-符号的属性
                    String[] strings = setField.split("");
                    StringBuilder newStr = new StringBuilder();
                    for (int i = 0; i < strings.length; i++) {
                        if (strings[i].equals("-")) strings[i + 1] = strings[i + 1].toUpperCase();
                        if (!strings[i].equals("-")) newStr.append(strings[i]);
                    }
                    try {
                        DruidDataSource.class.getMethod(newStr.toString(),String.class).invoke(dataSource,V);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                });
                datasourceMap.put(datasourceName,dataSource);
            });
            //设置目标数据源
            datasourceChooser.setTargetDataSources(datasourceMap);
            //设置默认数据源
            datasourceChooser.setDefaultTargetDataSource(mainDataSource);
            return datasourceChooser;
        }
    }

    上面使用数据源配置类中使用反射对其他数据源进行连接属性的设置,然后设置目标数据源和默认数据源,里面有一个DatasourceChooser

    DatasourceChooser

    DatasourceChooser继承自AbstractRoutingDataSourceAbstractRoutingDataSource可以实现数据源的切换,它里面的 determineCurrentLookupKey()方法需要我们返回一个数据源的名称,它会自动给我们匹配上数据源。

    /**
     * @author 刘牌
     * @date 2022/3/112:21
     */
    public class DatasourceChooser extends AbstractRoutingDataSource {
        @Override
        protected Object determineCurrentLookupKey() {
            return DatasourceContext.getDatasource();
        }
    }

    如下是AbstractRoutingDataSource的部分源码,我们可以看出数据源是一个Map结构,可以通过数据源名称查找到对应的数据源。

    package org.springframework.jdbc.datasource.lookup;
    
    public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
        @Nullable
        private Map<Object, DataSource> resolvedDataSources;
        public AbstractRoutingDataSource() {
        }
        protected DataSource determineTargetDataSource() {
            Object lookupKey = this.determineCurrentLookupKey();
            DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
        }
    
        @Nullable
        protected abstract Object determineCurrentLookupKey();
    }

    DatasourceContext

    DatasourceContext内部是一个ThreadLocal,主要是用来存储每一个线程的数据源名称和获取数据源名称,而数据源的名称我们用过AOP切面 来获取。

    /**
     * @author 刘牌
     * @date 2022/3/112:22
     */
    public class DatasourceContext {
        private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
        public static void setDatasource(String key){
            threadLocal.set(key);
        }
        public static String getDatasource(){
            return threadLocal.get();
        }
    }

    数据源注解DatasourceScope

    DatasourceScope标准在方法上面,通过scope来指定数据源,不指定默认为主数据源main

    /**
     * @author 刘牌
     * @date 2022/3/111:22
     */
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface DatasourceScope {
        String scope() default "main";
    }

    数据源切面DynamicDatasourceAspect

    我们在访问每一个带有DatasourceScope注解的方法时,都会经过数据源切面DynamicDatasourceAspect,获取到注解上面的 scope的值后,通过DatasourceContext设置数据源名称,便可实现对数据源的切换。

    /**
     * @author 刘牌
     * @date 2022/3/111:34
     */
    @Aspect
    @Component
    public class DynamicDatasourceAspect {
    
        @Pointcut("@annotation(dataSourceScope)")
        public void dynamicPointcut(DatasourceScope dataSourceScope){}
    
        @Around(value = "dynamicPointcut(dataSourceScope)", argNames = "joinPoint,dataSourceScope")
        public Object dynamicAround(ProceedingJoinPoint joinPoint , DatasourceScope dataSourceScope) throws Throwable {
            String scope = dataSourceScope.scope();
            DatasourceContext.setDatasource(scope);
            return joinPoint.proceed();
        }
    }

    使用

    只需要在具体的方法上面标注数据源注解@DatasourceScope,并指定scope的值,便可实现切换,如果不指定,那么就使用主数据源。

    /**
     * @author 刘牌
     * @date 2022/3/19:49
     */
    @Service
    @AllArgsConstructor
    public class OrderService {
        
        private JdbcTemplate jdbcTemplate;
        
        @DatasourceScope(scope = "slave1")
        public R saveOrder(Integer userId , Integer commodityId){
            String sql = "INSERT INTO `order`(user_id,commodity_id) VALUES("+userId+","+commodityId+")";
            jdbcTemplate.execute(sql);
            return R.builder().code(200).msg("save order success").build();
        }
    }

    到此,关于“SpringBoot怎么实现多数据源的切换”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注亿速云网站,小编会继续努力为大家带来更多实用的文章!

    向AI问一下细节

    免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

    AI