在一个项目中使用多个数据源的情况很多,所以动态切换数据源是项目中标配的功能,当然网上有相关的依赖可以使用,比如动态数据源,其依赖为,

  1. <dependency>
  2. <groupId>com.baomidou</groupId>
  3. <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
  4. <version>3.5.1</version>
  5. </dependency>

  今天,不使用现成的API,手动实现一个动态数据源。

一、环境及依赖

springboot、mybatis-plus的基础上实现动态数据源切换,

springboot:2.3.3.RELEASE

mybatis-plus-boot-starter:3.5.0

mysql驱动:8.0.32

除了这些依赖外没有其他的,目标是动态切换数据源。

二、实现思路

  先来看下,单数据源的情况。

  在使用springboot和mybatis-plus时,我们没有配置数据源(DataSource),只配置了数据库相关的信息,便可以连接数据库进行数据库的操作,这是为什么呐。其实是基于spring-boot的自动配置,也就是autoConfiguration,在自动配置下有DataSourceAutoConfiguration类,该类会生成一个数据源并注入到spring的容器中,这样就可以使用该数据源提供的连接,访问数据库了。

  感兴趣的小伙伴可以了解下这个类的具体实现逻辑。

  要实现多数据源,并且可以自动切换。那么肯定就不能再使用DataSourceAutoConfigurtation了,因为它只能产生一个数据源,多个数据源要怎么办,spring提供了AbstractRoutingDataSource类,该类是一个抽象类,仅有一个抽象方法需要实现

  1. Determine the current lookup key. This will typically be implemented to check a thread-bound transaction context.
  2. Allows for arbitrary keys. The returned key needs to match the stored lookup key type,
  3. as resolved by the resolveSpecifiedLookupKey method.
  4. @Nullable
  5. protected abstract Object determineCurrentLookupKey();

可以根据该类实现一个动态数据源。好了,现在了解了实现思路,开始实现一个动态数据源,要做以下的准备工作。

1、配置文件;

2、自定义动态数据源;

2.1、配置文件

由于是多数据源,那么在配置文件中肯定是多个配置,不能再是一个数据库的配置了,这里使用两个mysql的配置进行演示,

  1. #master 默认数据源
  2. spring:
  3. datasource:
  4. master:
  5. driver-class-name: com.mysql.cj.jdbc.Driver
  6. jdbc-url: jdbc:mysql://127.0.0.1:3306/test?serverTimezone=GMT%2B8&autoReconnect=true&allowMultiQueries=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false
  7. username: root
  8. password: 123456
  9. #slave 从数据源
  10. slave:
  11. driver-class-name: com.mysql.cj.jdbc.Driver
  12. jdbc-url: jdbc:mysql://127.0.0.1:3306/test2?serverTimezone=GMT%2B8&autoReconnect=true&allowMultiQueries=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false
  13. username: root
  14. password: 123456

这里使用了一个master一个slave两个数据源配置,其地址是一致的,但数据库示例不一样。 有了数据源的信息下一步要实现自己的数据源,

2.2、自定义动态数据源

  前边说,spring提供了AbstractRoutingDataSource类可以实现动态数据源,看下实现。

DynamicDatasource.java

  1. package com.wcj.my.config.dynamic.source;
  2. import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
  3. /**
  4. * 动态数据源
  5. * @date 2023/6/8 19:18
  6. */
  7. public class DynamicDatasource extends AbstractRoutingDataSource {
  8. /**
  9. * Determine the current lookup key. This will typically be
  10. * implemented to check a thread-bound transaction context.
  11. * <p>Allows for arbitrary keys. The returned key needs
  12. * to match the stored lookup key type, as resolved by the
  13. * {@link #resolveSpecifiedLookupKey} method.
  14. */
  15. @Override
  16. protected Object determineCurrentLookupKey() {
  17. return DynamicDatasourceHolder.getDataSource();
  18. }
  19. }

这里的determineCurrentLookupKey方法,需要返回一个数据源,也就是说返回一个数据源的映射,这里返回一个DynamicDatasourceHolder.getDataSource()方法的返回值,DynamicDatasourceHolder是一个保存多个数据源的地方,

