原文链接: 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 Boot2.x 动态数据源配置的更多相关文章

  1. Druid动态数据源配置

    上文已经讲了单个数据源的Druid的配置(http://www.cnblogs.com/nbfujx/p/7686634.html) Druid动态数据源配置 主要是继承AbstractRouting ...

  2. Springboot+Druid 动态数据源配置监控

    一.引入maven依赖,使用 starter 与原生 druid 依赖配置有所不同 <dependency> <groupId>com.alibaba</groupId& ...

  3. Spring Boot + Mybatis多数据源和动态数据源配置

    文章转自 https://blog.csdn.net/neosmith/article/details/61202084 网上的文章基本上都是只有多数据源或只有动态数据源,而最近的项目需要同时使用两种 ...

  4. Spring Boot2.4双数据源的配置

    相较于单数据源,双数据源配置有时候在数据分库的时候可能更加有利 但是在参考诸多博客以及书籍(汪云飞的实战书)的时候,发现对于spring boot1.X是完全没问题的,一旦切换到spring boot ...

  5. Spring数据访问1 - 数据源配置及数据库连接池的概念

    无论你要选择哪种数据访问方式,首先你都需要配置好数据源引用. Spring中配置数据源的几种方式 通过在JDBC驱动程序定义的数据源: 通过JNDI查找的数据源: 连接池的数据源: 对于即将发布到生产 ...

  6. Spring(AbstractRoutingDataSource)实现动态数据源切换--转载

    原始出处:http://linhongyu.blog.51cto.com/6373370/1615895 一.前言 近期一项目A需实现数据同步到另一项目B数据库中,在不改变B项目的情况下,只好选择项目 ...

  7. dubbo服务+Spring事务+AOP动态数据源切换 出错

    1:问题描述,以及分析 项目用了spring数据源动态切换,服务用的是dubbo.在运行一段时间后程序异常,更新操作没有切换到主库上. 这个问题在先调用读操作后再调用写操作会出现. 经日志分析原因: ...

  8. Spring(AbstractRoutingDataSource)实现动态数据源切换

    转自: http://blog.51cto.com/linhongyu/1615895 一.前言 近期一项目A需实现数据同步到另一项目B数据库中,在不改变B项目的情况下,只好选择项目A中切换数据源,直 ...

  9. spring boot mybatis 多数据源配置

    package com.xynet.statistics.config.dataresources; import org.springframework.jdbc.datasource.lookup ...

随机推荐

  1. 17.3.13--python编码问题

    1----字符编码: 字符编码(英语:Character encoding).字集码是把字符集中的字符编码为指定集合中某一对象(例如:比特模式.自然数串行.8位组或者电脉冲),以便文本在计算机中存储和 ...

  2. 吴裕雄--天生自然 PYTHON3开发学习:函数

    def 函数名(参数列表): 函数体 # 计算面积函数 def area(width, height): return width * height def print_welcome(name): ...

  3. android 获得存储设备状态

    1.获取存储器总大小,可用大小 File path= Environment.getExternalStorageDirectory();StatFs fs = new StatFs(path.get ...

  4. Web 手工测试

    day 1 学习目标: 熟练搭建本地测试环境 掌握熟悉项目的步骤和内容 掌握项目基本的测试流程 基础环境介绍: 项目环境的组成部分: 操作系统 windows win7 win10 Linux Cen ...

  5. 三十六、www服务nginx介绍

    一.Nginx介绍 ,相对于LAMP经典组合而言,LNMP是近几年来流行的组合.(linux+nginx+mysql+php) Nginx是一个开源www服务软件,是俄罗斯人开发的,本身是一款静态ww ...

  6. 允许外部访问Windows本机的指定端口

    背景:目前公司有一台公网Windows服务器,有公网IP和内网IP,防火墙已开启 需求:9999端口需要对外开放 方案:在防火墙的入站规则里添加一条规则,使外部能够访问9999端口 问题:添加好规则后 ...

  7. MySQL数据库优化、设计与高级应用

    MySQL数据库优化主要涉及两个方面,一方面是对SQL语句优化,另一方面是对数据库服务器和数据库配置的优化. 数据库优化 SQL语句优化 为了更好的看到SQL语句执行效率的差异,建议创建几个结构复杂的 ...

  8. css3 transform 变形属性详解

    本文主要介绍了css3 属性transform的相关内容,针对CSS3变形.CSS3转换.CSS3旋转.CSS3缩放.扭曲和矩阵做了详细的讲解.希望对你有所帮助. 这个很简单,就跟border-rad ...

  9. 接受H0的坏处|试验误差|置信度由来|

    生物统计与实验设计 置信度(0.05 0.01)是通过实验次数估计值的分布得到的,它是整个分布的期望,这个值的确立需要具体情况具体分析. 肯定很难,因为否定一次很容易.虽然如果没有否定(eg:得到p= ...

  10. [LC] 17. Letter Combinations of a Phone Number

    Given a string containing digits from 2-9 inclusive, return all possible letter combinations that th ...