基于 Spring Boot 2.x、Spring Data JPA、druid、mysql 的动态数据源配置Demo,适合用于数据库的读写分离等应用场景。通过在Service层方法上添加自定义注解实现读写不同的数据库。 同时解决了不能在同一个Service中切换多个数据源的问题。
配置文件已配置好druid监控相关属性,监控页面链接:ip:8080/druid。账号:admin,密码:123456。详情查看 application.yml 文件。
注意事项(前言)
在网上有很多关于动态切换数据源的配置教程,其中百分之九十的都是基于 Mybatis 的。当然也有零星的几篇基于 Spring Data JPA 的配置教程,不过当你按着这些教程使用后就会发现靠谱一点的还可以做到不同的请求可以使用不同的数据源,但是无法做到在同一个请求内进行多个数据源之间的切换。在业务逻辑相对复杂的情况下肯定是不能满足需求的。
那么是什么原因导致在同一请求内切换数据源失败呢?经过单步调试和查看日志发现自己写的注解确实生效了,只不过在第二次切换数据源时没有执行 AbstractRoutingDataSource
的 determineCurrentLookupKey()
的方法而是直接拿到了数据库连接去执行了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 中切换操作不同的数据源读写数据。
注意:如果在一次请求中通过数据源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 javax.persistence.EntityManager; import java.lang.reflect.Method; /** * @Author: ChangXuan * @Decription: * @Date: 22:28 2020/2/23 **/ @Component @Aspect public class DynamicDataSourceAspect { @Autowired private EntityManager entityManager; @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()); SessionImplementor session = entityManager.unwrap(SessionImplementor.class); //最关键的一句代码, 手动断开连接,不用重新设置 ,会自动重新设置连接。 session.disconnect(); } 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下使用注解的方式指定执行的方法使用哪个数据库。示例参考下方代码: