背景
隨著業(yè)務(wù)的發(fā)展,數(shù)據(jù)庫壓力的增大,如何分割數(shù)據(jù)庫的讀寫壓力是我們需要考慮的問題,而能夠動(dòng)態(tài)的切換數(shù)據(jù)源就是我們的首要目標(biāo)。
基礎(chǔ)
Spring作為我們項(xiàng)目的應(yīng)用容器,也對這方面提供了很好的支持,當(dāng)我們的持久化框架需要數(shù)據(jù)庫連接時(shí),我們需要做到動(dòng)態(tài)的切換數(shù)據(jù)源,這些Spring的AbstractRoutingDataSource都給我們留了拓展的空間,可以先來看看抽象類AbstractRoutingDataSource在獲取數(shù)據(jù)庫連接時(shí)做了什么
private Map<Object, DataSource> resolvedDataSources; //從配置文件讀取到的DataSources的Map
private DataSource resolvedDefaultDataSource; //默認(rèn)數(shù)據(jù)源
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在決定目標(biāo)數(shù)據(jù)源的時(shí)候,會先調(diào)用determineCurrentLookupKey()方法得到一個(gè)key,我們通過這個(gè)key從配置好的resolvedDataSources(Map結(jié)構(gòu))拿到這次調(diào)用對應(yīng)的數(shù)據(jù)源,而determineCurrentLookupKey()開放出來讓我們實(shí)現(xiàn)
實(shí)現(xiàn)
前面提到我們可以通過實(shí)現(xiàn)AbstractRoutingDataSource的determineCurrentLookupKey()方法來決定這次調(diào)用的數(shù)據(jù)源。首先說一下思路:當(dāng)我們的一個(gè)線程需要針對數(shù)據(jù)庫做一系列操作時(shí),每次都會去調(diào)用getConnection()方法獲取數(shù)據(jù)庫連接,然后執(zhí)行完后再釋放或歸還數(shù)據(jù)庫連接(SqlSessionTemplate就是這么做的),那么很明顯,我們需要能夠保證每次調(diào)用Dao層方法時(shí)都能動(dòng)態(tài)的切換數(shù)據(jù)源,這就需要Spring的AOP:我們定義一個(gè)切面,當(dāng)我們調(diào)用Dao層方法時(shí),執(zhí)行我們的邏輯來判斷這次調(diào)用的數(shù)據(jù)源,AOP切面定義如下:

@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 該包路徑的默認(rèn)數(shù)據(jù)源;
}
return null;
}
這里我們就可以得到我們需要的數(shù)據(jù)源,現(xiàn)在就是如何保存這個(gè)值了,因?yàn)檫@個(gè)值是我們這個(gè)線程才需要使用的,所以綜合考慮聲明一個(gè)ThreadLocal<String>來保存不同線程的不同值,目前已經(jīng)解決了當(dāng)前調(diào)用方法的數(shù)據(jù)源和數(shù)據(jù)源值的保存了,那么回到前面AbstractRoutingDataSource的determineTargetDataSource()方法中,我們就可以重寫抽象方法determineCurrentLookupKey,返回我們剛剛保存的數(shù)據(jù)源的值,代碼如下:
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(); //這里需要注意的時(shí),每次我們返回當(dāng)前數(shù)據(jù)源的值得時(shí)候都需要移除ThreadLocal的值,這是為了避免同一線程上一次方法調(diào)用對之后調(diào)用的影響
return dsName;
}
}
總結(jié)
大體的Spring實(shí)現(xiàn)多數(shù)據(jù)源的動(dòng)態(tài)切換思路如上