1. 程式人生 > >Spring多數據源、動態數據源源碼解析

Spring多數據源、動態數據源源碼解析

outer ecif tee web .data 多個 ava void 基於

在Java中所有的連接池都按照規範實現DataSource接口,在獲取連接的時候即可通過getConnection()獲取連接而不用關系底層究竟是何數據庫連接池。

1 public interface DataSource extends CommonDataSource, Wrapper {
2
3 Connection getConnection() throws SQLException;
4
5 Connection getConnection(String username, String password) throws SQLException;
6 }
在大多數系統中我們只需要一個數據源,而現在WEB系統通常是Spring為基石。不管你是xml配置,javaBean配置還是yml,properties配置文件配置,其核心就是註入一個數據源交給spring的進行管理。

而在部分系統中我們可能會面臨一些情況,連接多個表,主從,甚至多個不同的庫等等情況,核心需求就是我們可能需要配置多個連接池。

在mybatis系統中我們使用多數據源可以配置配置多個DataSource,SqlSessionFactory,SqlSessionTemplate,然後在xml和mapper也分開管理。
這種方案在小的系統足夠使用,作者認為更適合於多個不同的數據庫。

回歸正題,在Spring中從2.0.1版本默認提供了AbstractRoutingDataSource,我們繼承它實現相關方法,把所有需要的數據源設置進去即可動態的切換數據源。我們可以看下核心方法的源碼。

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {

?
//設置所有的數據源
private Map<Object, Object> targetDataSources;
//設置默認的數據源,在沒有找到相關數據源的時候會返回默認數據源
private Object defaultTargetDataSource;
//快速失敗,可忽略
private boolean lenientFallback = true;
//Jndi相關,可忽略
private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
//經過解析後的所有數據源,核心
private Map<Object, DataSource> resolvedDataSources;
//經過解析後的默認數據源,核心
private DataSource resolvedDefaultDataSource;
?
//設置相關參數方法
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
this.targetDataSources = targetDataSources;
}
public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
this.defaultTargetDataSource = defaultTargetDataSource;
}
public void setLenientFallback(boolean lenientFallback) {
this.lenientFallback = lenientFallback;
}
public void setDataSourceLookup(DataSourceLookup dataSourceLookup) {
this.dataSourceLookup = (dataSourceLookup != null ? dataSourceLookup : new JndiDataSourceLookup());}
?
@Override
br/>}
?
@Override
//檢測是否設置所有的數據源
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property ‘targetDataSources‘ is required");
}
//解析所有數據源,一般沒什麽用,主要是如果Map<Object, Object> targetDataSources的value是string則會從Jndi數據源查找
this.resolvedDataSources = new HashMap<Object, DataSource>(this.targetDataSources.size());
for (Map.Entry<Object, Object> entry : this.targetDataSources.entrySet()) {
Object lookupKey = resolveSpecifiedLookupKey(entry.getKey());
DataSource dataSource = resolveSpecifiedDataSource(entry.getValue());
this.resolvedDataSources.put(lookupKey, dataSource);
}
//同上解析默認數據源
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);}
}
?
@Override
br/>}
}
?
@Override
//核心,獲取數據源先查找當前連接池再獲取數據源
return determineTargetDataSource().getConnection();}
?
@Override
br/>}
?
@Override
return determineTargetDataSource().getConnection(username, password);
}
?
protected DataSource determineTargetDataSource() {
//調用determineCurrentLookupKey,然後去resolvedDefaultDataSource查找,有就返回對應數據源,沒有返回默認數據源
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = determineCurrentLookupKey();
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 + "]");
}
return dataSource;
}

//而determineCurrentLookupKey需要我們自己去實現。
//通常需要結合Aop和ThreadLocal。我們Aop從註解上獲取當前用戶用戶所希望的數據源,然後設置到當前線程。在determineCurrentLookupKey再從當前線程拿出來返回給determineTargetDataSource由其決定最終數據源
protected abstract Object determineCurrentLookupKey();

?
}

優點:方便配置,便捷使用。 缺點:默認實現有一定局限性,大多數人足夠使用。 如果你有更復雜的使用場景,多庫數據源,分組數據源,多主多從等等較復雜場景可以嘗試

一個基於springboot的快速集成多數據源的啟動器。

