背景
將您的 web 應(yīng)用程序轉(zhuǎn)化為多租戶(hù) SaaS 解決方案绢陌,介紹了將傳統(tǒng)應(yīng)用轉(zhuǎn)化為saas服務(wù)時(shí),需要實(shí)現(xiàn)的多租戶(hù)模型曾我。
簡(jiǎn)單來(lái)說(shuō)就是以上三種:
1.租戶(hù)使用獨(dú)立的一套應(yīng)用服務(wù)和數(shù)據(jù)庫(kù)服務(wù)勤家。
實(shí)現(xiàn)思路:每個(gè)租戶(hù)有一個(gè)獨(dú)立的二級(jí)域名。通過(guò)二級(jí)域名訪問(wèn)單獨(dú)的應(yīng)用或應(yīng)用集群掠手,應(yīng)用訪問(wèn)該租戶(hù)獨(dú)立的數(shù)據(jù)庫(kù)實(shí)例憾朴。2.租戶(hù)合用一組應(yīng)用服務(wù)和單獨(dú)的數(shù)據(jù)庫(kù)服務(wù)。
實(shí)現(xiàn)思路:租戶(hù)在注冊(cè)時(shí)喷鸽,會(huì)分配一個(gè)租戶(hù)編碼众雷。當(dāng)租戶(hù)通過(guò)統(tǒng)一的域名訪問(wèn)系統(tǒng)時(shí),應(yīng)用會(huì)根據(jù)用戶(hù)的會(huì)話ID或Token信息做祝,感知用戶(hù)的租戶(hù)編碼砾省,并根據(jù)租戶(hù)編碼,將租戶(hù)對(duì)應(yīng)的數(shù)據(jù)源綁定到當(dāng)前請(qǐng)求中混槐。3.租戶(hù)合用一組應(yīng)用服務(wù)和單獨(dú)的數(shù)據(jù)庫(kù)服務(wù)纯蛾。
實(shí)現(xiàn)思路:租戶(hù)在注冊(cè)時(shí),會(huì)分配一個(gè)租戶(hù)編碼纵隔。當(dāng)租戶(hù)通過(guò)統(tǒng)一的域名訪問(wèn)系統(tǒng)時(shí)翻诉,應(yīng)用會(huì)根據(jù)用戶(hù)的會(huì)話ID或Token信息炮姨,感知用戶(hù)的租戶(hù)編碼,并根據(jù)租戶(hù)編碼碰煌,在數(shù)據(jù)庫(kù)找到對(duì)應(yīng)的單獨(dú)表舒岸,或是作為條件過(guò)濾租戶(hù)數(shù)據(jù)。
方案從1到3芦圾,數(shù)字越大資源使用率越高蛾派,但不同的實(shí)現(xiàn)方式還是要根據(jù)團(tuán)隊(duì)的實(shí)現(xiàn)情況及應(yīng)用的特點(diǎn)出發(fā)來(lái)選擇具體的實(shí)現(xiàn)方式。
比如:一個(gè)web應(yīng)用个少,以前是為用戶(hù)本地部署的方案來(lái)開(kāi)發(fā)的洪乍。 現(xiàn)在要將該應(yīng)用部署在公有云上,實(shí)現(xiàn)多租戶(hù)的應(yīng)用夜焦。如果選擇1壳澳,應(yīng)用基本不同改動(dòng),只是部署起來(lái)就會(huì)痛苦一些茫经。如果選擇3巷波,應(yīng)用就得做大的修改。所以折中一下卸伞,所以選擇了方案2抹镊。方案2對(duì)應(yīng)用最大的改造就是要實(shí)現(xiàn)一個(gè)多數(shù)據(jù)源,通過(guò)租戶(hù)編碼加載到對(duì)應(yīng)的數(shù)據(jù)源荤傲。(注:我們建立了一個(gè)主數(shù)據(jù)垮耳,通過(guò)租戶(hù)登錄的二級(jí)域名建立了與租戶(hù)編碼的映射關(guān)系。)
下面主要是講解一下如何在spring中實(shí)現(xiàn)多數(shù)據(jù)源的創(chuàng)建與綁定的關(guān)系遂黍。
實(shí)現(xiàn)
獲取租戶(hù)編碼
繼承一個(gè)org.springframework.web.servlet.handler.HandlerInterceptorAdapter類(lèi)终佛,攔截所有請(qǐng)求,通過(guò)請(qǐng)求的域名地址中獲取對(duì)應(yīng)的租戶(hù)編碼妓湘。并設(shè)置到當(dāng)前線程的變量中(ThreadLocal)查蓉。
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
public class GoingRestInterceptor extends HandlerInterceptorAdapter {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
...
initTenantCodeInServlet(request);
...
public void initTenantCodeInRest(String path) {
path = path.replaceAll("http://www.", "").replaceAll("https://www.", "").replaceAll("http://", "").replaceAll("https://", "");
String[] arr = path.split("/");
String post = arr[0];
System.out.println("--=-=-=-=post="+post);
String[] postArray = post.split("\\.");
String tenantCode = "";
if(postArray.length == 3){
if(StringUtils.isEmpty(postArray[0])){
throw new BaseException("域名異常");
}else{
tenantCode = postArray[0];
}
}
//GoingRequestContext是一個(gè)基于ThreadLocal的實(shí)現(xiàn)類(lèi)
GoingRequestContext.setTenantCode(tenantCode);
}
}
編寫(xiě)完成以后乌询,在springmvc的配置中配置該攔截器榜贴。
<mvc:interceptors>
<bean class="GoingRestInterceptor" />
</mvc:interceptors>
動(dòng)態(tài)路由數(shù)據(jù)源
根據(jù)請(qǐng)求上下文中的租戶(hù)編碼獲取對(duì)應(yīng)的數(shù)據(jù)源。
繼承org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource類(lèi)妹田,實(shí)現(xiàn)一個(gè)動(dòng)態(tài)數(shù)據(jù)源類(lèi)DynamicDataSource唬党,替換原有的數(shù)據(jù)源實(shí)現(xiàn)類(lèi)。
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class MultiDataSrouce extends AbstractRoutingDataSource {
// 保存動(dòng)態(tài)創(chuàng)建的數(shù)據(jù)源
private static final Map<String, DataSource> targetDataSource = new HashMap<String, DataSource>();
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
// TODO Auto-generated method stub
return null;
}
@Override
protected DataSource determineTargetDataSource() {
// TODO Auto-generated method stub
return null;
}
@Override
protected String determineCurrentLookupKey() {
// TODO Auto-generated method stub
return null;
}
}
在該子類(lèi)中鬼佣,重載父類(lèi)的determineCurrentLookupKey方法驶拱,通過(guò)當(dāng)前線程中的租戶(hù)編碼作為查詢(xún)數(shù)據(jù)源的關(guān)鍵字。
@Override
protected String determineCurrentLookupKey() {
return GoingRequestContext.getTenantCode();
}
在該子類(lèi)中晶衷,重載父類(lèi)的determineTargetDataSource方法蓝纲,實(shí)現(xiàn)動(dòng)態(tài)獲取數(shù)據(jù)源的邏輯阴孟。
@Override
protected DataSource determineTargetDataSource() {
// 根據(jù)數(shù)據(jù)庫(kù)選擇方案,拿到要訪問(wèn)的數(shù)據(jù)庫(kù)
String dataSourceName = determineCurrentLookupKey();
// 根據(jù)數(shù)據(jù)庫(kù)名字税迷,從已創(chuàng)建的數(shù)據(jù)庫(kù)中獲取要訪問(wèn)的數(shù)據(jù)庫(kù)
DataSource dataSource = (DataSource) targetDataSource.get(dataSourceName);
if (null == dataSource) {
// 從已創(chuàng)建的數(shù)據(jù)庫(kù)中獲取要訪問(wèn)的數(shù)據(jù)庫(kù)永丝,如果沒(méi)有則創(chuàng)建一個(gè)
dataSource = this.selectDataSource(dataSourceName);
}
return dataSource;
}
其中selectDataSource方法的實(shí)現(xiàn)如下:
/**
* 該方法為同步方法,防止并發(fā)創(chuàng)建兩個(gè)相同的數(shù)據(jù)庫(kù) 使用雙檢鎖的方式箭养,防止并發(fā)
*
* @param dsName
* @return
*/
private synchronized DataSource selectDataSource(String dsName) {
// 再次從數(shù)據(jù)庫(kù)中獲取慕嚷,雙檢鎖
DataSource obj = (DataSource) this.targetDataSource.get(dsName);
if (null != obj) {
return obj;
}
// 為空則創(chuàng)建數(shù)據(jù)庫(kù)
DataSource dataSource = this.findAndCreateDataSource(dsName);
if (null != dataSource) {
// 將新創(chuàng)建的數(shù)據(jù)庫(kù)保存到map中
this.targetDataSource.put(dsName, dataSource);
return dataSource;
} else {
throw new BaseException("沒(méi)有相關(guān)租戶(hù)");//創(chuàng)建數(shù)據(jù)源失敗
}
}
其中的findAndCreateDataSource方法的實(shí)現(xiàn)為:
/**
* 查詢(xún)對(duì)應(yīng)數(shù)據(jù)庫(kù)的信息
*
* @param dsName
* @return
*/
public DataSource findAndCreateDataSource(String dsName) {
//找到租戶(hù)的數(shù)據(jù)源配置信息
HashMap<String, Datasource> map = FrameworkGlobalMap.getDatasources();
Datasource datasource = map.get(dsName);
if (datasource == null) {
return null;
}
DataSourceCreator dataSourceCreator = GoingInstanceFactory.getInstance(DataSourceCreator.class);
dataSourceCreator.createC3p0DataSource(datasource);
DataSource dataSource = (DataSource)GoingInstanceFactory.getInstance(dsName);
return dataSource;
}
其中的DataSourceCreator是一個(gè)動(dòng)態(tài)創(chuàng)建數(shù)據(jù)源的實(shí)現(xiàn)類(lèi),主要的方法實(shí)現(xiàn)如下所示:
public void createC3p0DataSource(Datasource datasource) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(ComboPooledDataSource.class);
builder.addPropertyValue("driverClass", datasource.getDriverClass());
builder.addPropertyValue("jdbcUrl", datasource.getJdbcUrl());
LogUtils.getSingleton().debug(datasource.getJdbcUrl());
builder.addPropertyValue("user", datasource.getUser());
builder.addPropertyValue("password", datasource.getPassword());
if (datasource.getIdleConnectionTestPeriod() != null)
builder.addPropertyValue("idleConnectionTestPeriod", datasource.getIdleConnectionTestPeriod());
if (datasource.getInitialPoolSize() != null)
builder.addPropertyValue("initialPoolSize", datasource.getInitialPoolSize());
if (datasource.getMaxIdleTime() != null)
builder.addPropertyValue("maxIdleTime", datasource.getMaxIdleTime());
if (datasource.getMaxPoolSize() != null)
builder.addPropertyValue("maxPoolSize", datasource.getMaxPoolSize());
if (datasource.getMinPoolSize() != null)
builder.addPropertyValue("minPoolSize", datasource.getMinPoolSize());
this.beanFactory.registerBeanDefinition(datasource.getDatasourceName(), builder.getBeanDefinition());
}
最后在spring的配置中替換原有datasource:
<bean id="dataSource" class="DynamicDataSource">
<property name="masterDataSource" ref="masterDataSource"/>
</bean>