利用SpringBoot实现多数据源的两种方式总结

时间:2022-03-10 08:48:23

前言

公司项目有连接多个不同数据库的需求,特研究了一下,根据网上的资料,造了一个基于AOP方式的数据源切换*,但继续探索,突然发现有开源的多数据源管理启动器。不过,本篇两种方式都会介绍。

 

基于dynamic-datasource实现多数据源

dynamic-datasource介绍

dynamic-datasource-spring-boot-starter 是一个基于springboot的快速集成多数据源的启动器。

其支持 Jdk 1.7+, SpringBoot 1.4.x 1.5.x 2.x.x

dynamic-datasource特性

  • 支持 数据源分组 ,适用于多种场景 纯粹多库 读写分离 一主多从 混合模式。
  • 支持数据库敏感配置信息 加密 ENC()。
  • 支持每个数据库独立初始化表结构schema和数据库database。
  • 支持无数据源启动,支持懒加载数据源(需要的时候再创建连接)。
  • 支持 自定义注解 ,需继承DS(3.2.0+)。
  • 提供并简化对Druid,HikariCp,BeeCp,Dbcp2的快速集成。
  • 提供对Mybatis-Plus,Quartz,ShardingJdbc,P6sy,Jndi等组件的集成方案。
  • 提供 自定义数据源来源 方案(如全从数据库加载)。
  • 提供项目启动后 动态增加移除数据源 方案。
  • 提供Mybatis环境下的 纯读写分离 方案。
  • 提供使用 spel动态参数 解析数据源方案。内置spel,session,header,支持自定义。
  • 支持 多层数据源嵌套切换 。(ServiceA >>> ServiceB >>> ServiceC)。
  • 提供 基于seata的分布式事务方案。
  • 提供 本地多数据源事务方案。 附:不能和原生spring事务混用。

我们目前只探讨使用dynamic-datasource进行数据源切换,其他请自行搜索

dynamic-datasource的相关约定

  • dynamic-datasource只做 切换数据源 这件核心的事情,并不限制你的具体操作,切换了数据源可以做任何CRUD。
  • 配置文件所有以下划线 _ 分割的数据源 首部 即为组的名称,相同组名称的数据源会放在一个组下。
  • 切换数据源可以是组名,也可以是具体数据源名称。组名则切换时采用负载均衡算法切换。
  • 默认的数据源名称为 master ,你可以通过 spring.datasource.dynamic.primary 修改。
  • 方法上的注解优先于类上注解。
  • DS支持继承抽象类上的DS,暂不支持继承接口上的DS。

引入dynamic-datasource依赖

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

配置数据源

spring:
datasource:
  dynamic:
    primary: mysql #设置默认的数据源或者数据源组,默认值即为master
    strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
    datasource:
      mysql:
        url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic
        username: root
        password: 123456
        driver-class-name: com.mysql.jdbc.Driver # 3.2.0开始支持SPI可省略此配置
      pgsql:
        url: ENC(xxxxx) # 内置加密
        username: ENC(xxxxx)
        password: ENC(xxxxx)
        driver-class-name: org.postgresql.Driver

使用 @DS 切换数据源

@DS 可以注解在方法上或类上,同时存在就近原则 方法上注解 优先于 类上注解

注解 结果
不使用@DS注解 默认数据源,即primary: mysql
@DS(“dsName”) dsName可以为组名也可以为具体某个库的名称

@DS使用实例

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

@Autowired
private JdbcTemplate jdbcTemplate;

// 不使用@DS注解则代表使用默认数据源
// 如果类上存在,则使用类上标注的数据源
public List selectAll() {
  return  jdbcTemplate.queryForList("select * from user");
}

@Override
@DS("pgsql")
// 方法上注解 优先于 类上注解,即使类上标注也优先采用方法上的标注
public List selectByCondition() {
  return  jdbcTemplate.queryForList("select * from user where age >10");
}
}

 

基于AOP手动实现多数据源

本次代码参考 https://github.com/mianshenglee/my-example/tree/master/multi-datasource/dynamic-datasource ,因源码不满足的需求,因此我在此基础做了修改。

项目工程结构

利用SpringBoot实现多数据源的两种方式总结

