3种方式实现多数据源控制/切换、实现读写分离;演示借助AbstractRoutingDataSource实现多数据源的动态切换代码【享学Spring】(中)

简介: 3种方式实现多数据源控制/切换、实现读写分离;演示借助AbstractRoutingDataSource实现多数据源的动态切换代码【享学Spring】(中)

SimpleDriverDataSource


和java.sql.Driver强相关。它直接继承自AbstractDriverBasedDataSource。它表示一个简单的数据源,每次获取Connection时,会重新建立一个Connection。通过Driver来获取Connection对象。 获取代码如下:


Connection connection = driver.connect(url, props);


以上实现类基于AbstractDriverBasedDataSource的实现方式,**当然也能做为管理多数据源的方案。**这就是我想说的方式二,具体详细代码省略。


注意,注意,注意:在性能要求不高的情况,可以试试直接使用它们玩玩。否则请务必使用连接池技术提升性能~

我一般只会把此种方式放在测试上~~


上面已经介绍了管理多数据源的两种方式,但都有弊端,真正使用起来也稍显麻烦。

接下来介绍的这种方式是使用最广泛也是本文的主菜~~~


方式三:AbstractRoutingDataSource动态切换数据源


在基于三层的后端架构中,操作数据库的是Dao层。比如现在我们一般使用SSM框架。若我们像上面两种方式操作多数据源,首先最大的缺点就是代码入侵性强、便管理。


此处我们还有一个方法,也就是使用AbstractRoutingDataSource的实现类通过AOP或者手动处理实现动态的使用我们的数据源,这样的入侵性较低,非常好的满足使用的需求。比如我们希望对于读写分离或者其他的数据同步的业务场景。


image.png


如上图,使用AbstractRoutingDataSource的实现类,进行灵活的切换,可以通过AOP或者手动编程设置当前的DataSource。这样的编写方式比较好,至于其中的实现原理是下面会分析的重点


AbstractRoutingDataSource 是个抽象类,具体实现需要调用者书写实现类的。


Spring 2.0.1引入了一个AbstractRoutingDataSource ,我相信这值得关注。


// @since 2.0.1   注意起始版本(该版本2006见发布)
// 并且实现了InitializingBean 接口
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
  // 根据key 缓存下来其对应的DataSource~~~~
  @Nullable
  private Map<Object, Object> targetDataSources;
  @Nullable
  private Object defaultTargetDataSource; // 默认数据源
  // 如果找不到当前查找键的特定数据源,请指定是否对默认数据源应用宽限回退。
  private boolean lenientFallback = true;
  // DataSourceLookup为一个函数式接口  只有一个方法DataSource getDataSource(String dataSourceName)
  // 此处使用的默认实现为JNDI
  private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
  @Nullable
  private Map<Object, DataSource> resolvedDataSources;
  @Nullable
  private DataSource resolvedDefaultDataSource;
  ... // 省略所有的set方法
  @Override
  public void afterPropertiesSet() {
    if (this.targetDataSources == null) {
      throw new IllegalArgumentException("Property 'targetDataSources' is required");
    }
    this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());
    // 遍历设置进来的目标数据源们~~~~
    this.targetDataSources.forEach((key, value) -> {
      Object lookupKey = resolveSpecifiedLookupKey(key);
      DataSource dataSource = resolveSpecifiedDataSource(value);
      // 把已经解决好的缓存起来(注意key和value和上有可能就是不同的了)
      // 注意:key可以是个Object  而不一定只能是String类型
      this.resolvedDataSources.put(lookupKey, dataSource);
    });
    if (this.defaultTargetDataSource != null) {
      this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
    }
  }
  // 子类IsolationLevelDataSourceRouter有复写此方法
  // 绝大多数情况下,我们会直接使用DataSource
  protected Object resolveSpecifiedLookupKey(Object lookupKey) {
    return lookupKey;
  }
  // 此处兼容String类型,若是string就使用dataSourceLookup去查找(默认是JNDI)
  protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException {
    if (dataSource instanceof DataSource) {
      return (DataSource) dataSource;
    } else if (dataSource instanceof String) {
      return this.dataSourceLookup.getDataSource((String) dataSource);
    } else {
      throw new IllegalArgumentException(
          "Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource);
    }
  }
  /链接数据库
  @Override
  public Connection getConnection() throws SQLException {
    return determineTargetDataSource().getConnection();
  }
  @Override
  public Connection getConnection(String username, String password) throws SQLException {
    return determineTargetDataSource().getConnection(username, password);
  }
  protected DataSource determineTargetDataSource() {
    Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
    Object lookupKey = determineCurrentLookupKey();
    DataSource dataSource = this.resolvedDataSources.get(lookupKey);
    // 若根据key没有找到dataSource,并且lenientFallback=true或者lookupKey == null  那就回滚到使用默认的数据源
    // 备注:此处可以看出key=null和this.lenientFallback =true有一样的效果
    if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
      dataSource = this.resolvedDefaultDataSource;
    }
    if (dataSource == null) {
      throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
    }
    return dataSource;
  }
  // 子类必须实现的抽象方法:提供key即可~~~~
  @Nullable
  protected abstract Object determineCurrentLookupKey();
  @Override
  @SuppressWarnings("unchecked")
  public <T> T unwrap(Class<T> iface) throws SQLException {
    if (iface.isInstance(this)) {
      return (T) this;
    }
    return determineTargetDataSource().unwrap(iface);
  }
  @Override
  public boolean isWrapperFor(Class<?> iface) throws SQLException {
    return (iface.isInstance(this) || determineTargetDataSource().isWrapperFor(iface));
  }
}


