原文链接: 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. 6)PHP,预定义变量

    预定义变量也叫超全局变量: :预定义变量又叫超全局变量,包括: $_GET, $_POST, $_SERVER, $_REQUEST, $GLOBALS, $_COOKIE, $_SESSION, . ...

  2. ZZJ_淘淘商城项目:day04(淘淘商城03 - 前台系统搭建、实现、内容管理系统实现)

    1.   今日大纲 1.  实现商品的编辑 2.  实现商品的规格参数功能 3.  搭建前台系统 4.  实现首页商品类目的显示 2.2.4.   未实现TODO 编辑时图片回显: 思路: 1.  查 ...

  3. windows系统安装msi文件总提示2502、2503的错误

    首先: 1.按WIN+R,在运行框中输入“gpedit.msc” 确认:2.打开本地策略组编辑器后依次展开 :“计算机配置”->“管理模板”->“windows组件”->“windo ...

  4. redis维护节点常用操作

    维护节点 添加主节点 hash槽重新分配 添加从节点 删除结点 1 添加主节点 集群创建成功后可以向集群中添加节点,下面是添加一个master主节点 添加7007结点作为新节点 执行命令:./redi ...

  5. 吴裕雄--天生自然python Google深度学习框架:MNIST数字识别问题

    import tensorflow as tf from tensorflow.examples.tutorials.mnist import input_data INPUT_NODE = 784 ...

  6. ubuntu .bashrc文件添加jdk后无法登录的解决方案

    1. 快捷键(ctl-alt-f2)进入虚拟终端 2. 执行export PATH=/bin:/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/ ...

  7. hybrid|Conform the norm of|Mollusk|uncanny|canny|Canvas|documentary

    hybrid混合物 Conform the norm of 符合规范 Mollusk贝类 uncanny诡异的 canny精明的 Canvas帆布 documentary纪录片  

  8. 网页元素检测工具:Spy_for_InternetExplorer下载地址

    本工具用于实时查看IE浏览器中打开的网页中元素的信息.支持iframe.frame框架. 下载地址: Spy_for_InternetExplorer.rar

  9. 存储映射I/O函数

    1.void  * mmap((void *addr, size_t length, int prot, int flags, int fd, off_t offset) 参数: addr:用于指定映 ...

  10. F5 BIG-IP之二 LTM实验一