项目依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>2.2.2.RELEASE</version>
      <relativePath/> <!-- lookup parent from repository -->
  </parent>
  <groupId>me.mason.demo</groupId>
  <artifactId>dynamic-datasource</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>dynamic-datasource</name>
  <description>Demo project for dynamic datasource</description>

  <properties>
      <java.version>1.8</java.version>
  </properties>

  <dependencies>
      <!--spring boot-->
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-configuration-processor</artifactId>
      </dependency>
      <dependency>
          <groupId>com.alibaba</groupId>
          <artifactId>druid-spring-boot-starter</artifactId>
          <version>1.1.9</version>
      </dependency>
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-jdbc</artifactId>
      </dependency>
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-aop</artifactId>
      </dependency>
      <!--mysql 驱动-->
      <dependency>
          <groupId>mysql</groupId>
          <artifactId>mysql-connector-java</artifactId>
          <scope>runtime</scope>
      </dependency>
      <!--mybatis-plus-->
      <dependency>
          <groupId>com.baomidou</groupId>
          <artifactId>mybatis-plus-boot-starter</artifactId>
          <version>3.3.0</version>
      </dependency>
      <dependency>
          <groupId>org.projectlombok</groupId>
          <artifactId>lombok</artifactId>
          <optional>true</optional>
      </dependency>
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-test</artifactId>
          <scope>test</scope>
          <exclusions>
              <exclusion>
                  <groupId>org.junit.vintage</groupId>
                  <artifactId>junit-vintage-engine</artifactId>
              </exclusion>
          </exclusions>
      </dependency>

  </dependencies>

  <build>
      <plugins>
          <plugin>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-maven-plugin</artifactId>
          </plugin>
      </plugins>
  </build>

</project>

配置文件

server.port=8080
server.servlet.context-path=/dd

logging.level.root=INFO
logging.level.me.mason.demo.dynamicdatasource.mapper=DEBUG

# mybatis-plus
mybatis-plus.type-aliases-package=me.mason.demo.dynamicdatasource.entity
# 默认位置,可不配置
#mybatis-plus.mapper-locations=classpath*:/mapper/*.xml
mybatis.mapper-locations=classpath*:/mapper/*.xml
# 使用数据库自增ID
mybatis-plus.global-config.db-config.id-type=auto

# master
spring.datasource.master.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.master.url=jdbc:mysql://10.0.1.243:3306/scheduling?useSSL=false&serverTimezone=GMT%2B8&characterEncoding=UTF-8
spring.datasource.master.username=root
spring.datasource.master.password=123456

# slave
spring.datasource.slave.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.slave.url=jdbc:mysql://10.0.1.243:3306/scheduling1?useSSL=false&serverTimezone=GMT%2B8&characterEncoding=UTF-8
spring.datasource.slave.username=root
spring.datasource.slave.password=123456

自定义注解

// 标记注解可使用在方法与类上
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DS {
	// 默认值为MASTER
  String value() default DataSourceConstants.DS_KEY_MASTER;
}

编写DataSourceConstants

/**
* 数据源常量
**/
public class DataSourceConstants {
  /**
   * master数据源
   */
  public static final String DS_KEY_MASTER = "master";
  /**
   * slave数据源
   */
  public static final String DS_KEY_SLAVE = "slave";
}

动态数据源名称上下文处理

/**
* 动态数据源名称上下文处理
**/
public class DynamicDataSourceContextHolder {

  /**
   * 动态数据源名称上下文
   */
  private static final ThreadLocal<String> DATASOURCE_CONTEXT_KEY_HOLDER = new ThreadLocal<>();

  /**
   * 设置数据源
   * @param key
   */
  public static void setContextKey(String key){
      DATASOURCE_CONTEXT_KEY_HOLDER.set(key);
  }

  /**
   * 获取数据源名称
   * @return
   */
  public static String getContextKey(){
      String key = DATASOURCE_CONTEXT_KEY_HOLDER.get();
      return key == null?DataSourceConstants.DS_KEY_MASTER:key;
  }

  /**
   * 删除当前数据源名称
   */
  public static void removeContextKey(){
      DATASOURCE_CONTEXT_KEY_HOLDER.remove();
  }
}

获取当前动态数据源方法

/**
* 动态数据源
**/
public class DynamicDataSource extends AbstractRoutingDataSource {

  @Override
  protected Object determineCurrentLookupKey() {
      return DynamicDataSourceContextHolder.getContextKey();
  }
}