具体选择哪个数据源是由determineCurrentLookupKey()方法的返回值决定的,该方法需要我们继承AbstractRoutingDataSource来重写。


通过上面源码展示,我们也可以看出AbstractRoutingDataSource切换数据源的源码不多,并且非常简单,相信建立在源码的基础上再去应用,会让你感觉到简直不要太easy。


使用AbstractRoutingDataSource动态切换数据源示例代码


这种功能属于技术模块,完全可以独立于业务模块之外开发出一个类似中间件的组件形式存在。下面结合我具体的使用案例,给大家贡献参考如下参考代码:


1、定义一个常量,表示所有的DataSource的key。(建议用这样的全局常量维护key,当然这不是必须的)

public abstract class DynamicDataSourceId {
    public static final String MASTER = "master";
    public static final String SLAVE1 = "slave1";
    public static final String SLAVE2 = "slave2";
    //... 可以继续无线扩展
    // 保存着有效的(调用者设置进来的)所有的DATA_SOURCE_IDS
    public static final List<String> DATA_SOURCE_IDS = new ArrayList();
    public static boolean containsDataSourceId(final String dataSourceId) {
        return dataSourceId != null && !dataSourceId.trim().isEmpty() ? DATA_SOURCE_IDS.contains(dataSourceId) : false;
    }
}


2、定义一个Holder,可以把数据源名称和当前线程绑定,提升易用性(当然也不是必须的)


public abstract class DynamicDataSourceContextHolder {
    //每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
    /**
     * 注意:使用静态方法setDataSourceId设置当前线程需要使用的数据源id(和当前线程绑定)
     */
    public static void setDataSourceId(final String dataSourceId) {
        CONTEXT_HOLDER.set(dataSourceId);
    }
    /**
     * 获取当前线程使用的数据源id
     */
    public static String getDataSourceId() {
        return CONTEXT_HOLDER.get();
    }
    /**
     * 清空当前线程使用的数据源id
     */
    public static void clearDataSourceId() {
        CONTEXT_HOLDER.remove();
    }
}


