JavaSpring
对于多数据源需求,Spring早在 2007 年就注意到并且给出了解决方案,原文见:dynamic-datasource-routing
Spring提供了一个AbstractRoutingDataSource类,用来实现对多个DataSource的按需路由,本文介绍的就是基于此方式实现的多数据源实践。

一、什么是AbstractRoutingDataSource

先看类上的注释:

Abstract {@link javax.sql.DataSource} implementation that routes {@link #getConnection()} calls to one of various target DataSources based on a lookup key. The latter is usually (but not necessarily) determined through some thread-bound transaction context.

课代表翻译:这是一个抽象类,可以通过一个lookup key,把对getConnection()方法的调用,路由到目标DataSource。后者(指lookup key)通常是由和线程绑定的上下文决定的。
这段注释可谓字字珠玑,没有一句废话。下文结合主要代码解释其含义。

  1. public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
  2. //目标 DataSource Map,可以装很多个 DataSource
  3. @Nullable
  4. private Map<Object, Object> targetDataSources;
  5. @Nullable
  6. private Map<Object, DataSource> resolvedDataSources;
  7. //Bean初始化时,将 targetDataSources 遍历并解析后放入 resolvedDataSources
  8. @Override
  9. public void afterPropertiesSet() {
  10. if (this.targetDataSources == null) {
  11. throw new IllegalArgumentException("Property 'targetDataSources' is required");
  12. }
  13. this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());
  14. this.targetDataSources.forEach((key, value) -> {
  15. Object lookupKey = resolveSpecifiedLookupKey(key);
  16. DataSource dataSource = resolveSpecifiedDataSource(value);
  17. this.resolvedDataSources.put(lookupKey, dataSource);
  18. });
  19. if (this.defaultTargetDataSource != null) {
  20. this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
  21. }
  22. }
  23. @Override
  24. public Connection getConnection() throws SQLException {
  25. return determineTargetDataSource().getConnection();
  26. }
  27. /**
  28. * Retrieve the current target DataSource. Determines the
  29. * {@link #determineCurrentLookupKey() current lookup key}, performs
  30. * a lookup in the {@link #setTargetDataSources targetDataSources} map,
  31. * falls back to the specified
  32. * {@link #setDefaultTargetDataSource default target DataSource} if necessary.
  33. * @see #determineCurrentLookupKey()
  34. */
  35. //根据 #determineCurrentLookupKey()返回的lookup key 去解析好的数据源 Map 里取相应的数据源
  36. protected DataSource determineTargetDataSource() {
  37. Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
  38. // 当前 lookupKey 的值由用户自己实现↓
  39. Object lookupKey = determineCurrentLookupKey();
  40. DataSource dataSource = this.resolvedDataSources.get(lookupKey);
  41. if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
  42. dataSource = this.resolvedDefaultDataSource;
  43. }
  44. if (dataSource == null) {
  45. throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
  46. }
  47. return dataSource;
  48. }
  49. /**
  50. * Determine the current lookup key. This will typically be
  51. * implemented to check a thread-bound transaction context.
  52. * <p>Allows for arbitrary keys. The returned key needs
  53. * to match the stored lookup key type, as resolved by the
  54. * {@link #resolveSpecifiedLookupKey} method.
  55. */
  56. // 该方法用来决定lookup key,通常用线程绑定的上下文来实现
  57. @Nullable
  58. protected abstract Object determineCurrentLookupKey();
  59. // 省略其余代码...
  60. }

首先看类图
AbstractRoutingDataSource-uml
是个DataSource,并且实现了InitializingBean,说明有Bean的初始化操作。
其次看实例变量
private Map<Object, Object> targetDataSources;private Map<Object, DataSource> resolvedDataSources;其实是一回事,后者是经过对前者的解析得来的,本质就是用来存储多个 DataSource实例的 Map。
最后看核心方法
使用DataSource,本质就是调用其getConnection()方法获得连接,从而进行数据库操作。
AbstractRoutingDataSource#getConnection()方法首先调用determineTargetDataSource(),决定使用哪个目标数据源,并使用该数据源的getConnection()连接数据库:

  1. @Override
  2. public Connection getConnection() throws SQLException {
  3. return determineTargetDataSource().getConnection();
  4. }
  5. protected DataSource determineTargetDataSource() {
  6. Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
  7. // 这里使用的 lookupKey 就能决定返回的数据源是哪个
  8. Object lookupKey = determineCurrentLookupKey();
  9. DataSource dataSource = this.resolvedDataSources.get(lookupKey);
  10. if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
  11. dataSource = this.resolvedDefaultDataSource;
  12. }
  13. if (dataSource == null) {
  14. throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
  15. }
  16. return dataSource;
  17. }