动态数据源配置

/**
* 动态数据源配置
**/
@EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class })
@Configuration
// 此处我们
//@PropertySource("classpath:config/jdbc.properties")
@MapperScan(basePackages = "me.mason.demo.dynamicdatasource.mapper")
public class DynamicDataSourceConfig {
  @Bean(DataSourceConstants.DS_KEY_MASTER)
  // 需要与配置文件中对应
  @ConfigurationProperties(prefix = "spring.datasource.master")
  public DruidDataSource masterDataSource() {
      return DruidDataSourceBuilder.create().build();
  }

  @Bean(DataSourceConstants.DS_KEY_SLAVE)
  @ConfigurationProperties(prefix = "spring.datasource.slave")
  public DruidDataSource slaveDataSource() {
      return DruidDataSourceBuilder.create().build();
  }

  @Bean
  @Primary
  public DynamicDataSource dynamicDataSource() {
      Map<Object, Object> dataSourceMap = new HashMap<>(2);
      dataSourceMap.put(DataSourceConstants.DS_KEY_MASTER, masterDataSource());
      dataSourceMap.put(DataSourceConstants.DS_KEY_SLAVE, slaveDataSource());
      //设置动态数据源
      DynamicDataSource dynamicDataSource = new DynamicDataSource();
      dynamicDataSource.setTargetDataSources(dataSourceMap);
      dynamicDataSource.setDefaultTargetDataSource(masterDataSource());
      return dynamicDataSource;
  }

}

AOP切面

/**
* 切面
*/
@Aspect
@Component
//@Order(-10)
public class DynamicDataSourceAspect {
	// 以在类上使用了@Service作为切入点
  @Pointcut("@within(org.springframework.stereotype.Service)")
  public void dataSourcePointCut() {
  }

  @Around("dataSourcePointCut()")
  public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
      MethodSignature signature = (MethodSignature) joinPoint.getSignature();
      Class<?> aClass = Class.forName(signature.getDeclaringType().getName());
      // 方法优先,如果方法上存在注解,则优先使用方法上的注解
      if (signature.getMethod().isAnnotationPresent(DS.class)) { DynamicDataSourceContextHolder.setContextKey(signature.getMethod().getAnnotation(DS.class).value());
          // 其次类优先,如果类上存在注解,则使用类上的注解
      }else  if (aClass.isAnnotationPresent(DS.class)) { DynamicDataSourceContextHolder.setContextKey(aClass.getAnnotation(DS.class).value());
          // 如果都不存在,则使用默认
      }   else {
DynamicDataSourceContextHolder.setContextKey(DataSourceConstants.DS_KEY_MASTER);
      }
      try {
          return joinPoint.proceed();
      } finally {
          DynamicDataSourceContextHolder.removeContextKey();
      }
  }
}

编写TestUser实体

@Data
@TableName("test_user")
public class TestUser implements Serializable {

  private static final long serialVersionUID = 1L;

  /** id */
  private Long id;
  /** 姓名 */
  private String name;
  /** 手机号 */
  private String phone;
  /** 职称职别 */
  private String title;
  /** 邮箱 */
  private String email;
  /** 性别 */
  private String gender;
  /** 出生时间 */
  private Date dateOfBirth;
  /** 1:已删除,0:未删除 */
  private Integer deleted;
  /** 创建时间 */
  private Date sysCreateTime;
  /** 创建人 */
  private String sysCreateUser;
  /** 更新时间 */
  private Date sysUpdateTime;
  /** 更新人 */
  private String sysUpdateUser;
  /** 版本号 */
  private Long recordVersion;

  public TestUser() {
  }

}

TestUserMapper

@Repository
public interface TestUserMapper extends BaseMapper<TestUser> {

  /**
   * 自定义查询
   * @param wrapper 条件构造器
   * @return
   */
  List<TestUser> selectAll(@Param(Constants.WRAPPER) Wrapper<TestUser> wrapper);
}

TestUserService

@Service
//@DS(DataSourceConstants.DS_KEY_SLAVE)
public class TestUserService {
  @Autowired
  private TestUserMapper testUserMapper;

