Spring實現多資料來源動態切換

weixin_33895657發表於2017-05-26

背景

隨著業務的發展,資料庫壓力的增大,如何分割資料庫的讀寫壓力是我們需要考慮的問題,而能夠動態的切換資料來源就是我們的首要目標。


基礎

Spring作為我們專案的應用容器,也對這方面提供了很好的支援,當我們的持久化框架需要資料庫連線時,我們需要做到動態的切換資料來源,這些Spring的AbstractRoutingDataSource都給我們留了擴充的空間,可以先來看看抽象類AbstractRoutingDataSource在獲取資料庫連線時做了什麼

private Map<Object, DataSource> resolvedDataSources; //從配置檔案讀取到的DataSources的Map
 
private DataSource resolvedDefaultDataSource; //預設資料來源
  
public Connection getConnection() throws SQLException {
   return determineTargetDataSource().getConnection();
}
 
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);
   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;
}
  
protected abstract Object determineCurrentLookupKey();

可以看到AbstractRoutingDataSource在決定目標資料來源的時候,會先呼叫determineCurrentLookupKey()方法得到一個key,我們通過這個key從配置好的resolvedDataSources(Map結構)拿到這次呼叫對應的資料來源,而determineCurrentLookupKey()開放出來讓我們實現


實現

前面提到我們可以通過實現AbstractRoutingDataSource的determineCurrentLookupKey()方法來決定這次呼叫的資料來源。首先說一下思路:當我們的一個執行緒需要針對資料庫做一系列操作時,每次都會去呼叫getConnection()方法獲取資料庫連線,然後執行完後再釋放或歸還資料庫連線(SqlSessionTemplate就是這麼做的),那麼很明顯,我們需要能夠保證每次呼叫Dao層方法時都能動態的切換資料來源,這就需要Spring的AOP:我們定義一個切面,當我們呼叫Dao層方法時,執行我們的邏輯來判斷這次呼叫的資料來源,AOP切面定義如下:

5721584-2b83d27e4eb60ec4.png
image.png
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PACKAGE, ElementType.TYPE, ElementType.METHOD})
public @interface DataSource {
    String value();
}
  
public Object doAround(ProceedingJoinPoint jp) throws Throwable {
 
    Signature signature = jp.getSignature();
 
    String dataSourceKey = getDataSourceKey(signature);
 
    if (StringUtils.hasText(dataSourceKey)) {
        MyDataSource.setDataSourceKey(dataSourceKey);
    }
 
    Object jpVal = jp.proceed();
 
    return jpVal;
}
  
public String getDataSourceKey(Signature signature) {
    if (signature == null) return null;
 
    if (signature instanceof MethodSignature) {
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        if (method.isAnnotationPresent(DataSource.class)) {
            return 方法的註解值;
        }
 
        Class declaringClazz = method.getDeclaringClass();
        if (declaringClazz.isAnnotationPresent(DataSource.class)) {
            return 類級別的註解值;
        }
 
        Package pkg = declaringClazz.getPackage();
        return 該包路徑的預設資料來源;
    }
 
    return null;
}

這裡我們就可以得到我們需要的資料來源,現在就是如何儲存這個值了,因為這個值是我們這個執行緒才需要使用的,所以綜合考慮宣告一個ThreadLocal<String>來儲存不同執行緒的不同值,目前已經解決了當前呼叫方法的資料來源和資料來源值的儲存了,那麼回到前面AbstractRoutingDataSource的determineTargetDataSource()方法中,我們就可以重寫抽象方法determineCurrentLookupKey,返回我們剛剛儲存的資料來源的值,程式碼如下:

public class MyDataSource extends AbstractRoutingDataSource {
 
    private static final ThreadLocal<String> dataSourceKey = new ThreadLocal<String>();
 
    public static void setDataSourceKey(String dataSource) {
        dataSourceKey.set(dataSource);
    }
 
    protected Object determineCurrentLookupKey() {
        String dsName = dataSourceKey.get();
        dataSourceKey.remove(); //這裡需要注意的時,每次我們返回當前資料來源的值得時候都需要移除ThreadLocal的值,這是為了避免同一執行緒上一次方法呼叫對之後呼叫的影響
        return dsName;
    }
 
}

總結

大體的Spring實現多資料來源的動態切換思路如上

相關文章