DynamicDatasourceHolder.java

  1. package com.wcj.my.config.dynamic.source;
  2. import java.util.Queue;
  3. import java.util.concurrent.ArrayBlockingQueue;
  4. /**
  5. * @date 2023/6/8 19:42
  6. */
  7. public class DynamicDatasourceHolder {
  8. //保存数据源的映射
  9. private static Queue<String> queue = new ArrayBlockingQueue<String>(1);
  10. public static String getDataSource() {
  11. return queue.peek();
  12. }
  13. public static void setDataSource(String dataSourceKey) {
  14. queue.add(dataSourceKey);
  15. }
  16. public static void removeDataSource(String dataSourceKey) {
  17. queue.remove(dataSourceKey);
  18. }
  19. }

该类很简单,使用一个队列保存数据源的映射,提供获取/设置数据源的方法。

这里使用ThreadLocal类更合适,这样可以实现线程的隔离,一个请求会有一个线程来处理,保证每隔线程使用的数据源是一样的。

到现在为止依旧没有出现如何创建多数据源,下面就来了,不着急。

DynamicDatasourceConfig.java

  1. package com.wcj.my.config.dynamic.source;
  2. import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
  3. import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
  4. import org.springframework.boot.context.properties.ConfigurationProperties;
  5. import org.springframework.boot.jdbc.DataSourceBuilder;
  6. import org.springframework.context.annotation.Bean;
  7. import org.springframework.context.annotation.Configuration;
  8. import org.springframework.context.annotation.Primary;
  9. import javax.sql.DataSource;
  10. import java.util.HashMap;
  11. import java.util.Map;
  12. /**
  13. * @date 2023/6/8 19:51
  14. */
  15. @Configuration
  16. @EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
  17. public class DynamicDatasourceConfig {
  18. @Bean("master")
  19. @ConfigurationProperties(prefix = "spring.datasource.master")
  20. public DataSource masterDatasource(){
  21. return DataSourceBuilder.create().build();
  22. }
  23. @Bean("slave")
  24. @ConfigurationProperties(prefix = "spring.datasource.slave")
  25. public DataSource slaveDatasource(){
  26. return DataSourceBuilder.create().build();
  27. }
  28. @Bean
  29. @Primary
  30. public DataSource dataSource(){
  31. Map<Object, Object> dataSourceMap = new HashMap<>(2);
  32. dataSourceMap.put("master", masterDatasource());
  33. dataSourceMap.put("slave", slaveDatasource());
  34. DynamicDatasource dynamicDatasource=new DynamicDatasource();
  35. dynamicDatasource.setTargetDataSources(dataSourceMap);
  36. dynamicDatasource.setDefaultTargetDataSource(masterDatasource());
  37. return dynamicDatasource;
  38. }
  39. }

首先,在该类上有个一个@Configuration注解,标明这是一个配置类;

其次,有一个@EnableAutonConfiguration注解,该注解中有个数组类型的exclude属性,排除不需要自动配置的类,这里排除的是当然就是DataSourceAutoConfiguration类了;因为下面会自动生成数据源,不需要自动配置了;

然后,在类中是标有@Bean的方法,这些方法便是生成数据源类,且映射为”master“、”slave“,可以有多个。使用的是DataSourceBuilder类帮助生成;

最后,生成一个DynamicDatasource,且标有@Primary注解,这里需要设置”master“、”slave“两个映射代表的数据源;

这样便向spring容器中注入了三个数据源,分别是”master“、”slave“代表的数据源,他们是需要实际使用的数据源。还有一个是DynamicDatasource,提供数据源的设置。这三个都是DataSource的子类。

三、使用多数据源

  上面已经完成了多数据源的配置,下面看怎么使用吧,还记得DynamicDatasourceHolder类中有set/get方法吗,就是使用这个类提供的方法,

