经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » Java相关 » Spring » 查看文章
SpringBoot 这么实现动态数据源切换,就很丝滑!
来源:cnblogs  作者:程序员小富  时间:2023/12/26 9:52:26  对本文有异议

大家好,我是小富~

简介

项目开发中经常会遇到多数据源同时使用的场景,比如冷热数据的查询等情况,我们可以使用类似现成的工具包来解决问题,但在多数据源的使用中通常伴随着定制化的业务,所以一般的公司还是会自行实现多数据源切换的功能,接下来一起使用实现自定义注解的形式来实现一下。

基础配置

yml配置

pom.xml文件引入必要的Jar

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5. <modelVersion>4.0.0</modelVersion>
  6. <parent>
  7. <groupId>org.springframework.boot</groupId>
  8. <artifactId>spring-boot-starter-parent</artifactId>
  9. <version>2.7.6</version>
  10. </parent>
  11. <groupId>com.dynamic</groupId>
  12. <artifactId>springboot-dynamic-datasource</artifactId>
  13. <version>0.0.1-SNAPSHOT</version>
  14. <properties>
  15. <maven.compiler.source>8</maven.compiler.source>
  16. <maven.compiler.target>8</maven.compiler.target>
  17. <mybatis.plus.version>3.5.3.1</mybatis.plus.version>
  18. <mysql.connector.version>8.0.32</mysql.connector.version>
  19. <druid.version>1.2.6</druid.version>
  20. </properties>
  21. <dependencies>
  22. <dependency>
  23. <groupId>org.springframework.boot</groupId>
  24. <artifactId>spring-boot-starter</artifactId>
  25. </dependency>
  26. <!-- springboot核心包 -->
  27. <dependency>
  28. <groupId>org.springframework.boot</groupId>
  29. <artifactId>spring-boot-starter-web</artifactId>
  30. </dependency>
  31. <!-- mysql驱动包 -->
  32. <dependency>
  33. <groupId>com.mysql</groupId>
  34. <artifactId>mysql-connector-j</artifactId>
  35. <version>${mysql.connector.version}</version>
  36. </dependency>
  37. <!-- lombok工具包 -->
  38. <dependency>
  39. <groupId>org.projectlombok</groupId>
  40. <artifactId>lombok</artifactId>
  41. <optional>true</optional>
  42. </dependency>
  43. <!-- MyBatis Plus -->
  44. <dependency>
  45. <groupId>com.baomidou</groupId>
  46. <artifactId>mybatis-plus-boot-starter</artifactId>
  47. <version>${mybatis.plus.version}</version>
  48. </dependency>
  49. <!-- druid -->
  50. <dependency>
  51. <groupId>com.alibaba</groupId>
  52. <artifactId>druid-spring-boot-starter</artifactId>
  53. <version>${druid.version}</version>
  54. </dependency>
  55. <dependency>
  56. <groupId>org.apache.commons</groupId>
  57. <artifactId>commons-lang3</artifactId>
  58. <version>3.7</version>
  59. </dependency>
  60. <dependency>
  61. <groupId>org.aspectj</groupId>
  62. <artifactId>aspectjweaver</artifactId>
  63. </dependency>
  64. </dependencies>
  65. <build>
  66. <plugins>
  67. <plugin>
  68. <groupId>org.springframework.boot</groupId>
  69. <artifactId>spring-boot-maven-plugin</artifactId>
  70. </plugin>
  71. </plugins>
  72. </build>
  73. </project>

管理数据源

我们应用ThreadLocal来管理数据源信息,通过其中内容的get,set,remove方法来获取、设置、删除当前线程对应的数据源。

  1. /**
  2. * ThreadLocal存放数据源变量
  3. *
  4. * @author 公众号:程序员小富
  5. * @date 2023/11/27 11:02
  6. */
  7. public class DataSourceContextHolder {
  8. private static final ThreadLocal<String> DATASOURCE_HOLDER = new ThreadLocal<>();
  9. /**
  10. * 获取当前线程的数据源
  11. *
  12. * @return 数据源名称
  13. */
  14. public static String getDataSource() {
  15. return DATASOURCE_HOLDER.get();
  16. }
  17. /**
  18. * 设置数据源
  19. *
  20. * @param dataSourceName 数据源名称
  21. */
  22. public static void setDataSource(String dataSourceName) {
  23. DATASOURCE_HOLDER.set(dataSourceName);
  24. }
  25. /**
  26. * 删除当前数据源
  27. */
  28. public static void removeDataSource() {
  29. DATASOURCE_HOLDER.remove();
  30. }
  31. }

重置数据源

