本文采用知识共享 署名-相同方式共享 4.0 国际 许可协议进行许可。
访问 https://creativecommons.org/licenses/by-sa/4.0/ 查看该许可协议。

源码上传至 Github: https://github.com/WarsFeng/SpringBoot_MultiDB

1) 前置知识点

1.1) 注解

注解仅为一个标识,起到注释或辅助程序运行的作用。

1.1.1) Meta Annotation

Java 提供了四个标准元注解,用于说明自定义注解的作用域

  • @Target: 注解使用范围,Type \ Field \ Method \ Parameter ...
  • @Retention: 存在于哪个阶段,Source \ Class \ Runtime
  • @Document: 包含进 JavaDoc
  • @Inherited: 标识的注解被子类继承
    • 有一例 @A,注解了 Class C,Class C2 extend C,则 C2 也拥有 @A

2) SpringBoot 实现读写分离

一般结合 MyBatis 使用,实现多数据源的支持。

2.1) Config

首先在 application.yml 中定义两个数据源 reader, writer:

spring:
  datasource:
    reader:
      url: jdbc:mysql://rcat:3306/multi_db?useUnicode=true@characterEncoding=utf-8
      username: root
      password: passwd
      driver-class-name: com.mysql.cj.jdbc.Driver
    writer:
      url: jdbc:mysql://rcat:3306/multi_db2?useUnicode=true@characterEncoding=utf-8
      username: root
      password: passwd
      driver-class-name: com.mysql.cj.jdbc.Driver

2.2) Enum and Annotation

我们将使用注解切换数据源,定义我们的枚举 DataSourceType 和注解 DBSelector
, 注解为 Method 级别注解,实现函数内的数据源选择。

public enum DataSourceType {
  READER,
  WRITER
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DBSelector {

  DataSourceType value() default DataSourceType.READER;
}

2.3) ThreadLocal handler

使用 ThreadLocal 保证代码优雅,定义数据源绑定器,下文会利用 AOP 将 dbType set 进 ThreadLocal

public class DataSourceBinding {

  private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();

  public static void bindDataSource(DataSourceType dbType) {
    threadLocal.set(dbType.getValue());
  }

  public static String getDataSource() {
    return threadLocal.get();
  }

  public static void clearDataSource() {
    threadLocal.remove();
  }
}

2.4) Cut Aspect

本文仅实现 Method 级别的数据源切换,为 @DBSelector 标注域内绑定上 dbType
, 方法开始时 bind,结束后 clear

@Aspect
@Order(1)
@Component
public class MultiDataSourceAspect {

  @Around("@annotation(cat.wars.course.multidb.annotation.DBSelector)")
  public Object around(ProceedingJoinPoint point) throws Throwable {
    MethodSignature signature = (MethodSignature) point.getSignature();
    DBSelector dbSelector = signature.getMethod().getAnnotation(DBSelector.class);
    if (null != dbSelector) {
      DataSourceBinding.bindDataSource(dbSelector.value().name());
    }

    try {
      return point.proceed();
    } finally {
      DataSourceBinding.clearDataSource();
    }
  }
}

2.5) Configuration

这是最重要的一个部分,Spring-JDBC 内置了多数据源的支持,基于 DataSource 实现了 AbstractRoutingDataSource 接口。

原理:AbstractRoutingDataSource 内置了一个 Map 可用于存储我们的多个数据源, getConnection 时会从 Map 中尝试 Get
, 用 determineCurrentLookupKey 返回值当作 Key, 所以我们应该怎么实现这个抽象类呢:

  • 构造函数中将多个数据源 Put 进 Map,使用 DataSourceType 当 Key
  • determineCurrentLookupKey 中直接将 ThreadLocal 中的 dbType 取出即完成~

接着用 @Bean@Primary 直接塞进容器中,这里我直接用内部类写在 Configuratoin 中了 =。=

@Configuration
public class MultiDataSourceConfig {

  @Bean
  @ConfigurationProperties("spring.datasource.reader")
  public DataSource reader() {
    return DataSourceBuilder.create().build();
  }

  @Bean
  @ConfigurationProperties("spring.datasource.writer")
  public DataSource writer() {
    return DataSourceBuilder.create().build();
  }

  @Bean
  @Primary
  public DynamicDataSource dataSource(DataSource reader, DataSource writer) {
    HashMap<Object, Object> targetDataSources = new HashMap<>();
    targetDataSources.put(DataSourceType.READER.name(), reader);
    targetDataSources.put(DataSourceType.WRITER.name(), writer);

    return new DynamicDataSource(reader, targetDataSources);
  }

  /** 动态数据源 DataSource */
  private static final class DynamicDataSource extends AbstractRoutingDataSource {

    public DynamicDataSource(DataSource defaultDataSource, Map<Object, Object> targetDataSources) {
      this.setDefaultTargetDataSource(defaultDataSource);
      if (null == targetDataSources) return;
      this.setTargetDataSources(targetDataSources);
      this.afterPropertiesSet();
    }

    @Override
    protected Object determineCurrentLookupKey() {
      return DataSourceBinding.getDataSource();
    }
  }
}

接着由于 SpringBoot 的默认配置会在启动时使用 DataSourceAutoConfiguration 获取配置中的 spring.datasource 初始化默认的 DataSource
, 我们需要在 SpringBoot 的启动器中排除掉它,如:

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class MultiDbApplication {

  public static void main(String[] args) {
    SpringApplication.run(MultiDbApplication.class, args);
  }
}

2.6) Test

测试!这里用 SpringMVC 测试效果:

@RestController
public class ReadWriteController {

  private final JdbcTemplate jdbcTemplate;

  public ReadWriteController(JdbcTemplate jdbcTemplate) {
    this.jdbcTemplate = jdbcTemplate;
  }

  @GetMapping("/read")
  @DBSelector(DataSourceType.READER)
  public String read() {
    return jdbcTemplate.queryForList("SELECT * FROM user").toString();
  }

  @GetMapping("/write")
  @DBSelector(DataSourceType.WRITER)
  public String write() {
    return jdbcTemplate.queryForList("SELECT * FROM t_user").toString();
  }
}