UserSerivce.java

  1. package com.wcj.my.service;
  2. import com.wcj.my.config.dynamic.source.DynamicDatasourceHolder;
  3. import com.wcj.my.dto.UserDto;
  4. import com.wcj.my.entity.User;
  5. import com.wcj.my.mapper.UserMapper;
  6. import org.springframework.beans.factory.annotation.Autowired;
  7. import org.springframework.stereotype.Service;
  8. /**
  9. * @date 2023/6/8 15:19
  10. */
  11. @Service
  12. public class UserService {
  13. @Autowired
  14. private UserMapper userMapper;
  15. /**默认使用master数据源
  16. */
  17. public boolean saveUser(UserDto userDto) {
  18. User user = new User();
  19. user.setUName(userDto.getName());
  20. user.setUCode(userDto.getCode());
  21. user.setUAge(userDto.getAge());
  22. user.setUAddress(userDto.getAddress());
  23. int num = userMapper.insert(user);
  24. if (num > 0) {
  25. return true;
  26. }
  27. return false;
  28. }
  29. /**
  30. *使用slave数据源
  31. */
  32. public boolean saveUserSlave(UserDto userDto) {
  33. DynamicDatasourceHolder.setDataSource("slave");
  34. User user = new User();
  35. user.setUName(userDto.getName());
  36. user.setUCode(userDto.getCode());
  37. user.setUAge(userDto.getAge());
  38. user.setUAddress(userDto.getAddress());
  39. int num = userMapper.insert(user);
  40. DynamicDatasourceHolder.removeDataSource("slave");
  41. if (num > 0) {
  42. return true;
  43. }
  44. return false;
  45. }
  46. }

  上面的service层方法在调用dao层方法的时候,使用DynamicDatasourceHolder.setDataSource()方法设置了需要使用的数据源, 通过这样的方式便可以实现动态数据源了。

  不知道,小伙伴们有没有感觉到,这样每次在调用方法的时候都需要设置数据源是不是很麻烦,有没有一种更方面的方式,比如说注解。

四、动态数据源注解@DDS

  现在来实现一个动态数据源的注解来代替上面的每次都调用DynamicDatasourceHolder.setDataSource()方法来设置数据源。

  先看下,@DDS注解的定义

DDS.java

  1. package com.wcj.my.config.dynamic.source.aspect;
  2. import org.springframework.stereotype.Component;
  3. import java.lang.annotation.ElementType;
  4. import java.lang.annotation.Retention;
  5. import java.lang.annotation.RetentionPolicy;
  6. import java.lang.annotation.Target;
  7. /**动态数据源的注解
  8. * 用在类和方法上,方法上的优先级大于类上的
  9. * 默认值是master
  10. * @date 2023/6/9 16:19
  11. */
  12. @Target({ElementType.TYPE,ElementType.METHOD})
  13. @Retention(RetentionPolicy.RUNTIME)
  14. @Component
  15. public @interface DDS {
  16. String value() default "master";
  17. }

注解@DDS使用在类和方法上,切方法上的优先级大于类上的。有一个value的属性,指明使用的数据源,默认是”master“。

实现一个切面,来切@DDS注解

DynamicDatasourceAspect.java

  1. package com.wcj.my.config.dynamic.source.aspect;
  2. import com.wcj.my.config.dynamic.source.DynamicDatasourceHolder;
  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.util.Objects;
  10. /**
  11. * 动态数据源切面
  12. * @date 2023/6/9 16:23
  13. */
  14. @Aspect
  15. @Component
  16. public class DynamicDatasourceAspect {
  17. /**
  18. * 切点,切的是带有@DDS的注解
  19. */
  20. @Pointcut("@annotation(com.wcj.my.config.dynamic.source.aspect.DDS)")
  21. public void dynamicDatasourcePointcut(){
  22. }
  23. /**
  24. * 环绕通知
  25. * @param joinPoint
  26. * @return
  27. * @throws Throwable
  28. */
  29. @Around("dynamicDatasourcePointcut()")
  30. public Object around(ProceedingJoinPoint joinPoint)throws Throwable{
  31. String datasourceKey="master";
  32. //类上的注解
  33. Class<?> targetClass=joinPoint.getTarget().getClass();
  34. DDS annotation=targetClass.getAnnotation(DDS.class);
  35. //方法上的注解
  36. MethodSignature methodSignature=(MethodSignature)joinPoint.getSignature();
  37. DDS annotationMethod=methodSignature.getMethod().getAnnotation(DDS.class);
  38. if(Objects.nonNull(annotationMethod)){
  39. datasourceKey=annotationMethod.value();
  40. }else{
  41. datasourceKey=annotation.value();
  42. }
  43. //设置数据源
  44. DynamicDatasourceHolder.setDataSource(datasourceKey);
  45. try{
  46. return joinPoint.proceed();
  47. }finally {
  48. DynamicDatasourceHolder.removeDataSource(datasourceKey);
  49. }
  50. }
  51. }

