Bootstrap

详细分析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>
;