详细分析MybatisPLus中@DS切换数据源的基本知识(附Demo)

前言

Java项目中多个数据源,相应配置拿些方法哪些类访问
类似JDBC每个类都要写一遍会比较冗余,有没有集中式管理呢??

看这篇文章之前推荐阅读:

  1. java框架 零基础从入门到精通的学习路线 附开源项目面经等(超全)
  2. jdbc从入门到精通(全)
  3. 【Java项目】实战CRUD的功能整理(持续更新)

对于DS注解切换数据源,踩了很多坑,一一尝试之后,对此总结其基本知识

对于DS注解的基本知识推荐阅读:

  1. dynamic-datasource 源码
  2. dynamic-datasource 具体说明

尝试了好几次版本,发现一直失效,一开始以为是用错了,但是后面发现一定要经过AOP的切面类,也就是@EnableAspectJAutoProxy(exposeProxy = true)这种方式

在这里插入图片描述

1. 基本知识

动态数据源(Dynamic Datasource)是指在运行时根据不同的条件动态切换数据源的技术

在分布式系统、微服务架构和多租户系统中尤为重要

  • 数据源:数据库连接信息的集合,通常包含数据库的URL、用户名、密码、驱动类等

  • 动态数据源:根据业务逻辑或其他条件动态切换到不同的数据源,以实现读写分离、负载均衡、容灾备份等功能

其应用场景有如下:

  • 读写分离:将读操作和写操作分配到不同的数据库上,提高系统的读写性能
  • 负载均衡:通过多数据源分担负载,防止单个数据库过载
  • 多租户系统:为每个租户配置独立的数据源,隔离数据,保障安全性
  • 容灾备份:在主数据库出现故障时,自动切换到备份数据库,保证系统的高可用性

再来说说DS注解的基本概念:

@DS注解用于动态数据源切换,用于指定方法或类使用特定的数据源
@DS注解一般来自MyBatis-Plus中的Dynamic DataSource模块,它允许在方法级别进行数据源切换

需要注意下核心的功能:

  • 方法级别的数据源切换:@DS注解可以直接应用在方法上,使该方法在执行时使用指定的数据源
  • 类级别的数据源切换:@DS注解可以应用在类上,使该类中的所有方法在执行时使用指定的数据源
  • 优先级:方法级别的@DS注解优先于类级别的@DS注解

引入对应的依赖:

<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
  <version>${version}</version>
</dependency>

配置对应的数据源:

spring:
  datasource:
    dynamic:
      primary: master
      strict: false
      datasource:
        master:
          url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic
          username: root
          password: 123456
          driver-class-name: com.mysql.jdbc.Driver
        slave_1:
          url: jdbc:mysql://xx.xx.xx.xx:3307/dynamic
          username: root
          password: 123456
          driver-class-name: com.mysql.jdbc.Driver
        slave_2:
          url: ENC(xxxxx)
          username: ENC(xxxxx)
          password: ENC(xxxxx)
          driver-class-name: com.mysql.jdbc.Driver

基本的切换数据源方式如下:

@Service
@DS("slave")
public class UserServiceImpl implements UserService {

  @Autowired
  private JdbcTemplate jdbcTemplate;

  @Override
  @DS("slave_1")
  public List selectByCondition() {
    return jdbcTemplate.queryForList("select * from user where age >10");
  }
}

2. 源码分析

查看其源码:

package com.baomidou.dynamic.datasource.annotation;


import java.lang.annotation.*;

/**
 * The core Annotation to switch datasource. It can be annotate at class or method.
 *
 * @author TaoYu Kanyuxia
 * @since 1.0.0
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DS {

    /**
     * groupName or specific database name or spring SPEL name.
     *
     * @return the database you want to switch
     */
    String value();
}
  • @Target({ElementType.TYPE, ElementType.METHOD}):指定了注解 @DS 可以应用在哪些地方,指定了 @DS 注解可以应用在类和方法上

  • @Retention(RetentionPolicy.RUNTIME):指定了注解 @DS 在程序运行时保留,可以通过反射来访问并解析 @DS 注解的信息

  • @Documented:标记这个注解是应该被 javadoc 工具记录的,当生成 API 文档时,如果一个类用 @Documented 注解修饰,那么它的注解将出现在生成的文档中

  • String value();:属性,返回一个字符串,用于指定要切换到的数据库的名称

3. Demo

对应的版本号此处使用的4.3.0

<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
  <version>4.3.0</version>
</dependency>

其数据源配置如下:

spring:
  datasource:
    dynamic: # 多数据源配置
      druid: # Druid 【连接池】相关的全局配置
        initial-size: 1 # 初始连接数
        min-idle: 1 # 最小连接池数量
        max-active: 20 # 最大连接池数量
        max-wait: 600000 # 配置获取连接等待超时的时间,单位:毫秒
        time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒
        min-evictable-idle-time-millis: 300000 # 配置一个连接在池中最小生存的时间,单位:毫秒
        max-evictable-idle-time-millis: 900000 # 配置一个连接在池中最大生存的时间,单位:毫秒
        validation-query: SELECT 1 FROM DUAL # 配置检测连接是否有效
        test-while-idle: true
        test-on-borrow: false
        test-on-return: false
      primary: master
      datasource:
        master:
          name: manong
          url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.master.name}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例
          #          url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.master.name}?useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT # MySQL Connector/J 5.X 连接的示例
          #          url: jdbc:postgresql://127.0.0.1:5432/${spring.datasource.dynamic.datasource.master.name} # PostgreSQL 连接的示例
          #          url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例
          #          url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=${spring.datasource.dynamic.datasource.master.name} # SQLServer 连接的示例
          #          url: jdbc:dm://10.211.55.4:5236?schema=RUOYI_VUE_PRO # DM 连接的示例
          username: root
          password: root
        #          username: sa
        #          password: JSm:g(*%lU4ZAkz06cd52KqT3)i1?H7W
        #          username: SYSDBA # DM 连接的示例
        #          password: SYSDBA # DM 连接的示例
        slave: # 模拟从库,可根据自己需要修改
          name: manong
          lazy: true # 开启懒加载,保证启动速度
          url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.slave.name}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例
          #          url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.slave.name}?useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT # MySQL Connector/J 5.X 连接的示例
          #          url: jdbc:postgresql://127.0.0.1:5432/${spring.datasource.dynamic.datasource.slave.name} # PostgreSQL 连接的示例
          #          url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例
          #          url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=${spring.datasource.dynamic.datasource.slave.name} # SQLServer 连接的示例
          username: root
          password: root

在写代码的时候有两个注意事项:

  • 单机 + 单数据源:@Transactional 注解
  • 单机 + 多数据源:@DSTransactional 注解(新版本支持,旧版本可以使用@Transactional(propagation = Propagation.REQUIRES_NEW)
  • 多机 + 单/多数据源:Seata 分布式事务

3.1 成功案例

基本的代码如下:

  • 通过其他实现类来实现:(此处为GoodsStoragePlanServiceImpl 类)
@Service
@Validated
public class GoodsStoragePlanServiceImpl implements GoodsStoragePlanService {

    @Autowired
    private JdbcTemplate oracleJdbcTemplate;
    	
	@Autowired
    private  EnterpriseRegistryServiceImpl enterpriseRegistryServiceimpl;

    @DS("slave")
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    @Override
    public void selectGoodsStoragePlan() {
        enterpriseRegistryServiceimpl.selectGoodsStoragePlan();
    }

}

引用EnterpriseRegistryServiceImpl 类来触发

@Service
@Validated
public class EnterpriseRegistryServiceImpl implements EnterpriseRegistryService {
	
	@Autowired
    private JdbcTemplate oracleJdbcTemplate;

	@DS("tos200")
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void selectGoodsStoragePlan() {
        String sql = "SELECT * FROM C_OPER_PATH";
        oracleJdbcTemplate.queryForList(sql);
        System.out.println(oracleJdbcTemplate.queryForList(sql));
    }
}

对于成功的例子 说明是走了AOP切面类,从而触发DS的注解,而失败案例是没有触发到,直接在使用类上触发不到

此处为了说明AOP切面类的执行过程,并不是很规范

  1. 规范一些的话可以通过一个CommonImpl类来做一个切口,规定这是某个数据库的
  2. Impl实现类调用Mapper,Mapper使用注解来触发

3.2 失败案例

impl中直接调用另外一个DS注解的方法,AOP切面类无法执行成功

@Service
@Validated
public class GoodsStoragePlanServiceImpl implements GoodsStoragePlanService {

    @Autowired
    private JdbcTemplate oracleJdbcTemplate;

    
    @Override
    public void selectGoodsStoragePlan() {
        select();
    }

	@DS("slave")
    @Transactional(propagation = Propagation.REQUIRES_NEW)
	publivc void select(){
		String sql = "SELECT * FROM C_OPER_PATH";
        oracleJdbcTemplate.queryForList(sql);
        System.out.println(oracleJdbcTemplate.queryForList(sql));
	}

}

切记,必须只有经过AOP的切面才可执行成功,否则单纯加个注解DS,不一定能成功

另外一种动态切换数据源的方式推荐阅读:详细分析Java中DynamicDataSourceContextHolder动态数据源切换(附Demo)

4. 实战

以下实战与上述Demo类似,只不过用在了方法上 (与正常开发一样,只不过在此处添加了该注解)

@Service
@DS("slave")
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
public class TemperatureAlarmHistServiceImpl implements ITemperatureAlarmHistService {
	@Resource
	private TemperatureAlarmHistMapper temperatureAlarmHistMapper;


	public boolean saveForList(List<TemperatureAlarmHist> temperatureAlarmHistList) {
		return temperatureAlarmHistMapper.saveForList(temperatureAlarmHistList);
	}
}

对应的数据源配置如下:

#数据源配置
spring:
  #  排除DruidDataSourceAutoConfigure
  autoconfigure:
    exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
  datasource:
    dynamic:
      # 设置默认的数据源或者数据源组,默认值即为master
      primary: master
      datasource:
        master:
           url: jdbc:mysql://localhost:3306/db_master
          username: root
          password: root
        slave:
          url: jdbc:oracle:manong:@//localhost:1521/GIS
          username: root
          password: ROOTGPS1
        ep:
          url: jdbc:sqlserver://localhost:1433;databaseName=manong
          username: root
          password: root

提供的配置中,exclude 属性指定了要排除的自动配置类 com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure

这样做的目的是排除Druid连接池的自动配置,避免它与手动配置的数据源发生冲突

另外一种排除的方式:

@SpringBootApplication(exclude = DruidDataSourceAutoConfigure.class)
public class Application {
 
  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }
}