这样一个动态数据源的注解便可以了,看下怎么使用,

UserServiceByAnnotation.java

  1. package com.wcj.my.service;
  2. import com.wcj.my.config.dynamic.source.DynamicDatasourceHolder;
  3. import com.wcj.my.config.dynamic.source.aspect.DDS;
  4. import com.wcj.my.dto.UserDto;
  5. import com.wcj.my.entity.User;
  6. import com.wcj.my.mapper.UserMapper;
  7. import org.springframework.beans.factory.annotation.Autowired;
  8. import org.springframework.stereotype.Service;
  9. /**
  10. * @date 2023/6/8 15:19
  11. */
  12. @Service
  13. public class UserServiceByAnnotation {
  14. @Autowired
  15. private UserMapper userMapper;
  16. @DDS("master")
  17. public boolean saveUser(UserDto userDto){
  18. User user=new User();
  19. user.setUName(userDto.getName());
  20. user.setUCode(userDto.getCode());
  21. user.setUAge(userDto.getAge());
  22. user.setUAddress(userDto.getAddress());
  23. int num=userMapper.insert(user);
  24. if(num>0){
  25. return true;
  26. }
  27. return false;
  28. }
  29. @DDS("slave")
  30. public boolean saveUserSlave(UserDto userDto){
  31. User user=new User();
  32. user.setUName(userDto.getName());
  33. user.setUCode(userDto.getCode());
  34. user.setUAge(userDto.getAge());
  35. user.setUAddress(userDto.getAddress());
  36. int num=userMapper.insert(user);
  37. if(num>0){
  38. return true;
  39. }
  40. return false;
  41. }
  42. }

使用起来很简单,在需要切换数据源的方法或类上使用@DDS注解即可,使用value来改变数据源就好了。

五、动态数据源的原理

  很多小伙伴可能和我有一样的疑惑,使用DynamicDatasourceHolder.setDataSource或@DDS就可以设置数据源了,是怎么实现的,下面分析下,我们指定dao层的Mapper其实是一个代理对象,其会使用mybatis中的sqlSessionTempalte进行数据库的操作,在sqlSessionTemplate中会使用DefaultSqlSession对象,最终会使用DataSource,而使用了动态数据源的对象中会注入一个DynamicDataSource,在进行数据库操作时最终会获得一个数据库连接,这里便会使用DynamicDataSource获得一个连接,由于它继承了AbstractRoutingDataSource类,看下其getConnection方法,

  1. @Override
  2. public Connection getConnection() throws SQLException {
  3. return determineTargetDataSource().getConnection();
  4. }

看下determineTargetDataSource()方法,

  1. protected DataSource determineTargetDataSource() {
  2. Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");、
  3. //自己实现的,在调用方法时进行了设置,实现动态数据源的目的
  4. Object lookupKey = determineCurrentLookupKey();
  5. DataSource dataSource = this.resolvedDataSources.get(lookupKey);
  6. if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
  7. dataSource = this.resolvedDefaultDataSource;
  8. }
  9. if (dataSource == null) {
  10. throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
  11. }
  12. return dataSource;
  13. }

看上面的注释,determineCurrentLookupkey()方法便是在DynamicDatasource类中进行了实现,从而实现了动态设置数据源的目的。

六、总结

本文动手实现了一个动态数据源,并切提供了注解的方式,主要有以下几点

1、继承AbstractRoutingDataSource类的determineCurrentLookupkey()方法,动态设置数据源;

2、取消DataSourceAutoConfiguration的自动配置,手动向spring容器中注入多个数据源;

3、基于@DDS注解动态设置数据源;

最后,本文用到的源码均可关注下方公众号获得。另外,关注公众号回复”45“可获得一份极客时间的”mysql实战45讲“,很干的干货!

