Qicz’s Thoughts HUB

The creative and technical writing. Do more, challenge more, know more, be more.

Spring动态数据源原理

提到Spring的动态数据源重点在于对AbstractRoutingDataSource实现及工作原理的认知。

先从Spring的DataSource开始说起,而说到DataSource,用最原始的jdbc来说明可能会更容易理解了。

在使用jdbc时,要进行一个sql操作,我们需要这样的一个链路:

Statement <- Connection <- DataSource

也就是根源上就是获取一个DataSource,这就是关联到一个具体源的对象。那么切换源,从原理上来讲就是切换这个DataSource对象了。

那么,下一步,来看看这个AbstractRoutingDataSource的工作原理是不是就是这样的呢?这里摘取它的部分实现源码:

 1@Nullable
 2private Map<Object, Object> targetDataSources;
 3
 4@Nullable
 5private Object defaultTargetDataSource;
 6...
 7@Override
 8public void afterPropertiesSet() {
 9  if (this.targetDataSources == null) {
10    throw new IllegalArgumentException("Property 'targetDataSources' is required");
11  }
12  this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());
13  this.targetDataSources.forEach((key, value) -> {
14    Object lookupKey = resolveSpecifiedLookupKey(key);
15    DataSource dataSource = resolveSpecifiedDataSource(value);
16    this.resolvedDataSources.put(lookupKey, dataSource);
17  });
18  if (this.defaultTargetDataSource != null) {
19    this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
20  }
21}
22...
23@Override
24public Connection getConnection() throws SQLException {
25  return determineTargetDataSource().getConnection();
26}
27
28@Override
29public Connection getConnection(String username, String password) throws SQLException {
30  return determineTargetDataSource().getConnection(username, password);
31}
32...
33/**
34 * Retrieve the current target DataSource. Determines the
35 * {@link #determineCurrentLookupKey() current lookup key}, performs
36 * a lookup in the {@link #setTargetDataSources targetDataSources} map,
37 * falls back to the specified
38 * {@link #setDefaultTargetDataSource default target DataSource} if necessary.
39 * @see #determineCurrentLookupKey()
40 */
41protected DataSource determineTargetDataSource() {
42  Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
43  Object lookupKey = determineCurrentLookupKey();
44  DataSource dataSource = this.resolvedDataSources.get(lookupKey);
45  if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
46    dataSource = this.resolvedDefaultDataSource;
47  }
48  if (dataSource == null) {
49    throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
50  }
51  return dataSource;
52}
53
54/**
55	 * Determine the current lookup key. This will typically be
56	 * implemented to check a thread-bound transaction context.
57	 * <p>Allows for arbitrary keys. The returned key needs
58	 * to match the stored lookup key type, as resolved by the
59	 * {@link #resolveSpecifiedLookupKey} method.
60	 */
61@Nullable
62protected abstract Object determineCurrentLookupKey();

现在来进行一一的解读。

  • afterPropertiesSet是啥?这就不细说了。看源码就是解析targetDataSources然后放到resolvedDataSources
  • getConnection获取一个Connection对象,这里和jdbc就是一个玩意了;
  • determineTargetDataSource确定目标的DataSource,在这个方法中有这样一行Object lookupKey = determineCurrentLookupKey();,而这个就是调用了一个determineCurrentLookupKey方法;
  • determineCurrentLookupKey,一个抽象方法,返回一个当前处理的源的id或者说key

从源码中看出,AbstractRoutingDataSource的工作原理,大致就是,从一个Map的源集合中,可以一个当前处理的源Key来切换选择对应的DataSource,从而完成对应源的切换。

下面,从具体实现上来做解析,首先看看AbstractRoutingDataSource的定义

1public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean

这是一个抽象class,那么要用他,必然要实现一个实例class,如下。

1public class DynamicDataSource extends AbstractRoutingDataSource

那么重点就是对抽象方法determineCurrentLookupKey的处理和源Map的构建和操作了。

要知道,在实际应用中,应用都是工作在多线程环境的,那么对源的切换就变成了在多线程环境下的切换,而要保存多线程环境下的切换安全,我们自然而然想到了ThreadLocal。是的,我们就是借助它,来实现和完成在不同源的CurrentLookupKey的管理。

先看看ThreadLocal的处理:

 1public class DynamicDataSourceContextHolder {
 2    private DynamicDataSourceContextHolder() {
 3    }
 4  
 5    private static final ThreadLocal<String> dynamicDataSourceContextHolder = new ThreadLocal<String>();
 6
 7    public static void setCurrentDataSource(String dataSourceKey) {
 8        dynamicDataSourceContextHolder.set(dataSourceKey);
 9    }
10
11    public static String getCurrentDataSource() {
12        return dynamicDataSourceContextHolder.get();
13    }
14
15    public static void clearCurrentDataSource() {
16        dynamicDataSourceContextHolder.remove();
17    }
18}

实现很简单,就是使用ThreadLocal来管理key,这里的key是String类型的。在切换时,调用setCurrentDataSource,使用完成在调用clearCurrentDataSource,使用getCurrentDataSource来获取当前设置的key。

接着看在DynamicDataSource中的处理:

1@Override
2protected Object determineCurrentLookupKey() {
3    return DynamicDataSourceContextHolder.getCurrentDataSource();
4}

这样整个都串起来了:

实现继承AbstractRoutingDataSource的类DynamicDataSource,实现determineCurrentLookupKey方法,这个方法从ThreadLocal中拿到当前线程处理的源的key信息,在AbstractRoutingDataSourcedetermineTargetDataSource中根据这个key拿到在源Map中已经配置好的源DataSource,拿到了DataSource,再要拿到Connection还会远吗?^_^

剩下的就是构造一个有源Map管理(put,get)的DynamicDataSource并注入到IOC,就ok咯。

最后,可以在DynamicDataSource加入一个销毁DataSource的方法,避免应用关闭后,仍然占用资源。

1@PreDestroy
2public void closeDataSources(){
3  // 从Map<Object, Object> targetDataSources遍历获取对应的DataSource并关闭
4}