SpringBoot动态多数据源的优雅实现

简介: SpringBoot动态多数据源的优雅实现

前言

也是偶然的机会,跟一个小伙伴聊了下多数据源、灵活切换数据源的问题。那么,就想起来了,好久前使用过的一款开源的快速开发框架renren框架。其中,正好有一个动态数据源模块。个人,觉得设计的还是不错,采用了Spring的AOP切面,来切入数据源配置。实现原理比较简单,但是很易用。今天,就来跟大家解读下,其内部的实现。

配置

首先,我们简单粗暴地看一下使用该组件,如何配置。

pom文件,引入对应的依赖模块,就完成了对于切面的引入

<dependency>
   <groupId>io.renren</groupId>
   <artifactId>renren-dynamic-datasource</artifactId>
   <version>4.0.0</version>
</dependency>

在需要进入多数据源的项目配置文件中,配置数据源内容

spring:
datasource:
  type: com.alibaba.druid.pool.DruidDataSource
  druid:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/renren?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: root
    password: cs123456
    initial-size: 10
    max-active: 100
    min-idle: 10
    max-wait: 60000
    pool-prepared-statements: true
    max-pool-prepared-statement-per-connection-size: 20
    time-between-eviction-runs-millis: 60000
    min-evictable-idle-time-millis: 300000
     #Oracle需要打开注释
     #validation-query: SELECT 1 FROM DUAL
    test-while-idle: true
    test-on-borrow: false
    test-on-return: false
    stat-view-servlet:
      enabled: true
      url-pattern: /druid/*
       #login-username: admin
       #login-password: admin
    filter:
      stat:
        log-slow-sql: true
        slow-sql-millis: 1000
        merge-sql: false
      wall:
        config:
          multi-statement-allow: true
##多数据源的配置,需要引用renren-dynamic-datasource
dynamic:
datasource:
# 从数据源名称
  slave1:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/renren1?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true
    username: iicp_opt_app
    password: ds#123456
  slave2:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/renren2?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true
    username: iicp_opt_app
    password: ds#123456

主数据源,可以详细配置连接池信息

从数据,无需配置连接池信息,默认已经实现,采用DruidDataSource

上述,内容完成了,完整的配置。

应用

该动态数据源模块,设计为对于确切的服务,实现切面,切换服务内,数据源的灵活使用。所以落脚点,在服务类上。

默认数据源实现如下

@Service
public class TestServiceImpl implements TestService {
// bussiness code
}

动态数据源实现如下

@Service
@DataSource("slave1")
public class TestServiceImpl implements TestService {
// bussiness code
}

经过注解@DataSource("数据源名称") 来实现服务内数据源绑定。

从以上示例看,使用非常简单。

本文,主要来剖析实现,因此,我们不再演示实现效果,感兴趣的可以自行测试下。

实现原理剖析

数据源模块包括如下内容:

image.png

首先解析下切面的实现,DataSourceAspect

@Aspect
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class DataSourceAspect {
   protected Logger logger = LoggerFactory.getLogger(getClass());
   @Pointcut("@annotation(io.renren.commons.dynamic.datasource.annotation.DataSource) " +
           "|| @within(io.renren.commons.dynamic.datasource.annotation.DataSource)")
   public void dataSourcePointCut() {
  }
   @Around("dataSourcePointCut()")
   public Object around(ProceedingJoinPoint point) throws Throwable {
       // 完成注解内容获取
       MethodSignature signature = (MethodSignature) point.getSignature();
       Class targetClass = point.getTarget().getClass();
       Method method = signature.getMethod();
       DataSource targetDataSource = (DataSource)targetClass.getAnnotation(DataSource.class);
       DataSource methodDataSource = method.getAnnotation(DataSource.class);
       // 方法注解为最小粒度,确定数据源内容
       if(targetDataSource != null || methodDataSource != null){
           String value;
           if(methodDataSource != null){
               value = methodDataSource.value();
          }else {
               value = targetDataSource.value();
          }
// 存储数据源
           DynamicContextHolder.push(value);
           logger.debug("set datasource is {}", value);
      }
       try {
           return point.proceed();
      } finally {
           DynamicContextHolder.poll();
           logger.debug("clean datasource");
      }
  }
}

观察DynamicContextHolder 对于数据源存储的实现

public class DynamicContextHolder {
   @SuppressWarnings("unchecked")
   private static final ThreadLocal<Deque<String>> CONTEXT_HOLDER = new ThreadLocal() {
       @Override
       protected Object initialValue() {
           return new ArrayDeque();
      }
  };
   /**
    * 获得当前线程数据源
    *
    * @return 数据源名称
    */
   public static String peek() {
       return CONTEXT_HOLDER.get().peek();
  }
   /**
    * 设置当前线程数据源
    *
    * @param dataSource 数据源名称
    */
   public static void push(String dataSource) {
       CONTEXT_HOLDER.get().push(dataSource);
  }
   /**
    * 清空当前线程数据源
    */
   public static void poll() {
       Deque<String> deque = CONTEXT_HOLDER.get();
       deque.poll();
       if (deque.isEmpty()) {
           CONTEXT_HOLDER.remove();
      }
  }
}

从上述代码,我们可以看到,采用了ThreadLocal,线程本地变量,来实现存储当前线程数据源内容,以及做了及时的清空。

对于,多数据源实际实现,采用了Spring框架的AbstractRoutingDataSource

public class DynamicDataSource extends AbstractRoutingDataSource {
   @Override
   protected Object determineCurrentLookupKey() {
       return DynamicContextHolder.peek();
  }
}

那么,有了数据源,我们需要数据源工厂进行相关的引入和配置

