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();
    }
}


相关文章
|
4月前
|
Ubuntu Java Linux
在Spring Boot中使用iTextPDF创建动态PDF文档
iTextPDF 是一个用于创建和操作 PDF(Portable Document Format)文档的流行的 Java 库。它提供了一套全面的功能,用于处理 PDF 文件,包括创建新文档、修改现有文档以及提取信息。
97 1
|
4月前
|
移动开发 前端开发 Java
使用ipaguard插件对Spring Boot程序进行代码混淆
使用ipaguard插件对Spring Boot程序进行代码混淆
47 0
|
1月前
|
安全 数据安全/隐私保护
Springboot+Spring security +jwt认证+动态授权
Springboot+Spring security +jwt认证+动态授权
|
15天前
|
Java 关系型数据库 数据库
Spring Boot多数据源及事务管理:概念与实战
【4月更文挑战第29天】在复杂的企业级应用中,经常需要访问和管理多个数据源。Spring Boot通过灵活的配置和强大的框架支持,可以轻松实现多数据源的整合及事务管理。本篇博客将探讨如何在Spring Boot中配置多数据源,并详细介绍事务管理的策略和实践。
38 3
|
1月前
|
Java 数据库连接 Spring
Spring多数据源配置
Spring多数据源配置
|
2月前
|
Java 数据库连接 数据库
Spring Boot整合MyBatis Plus集成多数据源轻松实现数据读写分离
Spring Boot整合MyBatis Plus集成多数据源轻松实现数据读写分离
32 2
|
2月前
|
存储 Java 关系型数据库
Spring Batch学习记录及示例项目代码
Spring Batch学习记录及示例项目代码
|
3月前
|
IDE Java 应用服务中间件
基于Spring+mybatis的SSM超市消费积分管理系统代码实现含演示站
这是一个SSM超市消费积分管理系统。有2个角色:买家角色和管理员角色,现在开始分角色介绍下功能。买家角色核心功能有买家登录,查看网站首页,查看蔬菜详情,加入购物车,提交订单,查看我的订单。管理员角色核心功能有管理员登录,用户管理,管理员管理,商品管理,一级分类管理,二级分类管理,订单管理。更多的功能可以去演示站查看。
基于Spring+mybatis的SSM超市消费积分管理系统代码实现含演示站
|
4月前
|
Java Maven 数据安全/隐私保护
代码优雅升级,提升开发效率:挖掘Spring AOP配置的学习宝藏!
代码优雅升级,提升开发效率:挖掘Spring AOP配置的学习宝藏!
|
4月前
|
存储 NoSQL Java
Spring Boot动态秒杀系统接口安全性设计与实现
Spring Boot动态秒杀系统接口安全性设计与实现
64 0