相关文章
|
7月前
|
安全 Java 应用服务中间件
Spring Boot + Java 21:内存减少 60%,启动速度提高 30% — 零代码
通过调整三个JVM和Spring Boot配置开关,无需重写代码即可显著优化Java应用性能:内存减少60%,启动速度提升30%。适用于所有在JVM上运行API的生产团队,低成本实现高效能。
892 3
|
6月前
|
人工智能 监控 Java
零代码改造 + 全链路追踪!Spring AI 最新可观测性详细解读
Spring AI Alibaba 通过集成 OpenTelemetry 实现可观测性,支持框架原生和无侵入探针两种方式。原生方案依赖 Micrometer 自动埋点,适用于快速接入;无侵入探针基于 LoongSuite 商业版,无需修改代码即可采集标准 OTLP 数据,解决了原生方案扩展性差、调用链易断链等问题。未来将开源无侵入探针方案,整合至 AgentScope Studio,并进一步增强多 Agent 场景下的观测能力。
2684 84
|
6月前
|
安全 Java 测试技术
《深入理解Spring》单元测试——高质量代码的守护神
Spring测试框架提供全面的单元与集成测试支持,通过`@SpringBootTest`、`@WebMvcTest`等注解实现分层测试,结合Mockito、Testcontainers和Jacoco,保障代码质量,提升开发效率与系统稳定性。
|
7月前
|
安全 IDE Java
Spring 的@FieldDefaults和@Data:Lombok 注解以实现更简洁的代码
本文介绍了如何在 Spring 应用程序中使用 Project Lombok 的 `@Data` 和 `@FieldDefaults` 注解来减少样板代码,提升代码可读性和可维护性,并探讨了其适用场景与限制。
275 0
Spring 的@FieldDefaults和@Data:Lombok 注解以实现更简洁的代码
|
10月前
|
Java 数据库连接 数据库
Spring boot 使用mybatis generator 自动生成代码插件
本文介绍了在Spring Boot项目中使用MyBatis Generator插件自动生成代码的详细步骤。首先创建一个新的Spring Boot项目,接着引入MyBatis Generator插件并配置`pom.xml`文件。然后删除默认的`application.properties`文件,创建`application.yml`进行相关配置,如设置Mapper路径和实体类包名。重点在于配置`generatorConfig.xml`文件,包括数据库驱动、连接信息、生成模型、映射文件及DAO的包名和位置。最后通过IDE配置运行插件生成代码,并在主类添加`@MapperScan`注解完成整合
1565 1
Spring boot 使用mybatis generator 自动生成代码插件
|
9月前
|
安全 Java Nacos
0代码改动实现Spring应用数据库帐密自动轮转
Nacos作为国内被广泛使用的配置中心,已经成为应用侧的基础设施产品,近年来安全问题被更多关注,这是中国国内软件行业逐渐迈向成熟的标志,也是必经之路,Nacos提供配置加密存储-运行时轮转的核心安全能力,将在应用安全领域承担更多职责。
|
10月前
|
Java 调度 流计算
基于Java 17 + Spring Boot 3.2 + Flink 1.18的智慧实验室管理系统核心代码
这是一套基于Java 17、Spring Boot 3.2和Flink 1.18开发的智慧实验室管理系统核心代码。系统涵盖多协议设备接入(支持OPC UA、MQTT等12种工业协议)、实时异常检测(Flink流处理引擎实现设备状态监控)、强化学习调度(Q-Learning算法优化资源分配)、三维可视化(JavaFX与WebGL渲染实验室空间)、微服务架构(Spring Cloud构建分布式体系)及数据湖建设(Spark构建实验室数据仓库)。实际应用中,该系统显著提升了设备调度效率(响应时间从46分钟降至9秒)、设备利用率(从41%提升至89%),并大幅减少实验准备时间和维护成本。
506 0
|
12月前
|
人工智能 前端开发 Java
十几行代码实现 Manus,Spring AI Alibaba Graph 快速预览
Spring AI Alibaba Graph 的核心开发已完成,即将发布正式版本。开发者可基于此轻松构建工作流、智能体及多智能体系统,功能丰富且灵活。文章通过三个示例展示了其应用:1) 客户评价处理系统,实现两级问题分类与自动处理;2) 基于 ReAct Agent 的天气预报查询系统,循环执行用户指令直至完成;3) 基于 Supervisor 多智能体的 OpenManus 实现,简化了流程控制逻辑并优化了工具覆盖度。此外,还提供了运行示例的方法及未来规划,欢迎开发者参与贡献。
|
缓存 监控 Java

热门文章

最新文章