  /**
   * 查询master库User
   * @return
   */
//    @DS(DataSourceConstants.DS_KEY_MASTER)
  public List<TestUser> getMasterUser(){
      QueryWrapper<TestUser> queryWrapper = new QueryWrapper<>();
      return testUserMapper.selectAll(queryWrapper.isNotNull("name"));
  }

  /**
   * 查询slave库User
   * @return
   */
//    @DS(DataSourceConstants.DS_KEY_MASTER)
  public List<TestUser> getSlaveUser(){
      return testUserMapper.selectList(null);
  }
}

TestUserController

@RestController
@RequestMapping("/user")
public class TestUserController {
  @Autowired
  private TestUserService testUserService;
  /**
   * 查询全部
   */
  @GetMapping("/listall")
  public Object listAll() {
      int initSize = 2;
      Map<String, Object> result = new HashMap<>(initSize);
      List<TestUser> masterUser = testUserService.getMasterUser();
      result.put("masterUser", masterUser);
      List<TestUser> slaveUser = testUserService.getSlaveUser();
      result.put("getSlaveUser", slaveUser);
      return ResponseResult.success(result);
  }

}

MapperXml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
      PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
      "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="me.mason.demo.dynamicdatasource.mapper.TestUserMapper">
  <select id="selectAll" resultType="me.mason.demo.dynamicdatasource.entity.TestUser">
      select * from test_user
      <if test="ew!=null">
        ${ew.customSqlSegment}
      </if>
  </select>
</mapper>

启动测试

不使用注解

@Service
//@DS(DataSourceConstants.DS_KEY_SLAVE)
public class TestUserService {
  @Autowired
  private TestUserMapper testUserMapper;


  /**
   * 查询master库User
   * @return
   */
//    @DS(DataSourceConstants.DS_KEY_MASTER)
  public List<TestUser> getMasterUser(){
      QueryWrapper<TestUser> queryWrapper = new QueryWrapper<>();
      return testUserMapper.selectAll(queryWrapper.isNotNull("name"));
  }

  /**
   * 查询slave库User
   * @return
   */
//    @DS(DataSourceConstants.DS_KEY_MASTER)
  public List<TestUser> getSlaveUser(){
      return testUserMapper.selectList(null);
  }

}

效果

该代码优先级与使用框架效果一致,即不使用注解将默认使用MASTER数据库,方法上存在注解优先使用方法上标注的注解。

已知MASTER 6条数据, SLAVE4条数据

访问 http://127.0.0.1:8080/dd/user/listall 查看效果

利用SpringBoot实现多数据源的两种方式总结

类上使用注解

@Service
@DS(DataSourceConstants.DS_KEY_SLAVE)
public class TestUserService {
  @Autowired
  private TestUserMapper testUserMapper;


  /**
   * 查询master库User
   * @return
   */
//    @DS(DataSourceConstants.DS_KEY_MASTER)
  public List<TestUser> getMasterUser(){
      QueryWrapper<TestUser> queryWrapper = new QueryWrapper<>();
      return testUserMapper.selectAll(queryWrapper.isNotNull("name"));
  }

  /**
   * 查询slave库User
   * @return
   */
//    @DS(DataSourceConstants.DS_KEY_MASTER)
  public List<TestUser> getSlaveUser(){
      return testUserMapper.selectList(null);
  }
}

效果

利用SpringBoot实现多数据源的两种方式总结

方法上使用注解

@Service
@DS(DataSourceConstants.DS_KEY_SLAVE)
public class TestUserService {
  @Autowired
  private TestUserMapper testUserMapper;
  /**
   * 查询master库User
   * @return
   */
  @DS(DataSourceConstants.DS_KEY_SLAVE)
  public List<TestUser> getMasterUser(){
      QueryWrapper<TestUser> queryWrapper = new QueryWrapper<>();
      return testUserMapper.selectAll(queryWrapper.isNotNull("name"));
  }
  /**
   * 查询slave库User
   * @return
   */
  @DS(DataSourceConstants.DS_KEY_MASTER)
  public List<TestUser> getSlaveUser(){
      return testUserMapper.selectList(null);
  }
}

效果

利用SpringBoot实现多数据源的两种方式总结

 

总结

到此这篇关于利用SpringBoot实现多数据源的两种方式的文章就介绍到这了,更多相关SpringBoot实现多数据源内容请搜索服务器之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持服务器之家!

原文链接:https://blog.csdn.net/fengxueliuke/article/details/120914389