创建 DynamicDataSource 类并继承 AbstractRoutingDataSource,这样我们就可以重置当前的数据库路由,实现切换成想要执行的目标数据库。

  1. import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
  2. import javax.sql.DataSource;
  3. import java.util.Map;
  4. /**
  5. * 重置当前的数据库路由,实现切换成想要执行的目标数据库
  6. *
  7. * @author 公众号:程序员小富
  8. * @date 2023/11/27 11:02
  9. */
  10. public class DynamicDataSource extends AbstractRoutingDataSource {
  11. public DynamicDataSource(DataSource defaultDataSource, Map<Object, Object> targetDataSources) {
  12. super.setDefaultTargetDataSource(defaultDataSource);
  13. super.setTargetDataSources(targetDataSources);
  14. }
  15. /**
  16. * 这一步是关键,获取注册的数据源信息
  17. * @return
  18. */
  19. @Override
  20. protected Object determineCurrentLookupKey() {
  21. return DataSourceContextHolder.getDataSource();
  22. }
  23. }

配置数据库

在 application.yml 中配置数据库信息,使用dynamic_datasource_1dynamic_datasource_2两个数据库

  1. spring:
  2. datasource:
  3. type: com.alibaba.druid.pool.DruidDataSource
  4. druid:
  5. master:
  6. url: jdbc:mysql://127.0.0.1:3306/dynamic_datasource_1?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
  7. username: root
  8. password: 12345
  9. driver-class-name: com.mysql.cj.jdbc.Driver
  10. slave:
  11. url: jdbc:mysql://127.0.0.1:3306/dynamic_datasource_2?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
  12. username: root
  13. password: 12345
  14. driver-class-name: com.mysql.cj.jdbc.Driver

再将多个数据源注册到DataSource.

  1. import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
  2. import org.springframework.boot.context.properties.ConfigurationProperties;
  3. import org.springframework.context.annotation.Bean;
  4. import org.springframework.context.annotation.Configuration;
  5. import org.springframework.context.annotation.Primary;
  6. import javax.sql.DataSource;
  7. import java.util.HashMap;
  8. import java.util.Map;
  9. /**
  10. * 注册多个数据源
  11. *
  12. * @author 公众号:程序员小富
  13. * @date 2023/11/27 11:02
  14. */
  15. @Configuration
  16. public class DateSourceConfig {
  17. @Bean
  18. @ConfigurationProperties("spring.datasource.druid.master")
  19. public DataSource dynamicDatasourceMaster() {
  20. return DruidDataSourceBuilder.create().build();
  21. }
  22. @Bean
  23. @ConfigurationProperties("spring.datasource.druid.slave")
  24. public DataSource dynamicDatasourceSlave() {
  25. return DruidDataSourceBuilder.create().build();
  26. }
  27. @Bean(name = "dynamicDataSource")
  28. @Primary
  29. public DynamicDataSource createDynamicDataSource() {
  30. Map<Object, Object> dataSourceMap = new HashMap<>();
  31. // 设置默认的数据源为Master
  32. DataSource defaultDataSource = dynamicDatasourceMaster();
  33. dataSourceMap.put("master", defaultDataSource);
  34. dataSourceMap.put("slave", dynamicDatasourceSlave());
  35. return new DynamicDataSource(defaultDataSource, dataSourceMap);
  36. }
  37. }

启动类配置

在启动类的@SpringBootApplication注解中排除DataSourceAutoConfiguration,否则会报错。

  1. @SpringBootApplication(exclude = DataSourceAutoConfiguration.class)

到这多数据源的基础配置就结束了,接下来测试一下

测试切换

准备SQL

创建两个库dynamic_datasource_1、dynamic_datasource_2,库中均创建同一张表 t_dynamic_datasource_data。

  1. CREATE TABLE `t_dynamic_datasource_data` (
  2. `id` bigint(20) NOT NULL AUTO_INCREMENT,
  3. `source_name` varchar(255) DEFAULT NULL,
  4. PRIMARY KEY (`id`)
  5. );

dynamic_datasource_1.t_dynamic_datasource_data表中插入

  1. insert into t_dynamic_datasource_data (source_name) value ('dynamic_datasource_master');

dynamic_datasource_2.t_dynamic_datasource_data表中插入

  1. insert into t_dynamic_datasource_data (source_name) value ('dynamic_datasource_slave');

手动切换数据源

这里我准备了一个接口来验证,传入的 datasourceName 参数值就是刚刚注册的数据源的key。

  1. /**
  2. * 动态数据源切换
  3. *
  4. * @author 公众号:程序员小富
  5. * @date 2023/11/27 11:02
  6. */
  7. @RestController
  8. public class DynamicSwitchController {
  9. @Resource
  10. private DynamicDatasourceDataMapper dynamicDatasourceDataMapper;
  11. @GetMapping("/switchDataSource/{datasourceName}")
  12. public String switchDataSource(@PathVariable("datasourceName") String datasourceName) {
  13. DataSourceContextHolder.setDataSource(datasourceName);
  14. DynamicDatasourceData dynamicDatasourceData = dynamicDatasourceDataMapper.selectOne(null);
  15. DataSourceContextHolder.removeDataSource();
  16. return dynamicDatasourceData.getSourceName();
  17. }
  18. }