花了半天时间,使用spring-boot实现动态数据源,切换自如的更多相关文章

  1. 如何通过Spring Boot配置动态数据源访问多个数据库

    之前写过一篇博客<Spring+Mybatis+Mysql搭建分布式数据库访问框架>描述如何通过Spring+Mybatis配置动态数据源访问多个数据库.但是之前的方案有一些限制(原博客中 ...

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

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

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

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

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

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

  5. spring AbstractRoutingDataSource实现动态数据源切换

    使用Spring 提供的 AbstractRoutingDataSource 实现 创建 AbstractRoutingDataSource 实现类,负责保存所有数据源与切换数据源策略:public ...

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

    [参考]Spring(AbstractRoutingDataSource)实现动态数据源切换--转载 [参考] 利用Spring的AbstractRoutingDataSource解决多数据源的问题 ...

  7. Spring Boot与多数据源那点事儿~

    持续原创输出,点击上方蓝字关注我 目录 前言 写这篇文章的目的 什么是多数据源? 何时用到多数据源? 整合单一的数据源 整合Mybatis 多数据源如何整合? 什么是动态数据源? 数据源切换如何保证线 ...

  8. Spring Boot 2 (二):Spring Boot 2 动态 Banner

    Spring Boot 2 (二):Spring Boot 2 动态 Banner Spring Boot 2.0 提供了很多新特性,其中就有一个小彩蛋:动态 Banner. 一.配置依赖 使用 Sp ...

  9. spring boot项目自定义数据源,mybatisplus分页、逻辑删除无效解决方法

    Spring Boot项目中数据源的配置可以通过两种方式实现: 1.application.yml或者application.properties配置 2.注入DataSource及SqlSessio ...

  10. Spring Boot配置多数据源并实现Druid自动切换

    原文:https://blog.csdn.net/acquaintanceship/article/details/75350653 Spring Boot配置多数据源配置yml文件主数据源配置从数据 ...

随机推荐

  1. 全面了解 Redis 高级特性,实现高性能、高可靠的数据存储和处理

    目录 高性能.高可用.高可扩展性的原理 持久化 RDB持久化 AOF持久化 持久化的配置 RDB配置 AOF配置 持久化的恢复 RDB的恢复 AOF的恢复 RDB和AOF的选择 持久化对性能的影响 数 ...

  2. 自己定义jquery插件轮播图

    轮播图-html <!DOCTYPE html> <html lang="en"> <head> <meta charset=" ...

  3. MySQL相关知识点思维导图整理

    MySQL相关知识点思维导图整理 Xmind思维导图下载地址: 蓝奏云:https://shuihan.lanzoui.com/iXZw7frkn5a

  4. xcodebuild命令行工具使用详解

    xcodebuild命令行工具使用 如何通过命令行编译ios项目? xcodebuild是一个命令行工具,允许你从命令行对Xcode项目和工作区执行编译.查询.分析.测试和归档操作.它对项目中包含的一 ...

  5. [Linux]Linux执行sh脚本时,出现$‘\r‘: command not found(未找到命令)"错误的解决方案[转载]

    1 文由 为什么要把这么一个看似很简单的问题,还要以[转载]的方式专门用博客写出来? 主要是在编写crontab的自动化定时脚本的过程中,发现是这个错导致的自动化脚本频繁执行异常时,已经花了好几个小时 ...

  6. 浅析Nordic nRF5 SDK例程架构

    很多刚接触Nordic nRF5 SDK的初学者出于对新平台的不熟悉,会觉得这个SDK很难,本文讲浅析nRF5 SDK中例程的架构,让初学者能够快速上手SDK. 在开始之前,先推荐阅读观看下面这些文章 ...

  7. 中国省市区--地区SQL表

    SET FOREIGN_KEY_CHECKS=0; -- ---------------------------- -- Table structure for rc_district -- ---- ...

  8. Go语言入门实战: 猜谜游戏+在线词典

    包含基础语法和入门Go语言的3个案例 速览基础语法 对于耳熟能详的顺序结构.分支结构(if else-if else.switch).循环结构(for)不作赘述. 数组: 长度固定的元素序列 pack ...

  9. 从 1 秒到 10 毫秒!在 APISIX 中减少 Prometheus 请求阻塞

    本文介绍了 Prometheus 插件造成长尾请求现象的原因,以及如何解决这个问题. 作者屠正松,Apache APISIX PMC Member. 原文链接 现象 在 APISIX 社区中,曾有部分 ...

  10. Midjourney 提示词工具(10 个国内外最好最推荐的)

    Midjourney,是一个革命性的基于人工智能的艺术生成器,可以从被称为提示的简单文本描述中生成令人惊叹的图像.Midjourney已经迅速成为艺术家.设计师和营销人员的首选工具(包括像我这样根本不 ...