基于spring通過(guò)多數(shù)據(jù)源實(shí)現(xiàn)多租戶(hù)應(yīng)用

背景

將您的 web 應(yīng)用程序轉(zhuǎn)化為多租戶(hù) SaaS 解決方案绢陌,介紹了將傳統(tǒng)應(yīng)用轉(zhuǎn)化為saas服務(wù)時(shí),需要實(shí)現(xiàn)的多租戶(hù)模型曾我。

多租戶(hù)模型.gif

簡(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>
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市毕泌,隨后出現(xiàn)的幾起案子喝检,更是在濱河造成了極大的恐慌,老刑警劉巖撼泛,帶你破解...
    沈念sama閱讀 221,273評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件挠说,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡坎弯,警方通過(guò)查閱死者的電腦和手機(jī)纺涤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)抠忘,“玉大人撩炊,你說(shuō)我怎么就攤上這事∑槁觯” “怎么了拧咳?”我有些...
    開(kāi)封第一講書(shū)人閱讀 167,709評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)囚灼。 經(jīng)常有香客問(wèn)我骆膝,道長(zhǎng),這世上最難降的妖魔是什么灶体? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,520評(píng)論 1 296
  • 正文 為了忘掉前任阅签,我火速辦了婚禮,結(jié)果婚禮上蝎抽,老公的妹妹穿的比我還像新娘政钟。我一直安慰自己,他們只是感情好樟结,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,515評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布养交。 她就那樣靜靜地躺著,像睡著了一般瓢宦。 火紅的嫁衣襯著肌膚如雪碎连。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,158評(píng)論 1 308
  • 那天驮履,我揣著相機(jī)與錄音鱼辙,去河邊找鬼廉嚼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛倒戏,可吹牛的內(nèi)容都是我干的前鹅。 我是一名探鬼主播,決...
    沈念sama閱讀 40,755評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼峭梳,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼舰绘!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起葱椭,我...
    開(kāi)封第一講書(shū)人閱讀 39,660評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤捂寿,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后孵运,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體秦陋,經(jīng)...
    沈念sama閱讀 46,203評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,287評(píng)論 3 340
  • 正文 我和宋清朗相戀三年治笨,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了驳概。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,427評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡旷赖,死狀恐怖顺又,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情等孵,我是刑警寧澤稚照,帶...
    沈念sama閱讀 36,122評(píng)論 5 349
  • 正文 年R本政府宣布,位于F島的核電站俯萌,受9級(jí)特大地震影響果录,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜咐熙,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,801評(píng)論 3 333
  • 文/蒙蒙 一弱恒、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧棋恼,春花似錦返弹、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,272評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)嘲玫。三九已至悦施,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間去团,已是汗流浹背抡诞。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,393評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工穷蛹, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人昼汗。 一個(gè)月前我還...
    沈念sama閱讀 48,808評(píng)論 3 376
  • 正文 我出身青樓肴熏,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親顷窒。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蛙吏,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,440評(píng)論 2 359