@Configuration
@EnableConfigurationProperties(DynamicDataSourceProperties.class)
public class DynamicDataSourceConfig {
   @Autowired
   private DynamicDataSourceProperties properties;
   @Bean
   @ConfigurationProperties(prefix = "spring.datasource.druid")
   public DataSourceProperties dataSourceProperties() {
       return new DataSourceProperties();
  }
   @Bean
   public DynamicDataSource dynamicDataSource(DataSourceProperties dataSourceProperties) {
       DynamicDataSource dynamicDataSource = new DynamicDataSource();
       dynamicDataSource.setTargetDataSources(getDynamicDataSource());
       //默认数据源
       DruidDataSource defaultDataSource = DynamicDataSourceFactory.buildDruidDataSource(dataSourceProperties);
       dynamicDataSource.setDefaultTargetDataSource(defaultDataSource);
       return dynamicDataSource;
  }
   private Map<Object, Object> getDynamicDataSource(){
       Map<String, DataSourceProperties> dataSourcePropertiesMap = properties.getDatasource();
       Map<Object, Object> targetDataSources = new HashMap<>(dataSourcePropertiesMap.size());
       dataSourcePropertiesMap.forEach((k, v) -> {
           DruidDataSource druidDataSource = DynamicDataSourceFactory.buildDruidDataSource(v);
           targetDataSources.put(k, druidDataSource);
      });
       return targetDataSources;
  }
}
public class DynamicDataSourceFactory {
   public static DruidDataSource buildDruidDataSource(DataSourceProperties properties) {
       DruidDataSource druidDataSource = new DruidDataSource();
       druidDataSource.setDriverClassName(properties.getDriverClassName());
       druidDataSource.setUrl(properties.getUrl());
       druidDataSource.setUsername(properties.getUsername());
       druidDataSource.setPassword(properties.getPassword());
       druidDataSource.setInitialSize(properties.getInitialSize());
       druidDataSource.setMaxActive(properties.getMaxActive());
       druidDataSource.setMinIdle(properties.getMinIdle());
       druidDataSource.setMaxWait(properties.getMaxWait());
       druidDataSource.setTimeBetweenEvictionRunsMillis(properties.getTimeBetweenEvictionRunsMillis());
       druidDataSource.setMinEvictableIdleTimeMillis(properties.getMinEvictableIdleTimeMillis());
       druidDataSource.setMaxEvictableIdleTimeMillis(properties.getMaxEvictableIdleTimeMillis());
       druidDataSource.setValidationQuery(properties.getValidationQuery());
       druidDataSource.setValidationQueryTimeout(properties.getValidationQueryTimeout());
       druidDataSource.setTestOnBorrow(properties.isTestOnBorrow());
       druidDataSource.setTestOnReturn(properties.isTestOnReturn());
       druidDataSource.setPoolPreparedStatements(properties.isPoolPreparedStatements());
       druidDataSource.setMaxOpenPreparedStatements(properties.getMaxOpenPreparedStatements());
       druidDataSource.setSharePreparedStatements(properties.isSharePreparedStatements());
       try {
           druidDataSource.setFilters(properties.getFilters());
           druidDataSource.init();
      } catch (SQLException e) {
           e.printStackTrace();
      }
       return druidDataSource;
  }
}

关于具体的属性,我们不再细看了。

以上,就是该数据源模块的简单实现。涉及到的内容不是很复杂,但是对于重复的轮子来说,很有参考价值;对于有使用需求的小伙伴,也是很易用。

对于有更复杂的需求,我们可以自行扩展使用。

总结

简单易用的特点,我觉得挺有思考和使用价值,故而分析一下,给大家所分享。作为快速开发框架,有一定的优越性,也值得我们去学习。

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。 &nbsp; 相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情:&nbsp;https://www.aliyun.com/product/rds/mysql&nbsp;
目录
相关文章
|
Java 数据库连接
SpringBoot配置多数据源实战
第四届光学与机器视觉国际学术会议(ICOMV 2025) 2025 4th International Conference on Optics and Machine Vision
1010 8
|
Java
SpringBoot 配置多数据源
SpringBoot 配置多数据源
319 0
|
开发框架 Java 数据库
Spring Boot集成多数据源的最佳实践
Spring Boot集成多数据源的最佳实践
|
开发框架 Java 数据库
Spring Boot集成多数据源的最佳实践
Spring Boot集成多数据源的最佳实践
|
Java 数据管理 关系型数据库
Spring Boot中实现多数据源配置
Spring Boot中实现多数据源配置
|
JavaScript Java 测试技术
基于SpringBoot+Vue的宝鸡文理学院学生成绩动态追踪系统的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue的宝鸡文理学院学生成绩动态追踪系统的详细设计和实现(源码+lw+部署文档+讲解等)
219 0
|
Java 应用服务中间件 容器
手写SpringBoot(二)之动态切换Servlet容器
我们在切换serlvet容器的时候,会将SpringBoot默认的tomcat jar包给排除掉,换上我们需要的jar包,比如jetty。
282 0
|
关系型数据库 数据库 数据安全/隐私保护
springboot+dynamic-datasource多数据源配置动态切换
springboot+dynamic-datasource多数据源配置动态切换
6346 0
|
druid Java 关系型数据库
Spring Boot2 系列教程(二十五)Spring Boot 整合 Jpa 多数据源
Spring Boot2 系列教程(二十五)Spring Boot 整合 Jpa 多数据源
1723 0
|
druid Java 关系型数据库
在Spring Boot中集成Druid实现多数据源有两种常用的方式:使用Spring Boot的自动配置和手动配置。
在Spring Boot中集成Druid实现多数据源有两种常用的方式:使用Spring Boot的自动配置和手动配置。
4026 5