如何构建动态数据源

117 阅读3分钟

首先,我们先了解一下动态数据源路由的概念,即:能够将注入的DataSource根据租户使用的不同而使用不同的数据源,同时可根据租户的信息动态的增减数据源,并且还能实时生效。

如何设计一个动态数据源

spring是如何管理动态数据源

  1. 在application.yml中配置一个多数据源,类似于下图:

image.png

  1. 配置一个注解的bean,把配置文件中的内容读取出来,类似于下图:

image.png

  1. 根据不同的请求,分别去请求master和slave

那我们思考一个问题,以上方式有什么缺陷吗?

  1. 数据源只能写在文件中,每次修改都需要重启。
  2. 注解的bean代码也是写死的,修改配置文件的同时也要修改代码,并且还需要重启。

那我们如何解决以上的问题呢?

数据源配置信息存储

把从配置文件存储,改成DB存储或配置中心存储都可以,这样的话我们就可以动态的改变数据源的配置信息,并且能被程序捕捉到。

如何灵活的不用重启服务的方式更新数据源呢?

首先,我们需要看看,spring的动态数据源的原理是什么? 先看看入口类:DynamicDataSource,继承了父类AbstractRoutingDataSource,父类继承了InitializingBean接口,所以在启动项目时,会去进行初始化动作,把配在yaml中的配置数据源信息加载进来,并且在AbstractRoutingDataSource中,存放了关键的几个Map对象,存放了dataSources相关的内容。 启动方法后,会将配置的多个数据源信息,统统放到这里,通过getConnection()方法对外提供数据源。

这种方式上是否存在一定缺陷?

AbstractRoutingDataSource关键源码分析 先看看UML图 image.png

从UML图中,我们可以看到,是实现了DataSource接口,所以可以做各种DB查询的扩展。

再看看关键源码

public void afterPropertiesSet() { 
    // 判断目标数据源是否为空 
    if (this.targetDataSources == null) { 
        throw new IllegalArgumentException("Property 'targetDataSources' is required"); 
    } else { 
        // 先把实际对外调用的source置空 
        this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size()); 
        // 而后遍历目标数据源,把目标数据源的数据put到实际对外调用的数据源中。 
        this.targetDataSources.forEach((key, value) -> { 
            Object lookupKey = this.resolveSpecifiedLookupKey(key); 
            DataSource dataSource = this.resolveSpecifiedDataSource(value); 
            this.resolvedDataSources.put(lookupKey, dataSource); 
        }); 
        if (this.defaultTargetDataSource != null) { 
            this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource); 
        } 
    }
}
// 获取数据源
protected DataSource determineTargetDataSource() { 
    Assert.notNull(this.resolvedDataSources, "DataSource router not initialized"); 
    // 获取key 
    Object lookupKey = this.determineCurrentLookupKey(); 
    // 从resolvedDataSources 获取datasource 
    DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey); 
    if (dataSource == null && (this.lenientFallback || lookupKey == null)) { dataSource = this.resolvedDefaultDataSource; 
    } if (dataSource == null) { 
    throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]"); 
    } else { 
    return dataSource; 
    }
}

@Nullable
protected abstract Object determineCurrentLookupKey();

以上有两个问题:

  1. 只能项目启动时初始化;
  2. 在于this.resolvedDataSources = CollectionUtils.newHashMap方法会重新new一个Map类出来,这就会导致如果是在并发度较高的情况下,会发生找不到数据源的问题;或者正在查询的时候,发起了删除,产生不安全的隐患。

如何解决源码中的问题

另起炉灶

UML设计图 image.png

AbstractDynamicDataSource

// 构造底层map 使用ConcurrentHashMap代替普通的HashMap
protected Map<Object, T> resolvedDataSources = new ConcurrentHashMap<>();

protected abstract void resolveSpecifiedDataSource(Object lookupKey, Object dataSource) throws IllegalArgumentException;

@Override
public void afterPropertiesSet() { 
    if (this.targetDataSources == null) { 
        throw new IllegalArgumentException("Property 'targetDataSources' is required"); 
    } 
    // 替换实际数据源  这里直接替换,不需要再重新new一个Map
    this.targetDataSources.forEach((key, value) -> { 
        Object lookupKey = resolveSpecifiedLookupKey(key); 
        resolveSpecifiedDataSource(lookupKey, value); 
    });
}

@Override 
protected void resolveSpecifiedDataSource(Object lookupKey, Object dataSource) throws IllegalArgumentException { 
    // 判断删除标记,并关闭老的source,并删除数据源标记 
    if (dataSource instanceof Integer && ((int) dataSource) == GenericDataSource.DELETE_FLAG) { 
        DataSource source = resolvedDataSources.remove(lookupKey); 
        close(source); 
        removeLookupKey(lookupKey); 
    } 
    // 变更,并关闭老的source,新增数据源标记 
    else { 
        DataSource curSource = resolveSpecifiedDataSource(dataSource); 
        DataSource oldSource = resolvedDataSources.get(lookupKey); 
        resolvedDataSources.put(lookupKey, curSource); 
        close(oldSource); 
        addLookupKey(lookupKey); 
    } 
} 

protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException { 
    if (dataSource instanceof DataSource) { 
        return (DataSource) dataSource; 
    } else if (dataSource instanceof String) { 
        return dataSourceLookup.getDataSource((String) dataSource); 
    } else { 
        throw new IllegalArgumentException( "Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource); 
    } 
}

这样就能完美解决,因数据源变更导致的问题。

那如何解决动态问题呢

很多方案,比如,简单一点的就是定时查询DB的变更,然后发起对动态数据源的维护; 复杂一点,可以实时监听,这里就不赘述。