传入参数master时:127.0.0.1:9004/switchDataSource/master

传入参数slave时:127.0.0.1:9004/switchDataSource/slave

通过执行结果,我们看到传递不同的数据源名称,已经实现了查询对应的数据库数据。

注解切换数据源

上边已经成功实现了手动切换数据源,但这种方式顶多算是半自动,下边我们来使用注解方式实现动态切换。

定义注解

我们先定一个名为DS的注解,作用域为METHOD方法上,由于@DS中设置的默认值是:master,因此在调用主数据源时,可以不用进行传值。

  1. /**
  2. * 定于数据源切换注解
  3. *
  4. * @author 公众号:程序员小富
  5. * @date 2023/11/27 11:02
  6. */
  7. @Target({ElementType.METHOD, ElementType.TYPE})
  8. @Retention(RetentionPolicy.RUNTIME)
  9. @Documented
  10. @Inherited
  11. public @interface DS {
  12. // 默认数据源master
  13. String value() default "master";
  14. }

实现AOP

定义了@DS注解后,紧接着实现注解的AOP逻辑,拿到注解传递值,然后设置当前线程的数据源

  1. import com.dynamic.config.DataSourceContextHolder;
  2. import lombok.extern.slf4j.Slf4j;
  3. import org.aspectj.lang.ProceedingJoinPoint;
  4. import org.aspectj.lang.annotation.Around;
  5. import org.aspectj.lang.annotation.Aspect;
  6. import org.aspectj.lang.annotation.Pointcut;
  7. import org.aspectj.lang.reflect.MethodSignature;
  8. import org.springframework.stereotype.Component;
  9. import java.lang.reflect.Method;
  10. import java.util.Objects;
  11. /**
  12. * 实现@DS注解的AOP切面
  13. *
  14. * @author 公众号:程序员小富
  15. * @date 2023/11/27 11:02
  16. */
  17. @Aspect
  18. @Component
  19. @Slf4j
  20. public class DSAspect {
  21. @Pointcut("@annotation(com.dynamic.aspect.DS)")
  22. public void dynamicDataSource() {
  23. }
  24. @Around("dynamicDataSource()")
  25. public Object datasourceAround(ProceedingJoinPoint point) throws Throwable {
  26. MethodSignature signature = (MethodSignature) point.getSignature();
  27. Method method = signature.getMethod();
  28. DS ds = method.getAnnotation(DS.class);
  29. if (Objects.nonNull(ds)) {
  30. DataSourceContextHolder.setDataSource(ds.value());
  31. }
  32. try {
  33. return point.proceed();
  34. } finally {
  35. DataSourceContextHolder.removeDataSource();
  36. }
  37. }
  38. }

测试注解

再添加两个接口测试,使用@DS注解标注,使用不同的数据源名称,内部执行相同的查询条件,看看结果如何?

  1. @DS(value = "master")
  2. @GetMapping("/dbMaster")
  3. public String dbMaster() {
  4. DynamicDatasourceData dynamicDatasourceData = dynamicDatasourceDataMapper.selectOne(null);
  5. return dynamicDatasourceData.getSourceName();
  6. }

  1. @DS(value = "slave")
  2. @GetMapping("/dbSlave")
  3. public String dbSlave() {
  4. DynamicDatasourceData dynamicDatasourceData = dynamicDatasourceDataMapper.selectOne(null);
  5. return dynamicDatasourceData.getSourceName();
  6. }

通过执行结果,看到通过应用@DS注解也成功的进行了数据源的切换。

事务管理

在动态切换数据源的时候有一个问题是要考虑的,那就是事务管理是否还会生效呢?

我们做个测试,新增一个接口分别插入两条记录,其中在插入第二条数据时将值设置超过了字段长度限制,会产生Data too long for column异常。

  1. /**
  2. * 验证一下事物控制
  3. */
  4. // @Transactional(rollbackFor = Exception.class)
  5. @DS(value = "slave")
  6. @GetMapping("/dbTestTransactional")
  7. public void dbTestTransactional() {
  8. DynamicDatasourceData datasourceData = new DynamicDatasourceData();
  9. datasourceData.setSourceName("test");
  10. dynamicDatasourceDataMapper.insert(datasourceData);
  11. DynamicDatasourceData datasourceData1 = new DynamicDatasourceData();
  12. datasourceData1.setSourceName("testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttest");
  13. dynamicDatasourceDataMapper.insert(datasourceData1);
  14. }

经过测试发现执行结果如下,即便实现动态切换数据源,本地事务依然可以生效。

  • 不加上@Transactional注解第一条记录可以插入,第二条插入失败

  • 加上@Transactional注解两条记录都不会插入成功

本文案例地址:https://github.com/chengxy-nds/Springboot-Notebook/tree/master/springboot101/通用功能/springboot-config-order

原文链接:https://www.cnblogs.com/chengxy-nds/p/17926002.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号