所以重点就是determineCurrentLookupKey()方法,该方法是抽象方法,由用户自己实现,通过改变其返回值,控制返回不同的数据源。用表格表示如下:

lookupKey DataSource
first firstDataSource
second secondDataSource

如何实现这个方法呢?结合Spring在注释里给的提示:
后者(指lookup key)通常是由和线程绑定的上下文决定的。
应该能联想到ThreadLocal了吧!ThreadLocal可以维护一个与当前线程绑定的变量,充当这个线程的上下文。

二、实现

设计yaml文件外部化配置多个数据源

  1. spring:
  2. datasource:
  3. first:
  4. driver-class-name: org.h2.Driver
  5. jdbc-url: jdbc:h2:mem:db1
  6. username: sa
  7. password:
  8. second:
  9. driver-class-name: org.h2.Driver
  10. jdbc-url: jdbc:h2:mem:db2
  11. username: sa
  12. password:

创建lookupKey的上下文持有类:

  1. /**
  2. * 数据源 key 上下文
  3. * 通过控制 ThreadLocal变量 LOOKUP_KEY_HOLDER 的值用于控制数据源切换
  4. * @see RoutingDataSource
  5. */
  6. public class RoutingDataSourceContext {
  7. private static final ThreadLocal<String> LOOKUP_KEY_HOLDER = new ThreadLocal<>();
  8. public static void setRoutingKey(String routingKey) {
  9. LOOKUP_KEY_HOLDER.set(routingKey);
  10. }
  11. public static String getRoutingKey() {
  12. String key = LOOKUP_KEY_HOLDER.get();
  13. // 默认返回 key 为 first 的数据源
  14. return key == null ? "first" : key;
  15. }
  16. public static void reset() {
  17. LOOKUP_KEY_HOLDER.remove();
  18. }
  19. }

实现AbstractRoutingDataSource

  1. /**
  2. * 支持动态切换的数据源
  3. * 通过重写 determineCurrentLookupKey 实现数据源切换
  4. */
  5. public class RoutingDataSource extends AbstractRoutingDataSource {
  6. @Override
  7. protected Object determineCurrentLookupKey() {
  8. return RoutingDataSourceContext.getRoutingKey();
  9. }
  10. }

RoutingDataSource初始化上多个数据源:

  1. /**
  2. * 数据源配置
  3. * 把多个数据源,装配到一个 RoutingDataSource 里
  4. */
  5. @Configuration
  6. public class RoutingDataSourcesConfig {
  7. @Bean
  8. @ConfigurationProperties(prefix = "spring.datasource.first")
  9. public DataSource firstDataSource() {
  10. return DataSourceBuilder.create().build();
  11. }
  12. @Bean
  13. @ConfigurationProperties(prefix = "spring.datasource.second")
  14. public DataSource secondDataSource() {
  15. return DataSourceBuilder.create().build();
  16. }
  17. @Primary
  18. @Bean
  19. public RoutingDataSource routingDataSource() {
  20. RoutingDataSource routingDataSource = new RoutingDataSource();
  21. routingDataSource.setDefaultTargetDataSource(firstDataSource());
  22. Map<Object, Object> dataSourceMap = new HashMap<>();
  23. dataSourceMap.put("first", firstDataSource());
  24. dataSourceMap.put("second", secondDataSource());
  25. routingDataSource.setTargetDataSources(dataSourceMap);
  26. return routingDataSource;
  27. }
  28. }