一個標準的主從的配置如下,引入相關配置即可使用。更多使用的方式查看相關文檔。

spring:
datasource:
dynamic:
primary: master #設置默認的數據源或者數據源組,默認值即為master,如果你主從默認下主庫的名稱就是master可不定義此項。
datasource:
master:
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://47.100.20.186:3306/dynamic?characterEncoding=utf8&useSSL=false
slave_1:
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://47.100.20.186:3307/dynamic?characterEncoding=utf8&useSSL=false
slave_2:
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://47.100.20.186:3308/dynamic?characterEncoding=utf8&useSSL=false

實現核心源碼如下

public class DynamicRoutingDataSource extends AbstractRoutingDataSource {

/**
 * 所有庫
 */
private Map&lt;String, DataSource&gt; dataSourceMap;

/**
 * 分組數據庫
 */
private Map&lt;String, DynamicGroupDatasource&gt; groupDataSources = new HashMap&lt;&gt;();

@Setter
private DynamicDataSourceProvider dynamicDataSourceProvider;

@Setter
private Class&lt;? extends DynamicDataSourceStrategy&gt; dynamicDataSourceStrategyClass;

/**
 * 默認數據源名稱,默認master,可為組數據源名,可為單數據源名
 */
@Setter
private String primary;

@Override
protected Object determineCurrentLookupKey() {
    return DynamicDataSourceContextHolder.getDataSourceLookupKey();
}

@Override
protected DataSource determineTargetDataSource() {
    String lookupKey = (String) determineCurrentLookupKey();
    if (groupDataSources.containsKey(lookupKey)) {
        log.debug("從 {} 組數據源中返回數據源", lookupKey);
        return groupDataSources.get(lookupKey).determineDataSource();
    } else if (dataSourceMap.containsKey(lookupKey)) {
        log.debug("從 {} 單數據源中返回數據源", lookupKey);
        return dataSourceMap.get(lookupKey);
    }
    log.debug("從默認數據源中返回數據");
    return groupDataSources.containsKey(primary) ? groupDataSources.get(lookupKey).determineDataSource() : dataSourceMap.get(primary);
}

@Override
public void afterPropertiesSet() {
    this.dataSourceMap = dynamicDataSourceProvider.loadDataSources();
    log.debug("共加載 {} 個數據源", dataSourceMap.size());
    //分組數據源
    for (Map.Entry&lt;String, DataSource&gt; dsItem : dataSourceMap.entrySet()) {
        String dsName = dsItem.getKey();
        if (dsName.contains("_")) {
            String[] groupDs = dsName.split("_");
            String groupName = groupDs[0];
            DataSource dataSource = dsItem.getValue();
            if (groupDataSources.containsKey(groupName)) {
                groupDataSources.get(groupName).addDatasource(dataSource);
            } else {
                try {
                    DynamicGroupDatasource groupDatasource = new DynamicGroupDatasource(groupName, dynamicDataSourceStrategyClass.newInstance());
                    groupDatasource.addDatasource(dataSource);
                    groupDataSources.put(groupName, groupDatasource);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
    //檢測組數據源設置
    Iterator&lt;Map.Entry&lt;String, DynamicGroupDatasource&gt;&gt; groupIterator = groupDataSources.entrySet().iterator();
    while (groupIterator.hasNext()) {
        Map.Entry&lt;String, DynamicGroupDatasource&gt; item = groupIterator.next();
        log.debug("組 {} 下有 {} 個數據源", item.getKey(), item.getValue().size());
        if (item.getValue().size() == 1) {
            log.warn("請註意不要設置一個只有一個數據源的組,{} 組將被移除", item.getKey());
            groupIterator.remove();
        }
    }
    //檢測默認數據源設置
    if (groupDataSources.containsKey(primary)) {
        log.debug("當前的默認數據源是組數據源,組名為 {} ,其下有 {} 個數據源", primary, groupDataSources.size());
    } else if (dataSourceMap.containsKey(primary)) {
        log.debug("當前的默認數據源是單數據源,數據源名為{}", primary);
    } else {
        throw new RuntimeException("請檢查primary默認數據庫設置,當前未找到" + primary + "數據源");
    }
}

}

Spring多數據源、動態數據源源碼解析