前提是 上述这两种方式本身已经引入了该依赖包

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.1</version>
</dependency>
  • 17
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论
动态数据源切换是指在程序运行时根据需要切换数据源。实现动态数据源切换可以灵活地处理不同数据源的读写操作,提高系统的可扩展性和可维护性。下面介绍一种基于注解的实现方式。 首先,定义一个注解类`@DataSource`,用于标识需要切换数据源: ```java @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DataSource { String value() default "default"; } ``` 其,`value`属性用于指定数据源的名称。如果不指定,则使用默认数据源。 接着,定义一个数据源切换的切面类`DataSourceAspect`,用于切换数据源: ```java @Aspect @Component public class DataSourceAspect { @Pointcut("@within(com.example.datasource.annotation.DataSource) || @annotation(com.example.datasource.annotation.DataSource)") public void dataSourcePointCut() { } @Around("dataSourcePointCut()") public Object around(ProceedingJoinPoint point) throws Throwable { MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod(); DataSource dataSource = method.getAnnotation(DataSource.class); if (dataSource == null) { dataSource = method.getDeclaringClass().getAnnotation(DataSource.class); } if (dataSource != null) { DynamicDataSourceContextHolder.setDataSource(dataSource.value()); } try { return point.proceed(); } finally { DynamicDataSourceContextHolder.clearDataSource(); } } } ``` 在该切面类,使用`@Pointcut`注解定义一个切点`dataSourcePointCut`,用于匹配所有被`@DataSource`注解标识的类或方法。在`around`方法,通过反射获取方法上的`@DataSource`注解,并根据注解数据源名称切换数据源。在方法执行完毕后,清空数据源上下文。 最后,定义一个数据源上下文类`DynamicDataSourceContextHolder`,用于保存当前数据源的名称: ```java public class DynamicDataSourceContextHolder { private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>(); public static void setDataSource(String dataSource) { CONTEXT_HOLDER.set(dataSource); } public static String getDataSource() { return CONTEXT_HOLDER.get(); } public static void clearDataSource() { CONTEXT_HOLDER.remove(); } } ``` 在需要切换数据源的类或方法上,添加`@DataSource`注解即可: ```java @Service @DataSource("db1") public class UserServiceImpl implements UserService { @Override public User getUserById(Long id) { return userDao.getUserById(id); } } ``` 上述代码,`UserServiceImpl`类上的`@DataSource("db1")`表示使用名为`db1`的数据源。在调用`getUserById`方法时,切换到`db1`数据源进行操作。 以上就是基于注解实现动态数据源切换的方法。这种实现方式比较简单,但需要手动在需要切换数据源的类或方法上添加注解。如果需要更加自动化地切换数据源,可以考虑使用AOP或动态代理来实现。
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码农研究僧

你的鼓励将是我创作的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值