zoukankan      html  css  js  c++  java
  • Spring Boot2.x 动态数据源配置

    原文链接: Spring Boot2.x 动态数据源配置


    基于 Spring Boot 2.x、Spring Data JPA、druid、mysql 的动态数据源配置Demo,适合用于数据库的读写分离等应用场景。通过在Service层方法上添加自定义注解实现读写不同的数据库。

    配置文件已配置好druid监控相关属性,监控页面链接:ip:8080/druid。账号:admin,密码:123456。详情查看 application.yml 文件。

    注意事项(前言)

    在网上有很多关于动态切换数据源的配置教程,其中百分之九十的都是基于 Mybatis 的。当然也有零星的几篇基于 Spring Data JPA 的配置教程,不过当你按着这些教程使用后就会发现靠谱一点的还可以做到不同的请求可以使用不同的数据源,但是无法做到在同一个请求内进行多个数据源之间的切换。在业务逻辑相对复杂的情况下肯定是不能满足需求的。

    那么是什么原因导致在同一请求内切换数据源失败呢?经过单步调试和查看日志发现自己写的注解确实生效了,只不过在第二次切换数据源时没有执行 AbstractRoutingDataSourcedetermineCurrentLookupKey() 的方法而是直接拿到了数据库连接去执行了SQL语句。那么这个方法是做什么的呢?

    protected DataSource determineTargetDataSource() {
            Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
            Object lookupKey = this.determineCurrentLookupKey();
            DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
            if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
                dataSource = this.resolvedDefaultDataSource;
            }
    
            if (dataSource == null) {
                throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
            } else {
                return dataSource;
            }
        }

    从方法命名也能看出来这是个来决定使用哪个数据源的方法。上述源码第三行通过调用 this.determineCurrentLookupKey(); 方法获取应该使用的数据源所对应的 key 值。也就是我们在 DataSourceContextHolder 放到 contextHolder 中的值。因为我们使用 DynamicDataSource 继承 AbstractRoutingDataSource 并重写了 determineCurrentLookupKey() 方法。在重写的方法中我们获取到了之前存入的数据源所对应的key,所以如果每次切换数据源时执行此方法后才算切换成功。

    那么为什么使用 Spring Data JPA 切换一次数据源后第二次就切不过去了呢?经过查阅各种资料发现,在一个事务中如果不配置事务的传播级别是不会开启一个新事务的,因为 Spring 默认的事务级别是 PROPAGATION_REQUIRED 。也就是说如果不开启一个新的事务就不会进行数据源的切换。因为Spring Data JPA 整合了 hibernate ,且 hibernate 的 session 是与 transaction 绑定的,所以多次切换数据源时获取到的 session 的 hashCode 是同一个也就是第一次切换的数据源。这也就是为什么在同一个 Service 中无法做到可以切换多个数据源。(注:此 session 非常说的 web 中的那个 session)

    那怎么解决这个问题呢?既然session和当前的事务时绑定的,那是不是在切片中把要切换的 key 值存储到 contextHolder 中后,手动断掉原来的session连接就可以了?在切片操作中加入下面两行代码:

    SessionImplementor session = entityManager.unwrap(SessionImplementor.class);
    //最关键的一句代码, 手动断开连接,不用重新设置 ,会自动重新设置连接。
    session.disconnect();

    经过测试这样设置后则可以在同一个 Service 中切换操作不同的数据源读写数据。问题解决方案代码见 https://www.changxuan.top/?p=772

    注意:如果在一次请求中通过数据源A执行的一条SQL语句,然后又切换到数据源B执行同样的SQL语句。此时框架为了性能会直接返回从数据源A的数据库中查询到的数据。所以这种情况是会切换失败。

    配置 pom.xml 文件

          <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-aop</artifactId>
            </dependency>
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid-spring-boot-starter</artifactId>
                <version>1.1.21</version>
            </dependency>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-jdbc</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-jpa</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-devtools</artifactId>
                <scope>runtime</scope>
                <optional>true</optional>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-configuration-processor</artifactId>
                <optional>true</optional>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <optional>true</optional>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
                <exclusions>
                    <exclusion>
                        <groupId>org.junit.vintage</groupId>
                        <artifactId>junit-vintage-engine</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <scope>test</scope>
            </dependency>
        </dependencies>

    配置application.yml文件

    spring:
      datasource:
        druid:
          primary:
            driverClassName: com.mysql.cj.jdbc.Driver
            username: root
            password: root
            url: jdbc:mysql://localhost:3306/primary?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
            filters: stat,wall
          local:
            driver-class-name: com.mysql.cj.jdbc.Driver
            username: root
            password: root
            url: jdbc:mysql://localhost:3306/local?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
            filters: stat,wall
          stat-view-servlet:
            enabled: true
            login-username: admin
            login-password: 123456
            reset-enable: false
            url-pattern: /druid/*
          web-stat-filter:
            enabled: true
            # 添加过滤规则
            url-pattern: /*
            # 忽略过滤格式
            exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"
      jpa:
        database: MYSQL
        hibernate:
          show_sql: true
          format_sql: true
          primary-dialect: org.hibernate.dialect.MySQL5InnoDBDialect
          secondary-dialect: org.hibernate.dialect.MySQL5InnoDBDialect
          # 打开后会自动在主库生成表
          # ddl-auto: update
        database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
        # 打开后会自动在主库生成表
        # generate-ddl: true
    

    项目目录结构

    目录结构

    DataSource.java

    package dynamic.data.annotation;
    
    import dynamic.data.common.ContextConst;
    
    import java.lang.annotation.*;
    /**
     * @Author: ChangXuan
     * @Decription:
     * @Date: 22:25 2020/2/23
     **/
    @Target({ElementType.TYPE,ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface DataSource {
        ContextConst.DataSourceType value() default ContextConst.DataSourceType.PRIMARY;
    }

    DynamicDataSourceAspect.java

    package dynamic.data.aspect;
    
    import dynamic.data.common.ContextConst;
    import dynamic.data.datasource.DataSourceContextHolder;
    import dynamic.data.annotation.DataSource;
    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.reflect.MethodSignature;
    import org.springframework.stereotype.Component;
    
    import java.lang.reflect.Method;
    /**
     * @Author: ChangXuan
     * @Decription:
     * @Date: 22:28 2020/2/23
     **/
    @Component
    @Aspect
    public class DynamicDataSourceAspect {
        @Before("execution(* dynamic.data.service..*.*(..))")
        public void before(JoinPoint point){
            try {
                DataSource annotationOfClass = point.getTarget().getClass().getAnnotation(DataSource.class);
                String methodName = point.getSignature().getName();
                Class[] parameterTypes = ((MethodSignature) point.getSignature()).getParameterTypes();
                Method method = point.getTarget().getClass().getMethod(methodName, parameterTypes);
                DataSource methodAnnotation = method.getAnnotation(DataSource.class);
                methodAnnotation = methodAnnotation == null ? annotationOfClass:methodAnnotation;
                ContextConst.DataSourceType dataSourceType = methodAnnotation != null && methodAnnotation.value() !=null ? methodAnnotation.value() :ContextConst.DataSourceType.PRIMARY ;
                DataSourceContextHolder.setDataSource(dataSourceType.name());
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            }
        }
    
        @After("execution(* dynamic.data.service..*.*(..))")
        public void after(JoinPoint point){
            DataSourceContextHolder.clearDataSource();
        }
    }

    ContextConst.java

    package dynamic.data.common;
    
    /**
     * @Author: ChangXuan
     * @Decription:
     * @Date: 22:17 2020/2/23
     **/
    public interface ContextConst {
        enum DataSourceType{
            PRIMARY,LOCAL
        }
    }

    DataSourceContextHolder .java

    package dynamic.data.datasource;
    
    /**
     * @Author: ChangXuan
     * @Decription:
     * @Date: 22:23 2020/2/23
     **/
    public class DataSourceContextHolder {
    
        private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
    
        public static void setDataSource(String dbType){
            System.out.println("切换到["+dbType+"]数据源");
            contextHolder.set(dbType);
        }
    
        public static String getDataSource(){
            return contextHolder.get();
        }
    
        public static void clearDataSource(){
            contextHolder.remove();
        }
    }

    DynamicDataSource.java

    package dynamic.data.datasource;

    import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

    /**
    * @Author: ChangXuan
    * @Decription:
    * @Date: 22:22 2020/2/23
    **/
    public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
    return DataSourceContextHolder.getDataSource();
    }
    }

    MutiplyDataSource.java

    package dynamic.data.datasource;
    
    import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
    import dynamic.data.common.ContextConst;
    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.jdbc.datasource.DataSourceTransactionManager;
    import org.springframework.transaction.PlatformTransactionManager;
    
    import javax.sql.DataSource;
    import java.util.HashMap;
    /**
     * @Author: ChangXuan
     * @Decription:
     * @Date: 22:15 2020/2/23
     **/
    @Configuration
    public class MutiplyDataSource {
        @Bean(name = "dataSourcePrimary")
        @ConfigurationProperties(prefix = "spring.datasource.druid.primary")
        public DataSource primaryDataSource(){
            return DruidDataSourceBuilder.create().build();
        }
    
        @Bean(name = "dataSourceLocal")
        @ConfigurationProperties(prefix = "spring.datasource.druid.local")
        public DataSource localDataSource(){
            return DruidDataSourceBuilder.create().build();
        }
    
        @Primary
        @Bean(name = "dynamicDataSource")
        public DataSource dynamicDataSource() {
            DynamicDataSource dynamicDataSource = new DynamicDataSource();
            //配置默认数据源
            dynamicDataSource.setDefaultTargetDataSource(primaryDataSource());
    
            //配置多数据源
            HashMap<Object, Object> dataSourceMap = new HashMap();
            dataSourceMap.put(ContextConst.DataSourceType.PRIMARY.name(),primaryDataSource());
            dataSourceMap.put(ContextConst.DataSourceType.LOCAL.name(),localDataSource());
            dynamicDataSource.setTargetDataSources(dataSourceMap);
            return dynamicDataSource;
        }
    
        /**
         * 配置@Transactional注解事务
         * @return
         */
        @Bean
        public PlatformTransactionManager transactionManager() {
            return new DataSourceTransactionManager(dynamicDataSource());
        }
    }

    使用

    在 DynamicDataSourceAspect.java 中配置的service下使用注解的方式指定执行的方法使用哪个数据库。示例参考下方代码:

    使用示例
    primary数据库
    local
  • 相关阅读:
    Spring 让 LOB 数据操作变得简单易行
    让Apache Shiro保护你的应用
    MongoDB、Java及ORM
    Spring 的优秀工具类盘点,第 1 部分: 文件资源操作和 Web 相关工具类
    Spring 的优秀工具类盘点,第 2 部分: 特殊字符转义和方法入参检测工具类
    SpringMVC:上传与下载
    Web数据挖掘在电子商务中的应用
    Mongodb快速入门之使用Java操作Mongodb
    Mongodb数据库入门之Spring Mongodb
    基于综合兴趣度的协同过滤推荐算法
  • 原文地址:https://www.cnblogs.com/chxuan/p/12389075.html
Copyright © 2011-2022 走看看