演示一下手工切换的代码:

  1. public void init() {
  2. // 手工切换为数据源 first,初始化表
  3. RoutingDataSourceContext.setRoutingKey("first");
  4. createTableUser();
  5. RoutingDataSourceContext.reset();
  6. // 手工切换为数据源 second,初始化表
  7. RoutingDataSourceContext.setRoutingKey("second");
  8. createTableUser();
  9. RoutingDataSourceContext.reset();
  10. }

这样就实现了最基本的多数据源切换了。
不难发现,切换工作很明显可以抽成一个切面,可以优化一下,利用注解标明切点,哪里需要切哪里。

三、引入AOP

自定义注解

  1. @Target({ElementType.TYPE, ElementType.METHOD})
  2. @Retention(RetentionPolicy.RUNTIME)
  3. @Documented
  4. public @interface WithDataSource {
  5. String value() default "";
  6. }

创建切面

  1. @Aspect
  2. @Component
  3. // 指定优先级高于@Transactional的默认优先级
  4. // 从而保证先切换数据源再进行事务操作
  5. @Order(Ordered.LOWEST_PRECEDENCE - 1)
  6. public class DataSourceAspect {
  7. @Around("@annotation(withDataSource)")
  8. public Object switchDataSource(ProceedingJoinPoint pjp, WithDataSource withDataSource) throws Throwable {
  9. // 1.获取 @WithDataSource 注解中指定的数据源
  10. String routingKey = withDataSource.value();
  11. // 2.设置数据源上下文
  12. RoutingDataSourceContext.setRoutingKey(routingKey);
  13. // 3.使用设定好的数据源处理业务
  14. try {
  15. return pjp.proceed();
  16. } finally {
  17. // 4.清空数据源上下文
  18. RoutingDataSourceContext.reset();
  19. }
  20. }
  21. }

有了注解和切面,使用起来就方便多了:

  1. // 注解标明使用"second"数据源
  2. @WithDataSource("second")
  3. public List<User> getAllUsersFromSecond() {
  4. List<User> users = userService.selectAll();
  5. return users;
  6. }

关于切面有两个细节需要注意:

  1. 需要指定优先级高于声明式事务原因:声明式事务事务的本质也是 AOP,其只对开启时使用的数据源生效,所以一定要在切换到指定数据源之后再开启,声明式事务默认的优先级是最低级,这里只需要设定自定义的数据源切面的优先级比它高即可。
  2. 业务执行完之后一定要清空上下文原因:假设方法 A 使用@WithDataSource("second")指定走”second”数据源,紧跟着方法 B 不写注解,期望走默认的first数据源。但由于方法A放入上下文的lookupKey此时还是”second”并未删除,所以导致方法 B 执行的数据源与期望不符。

    四、回顾

    至此,基于AbstractRoutingDataSource+AOP的多数据源就实现好了。
    在配置DataSource这个Bean的时候,用的是自定义的RoutingDataSource,并且标记为 @Primary。这样就可以让mybatis-spring-boot-starter使用RoutingDataSource自动配置好mybatis,比搞两套DataSource+两套Mybatis配置的方案简单多了。
    特别说明:
    样例中为了减少代码层级,让展示更直观,在 controller 层写了事务注解,实际开发中可别这么干,controller 层的任务是绑定、校验参数,封装返回结果,尽量不要在里面写业务!

    五、优化

    对于一般的多数据源使用场景,这里的方案已足够覆盖,可以实现灵活切换。
    但还是存在如下不足:
  • 每个应用使用时都要新增相关类,大量重复代码
  • 修改或新增功能时,所有相关应用都得改
  • 功能不够强悍,没有高级功能,比如读写分离场景下的读多个从库负载均衡

其实把这些代码封装到一个starter里面,高级功能慢慢扩展就可以。
好在开源世界早就有现成工具可用了,开发mybatis-plus的”baomidou”团队在其生态中开源了一个多数据源框架 Dynamic-Datasource,底层原理就是AbstractRoutingDataSource,增加了更多强悍的扩展功能。

参考资料

dynamic-datasource-routing: https://spring.io/blog/2007/01/23/dynamic-datasource-routing
Dynamic-Datasource: https://gitee.com/baomidou/dynamic-datasource-spring-boot-starter