基於Hibernate實現多租戶(Multi-Tendency)功能

獲取源碼&查看在線Demo請移步《 使用Hibernate多租戶實現SaaS服務》


這幾天的研究,四處搜尋資料,基本理清實現多租戶的一個思路。至於多租戶是什麼可參考《淺析多租戶在 Java 平臺和某些 PaaS 上的實現》。裏面提到了很多激動人心的是JavaEE8會加入對多租戶的支持。但是這個真的不知道要等到什麼時候了。


我所維護的一個系統是基於Hibernate的,現在準備修改架構,希望能夠提供Saas服務。這樣一個升級,首先想到的是,數據庫的數據會急劇增加,面對大數據的時候,首先的選擇估計會扔掉Hibernate,使用純的JDBC或者用MyBatis。

如果使用JDBC那就完全自由了,但自由確實自由了,現有的整個系統是以Hibernate爲基礎的,扔掉Hibernate的前提是你要自己來實現至少用起來還算湊合的一個DAO。我沒有找到市面上有這麼一種JDBC框架,方便使用者進行分表、分庫等操作。

另外我還看到EclipseLink也可以實現多租戶功能,但據說他的社區比Hibernate差多了,也只能作罷。


所以,還是回到了這篇文章上來了:《數據層的多租戶淺談》(看完了記得回來,我這裏有補充)。

這裏講到了Hibernate在4.0的時候就已經支持多租戶了,實現起來分幾種方式
1.一個租戶一個單獨數據庫(DATABASE)-注意,是物理意義上的獨立,可以理解爲不同的數據源
2.多個租戶公用一個數據源,但每個租戶有不同的數據庫(SCHEMA);
3.多個租戶都在一個數據庫裏,數據也完全在一起,通過一個字段(列)來區分(DISCRIMINATOR);

我在看上面那篇引文的時候對這三種的描述上是有誤解的,我以爲:
DATABASE:不同數據庫,也許是同一個數據源上的
SCHEMA:對多個租戶進行分表(但從頭到尾,沒有看到任何地方用分表的方式實現過多租戶)

說說我對這三種情況如何選擇的想法。
首先如果你的數據量不大那你完全可以使用DISCRIMINATOR方式,也就是把所有數據都放到一個表裏面。這樣做的前提是,你是在從無到有新建一個項目。否則,將不支持多租戶的系統改造成多租戶時不建議這麼做。因爲這有可能會影響到你程序的方方面面,你要改的代碼也許不計其數。
第二種,分數據庫(SCHEMA)。反正我是選擇的這種方式,因爲多租戶首先考慮的是有可能數據量會很大,相對於DISCRIMINATOR方式而言,這種分數據庫其實就是分表了。另外,對於數據的備份、遷移都是非常方便的。
最後一種,分數據源(DATABASE)。我覺得這完全可以和SCHEMA方式配合起來用,作爲它的一種補充,什麼時候需要補充?當一個數據源上的數據庫多到嚴重影響性能的時候,就可以考慮分多個數據源了。


好了說了這麼多,直接上代碼吧,首先,Hibernate給了我們一個確定tendantId的接口:

import org.hibernate.context.spi.CurrentTenantIdentifierResolver;

import com.xyz.util.threadlocal.ThreadLocalUtil;

public class TenantIdResolver implements CurrentTenantIdentifierResolver {

    @Override
    public String resolveCurrentTenantIdentifier() {
        String tendantId = ThreadLocalUtil.tendantId.get();
        tendantId  = "dataSource1:db1";//TODO 刪除TEST
        return tendantId;
    }

    @Override
    public boolean validateExistingCurrentSessions() {
        return true;
    }

}

這裏也前面引文裏提到的不太一樣的是,我把tendantId的信息量加大了,不光能表示使用哪個數據庫,還能同時表示使用哪個數據源。(前面提到的,實現SCHEMA時還實現DATABASE)
這裏使用了一個ThreadLocal來存儲tendantId,具體的設置點在哪兒,你可以根據你的應用場景來考慮。我現在還沒考慮到這裏,等考慮好了再來補充。

import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;

import org.hibernate.HibernateException;
import org.hibernate.service.jdbc.connections.internal.C3P0ConnectionProvider;
import org.hibernate.service.jdbc.connections.spi.MultiTenantConnectionProvider;
import org.hibernate.service.spi.Configurable;
import org.hibernate.service.spi.ServiceRegistryAwareService;
import org.hibernate.service.spi.ServiceRegistryImplementor;
import org.hibernate.service.spi.Stoppable;

import com.xyz.util.threadlocal.ThreadLocalUtil;

/**
 * 分數據庫多租戶
 * @author huqiao([email protected])
 */
public class SchemaBasedMultiTenantConnectionProvider implements MultiTenantConnectionProvider, Stoppable,
Configurable, ServiceRegistryAwareService   {

    private static final long serialVersionUID = 1L;
    private final C3P0ConnectionProvider connectionProvider = new C3P0ConnectionProvider();
    private final C3P0ConnectionProvider connectionProvider2 = new C3P0ConnectionProvider();

    private final Map<String,C3P0ConnectionProvider> tenantIdConnMap = new HashMap<String,C3P0ConnectionProvider>();

    private C3P0ConnectionProvider getProvider(){
        String tenantIdentifier = ThreadLocalUtil.tendantId.get();
        tenantIdentifier = tenantIdentifier.split(":")[0];
        return tenantIdConnMap.get(tenantIdentifier);
    }

    @Override
    public Connection getAnyConnection() throws SQLException {
        return getProvider().getConnection();
    }

    @Override
    public void releaseAnyConnection(Connection connection) throws SQLException {
        getProvider().closeConnection(connection);
    }

    @Override
    public Connection getConnection(String tenantIdentifier) throws SQLException {
        //ThreadLocalUtil.tendantId.set(tenantIdentifier);
        tenantIdentifier = tenantIdentifier.split(":")[1];
        final Connection connection = getAnyConnection();
        try {
            connection.createStatement().execute("USE " + tenantIdentifier);
        } catch (SQLException e) {
            throw new HibernateException("Could not alter JDBC connection to specified schema [" + tenantIdentifier
                    + "]", e);
        }
        return connection;
    }

    @Override
    public void releaseConnection(String tenantIdentifier, Connection connection) throws SQLException {
        try {
            connection.createStatement().execute("USE test");
        } catch (SQLException e) {
            throw new HibernateException("Could not alter JDBC connection to specified schema [" + tenantIdentifier
                    + "]", e);
        }
        getProvider().closeConnection(connection);
    }

    @Override
    public boolean isUnwrappableAs(Class unwrapType) {
        return this.getProvider().isUnwrappableAs(unwrapType);
    }

    @Override
    public <T> T unwrap(Class<T> unwrapType) {
        return this.getProvider().unwrap(unwrapType);
    }

    @Override
    public void stop() {
        this.getProvider().stop();
    }

    @Override
    public boolean supportsAggressiveRelease() {
        return this.getProvider().supportsAggressiveRelease();
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    @Override
    public void configure(Map configurationValues) {

        //connectorProvider初始化
        this.connectionProvider.configure(configurationValues);

        configurationValues.put("hibernate.connection.url", "jdbc:mysql://{db-server-url}:3306/dbname?useUnicode=true&amp;characterEncoding=utf8");
        configurationValues.put("hibernate.connection.password", "password");
        this.connectionProvider2.configure(configurationValues);

        //connectorProvider與tenantId的關係映射
        tenantIdConnMap.put("dataSource1", connectionProvider);
        tenantIdConnMap.put("dataSource2", connectionProvider2);


    }

    @Override
    public void injectServices(ServiceRegistryImplementor serviceRegistry) {
        connectionProvider.injectServices(serviceRegistry);
        connectionProvider2.injectServices(serviceRegistry);
    }

}

和引文不同的是,這裏直接使用的是C3P0ConnectionProvider,而不是DriverManagerConnectionProviderImpl,DriverManagerConnectionProviderImpl自己實現了一個連接池,但我覺得使用C3P0應該纔是正道。

引文中只創建了一個connectionProvider,我在這裏創建了兩個,其實可以是N個。然後再在getProvider()方法中來確定使用哪個connectionProvider。OK這其實就是多個數據源的實現。

多個數據庫的實現在方法getConnection()裏,可以看到,在切換數據庫的時候就是簡單地使用了一個USE而已。

這個代碼現在存在的問題在configure()方法裏。我現在還沒考慮好如何根據業務需求來創建N個connectionProvier並且做好映射,這裏僅僅是爲了試驗,倉促地構建了另外一個connectionProvider,它和配置的connectionProvider的區別僅僅是url和password不一樣。

最後,injectServices方法裏,一定要爲每一個connectionProvider都injectServices一下。

接下來看看Hibernate配置:

<bean id="sessionFactory"
        class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
        <!-- 
        <property name="dataSource" ref="dataSource"/>
        -->
        <property name="hibernateProperties">
            <props>
                <!-- C3P0的配置 -->
                <prop key="c3p0.jdbcUrl" >${jdbc.url}</prop>
                <prop key="c3p0.user" >${jdbc.username}</prop>
                <prop key="c3p0.password"  >${jdbc.password}</prop>
                <prop key="c3p0.preferredTestQuery" >${preferredTestQuery}</prop>
                <prop key="c3p0.idleConnectionTestPeriod" >${idleConnectionTestPeriod}</prop>
                <prop key="c3p0.testConnectionOnCheckout" >${testConnectionOnCheckout}</prop>
                <!-- 數據源配置 -->
                <prop key="hibernate.connection.url">jdbc:mysql://localhost:3306/dbname?useUnicode=true&amp;characterEncoding=utf8</prop>
                <prop key="hibernate.connection.username">root</prop>
                <prop key="hibernate.connection.password">root</prop>
                <prop key="hibernate.connection.driver_class">com.mysql.jdbc.Driver</prop>
                <prop key="hibernate.connection.autocommit">false</prop>
                <!-- Hibernate配置 -->
                <prop key="hibernate.dialect">${hibernate.dialect}</prop>
                <prop key="hibernate.hbm2ddl.auto">${hibernate.hbm2ddl.auto}</prop>
                <prop key="hibernate.max_fetch_depth">${hibernate.maxFetchDepth}</prop>
                <prop key="hibernate.show_sql">${hibernate.show_sql}</prop>
                <prop key="hibernate.format_sql">${hibernate.format_sql}</prop>
                <prop key="hibernate.jdbc.batch_size">${hibernate.jdbc.batch_size}</prop>
                <prop key="hibernate.cache.use_query_cache">${cache.use_query_cache}</prop>
                <prop key="hibernate.cache.use_second_level_cache">${cache.use_second_level_cache}</prop>
                <prop key="hibernate.cache.region.factory_class">${hibernate.cache.region.factory_class}</prop>
                <prop key="hibernate.temp.use_jdbc_metadata_defaults">${hibernate.temp.use_jdbc_metadata_defaults}</prop>
                <!-- 多租戶配置  -->
                <prop key="hibernate.multiTenancy">SCHEMA</prop>
                <prop key="hibernate.tenant_identifier_resolver">com.xyz.TenantIdResolver</prop>
                <prop key="hibernate.multi_tenant_connection_provider">com.xyz.SchemaBasedMultiTenantConnectionProvider</prop>
            </props>
        </property>
        <property name="packagesToScan" value="com.xyz.*.entity"/>
    </bean>

因爲使用了C3P0,這裏增加了相應的配置。另外注意一下,我這裏原先配置的dataSource被廢掉了,應爲是我們自己創建C3P0的ConnectionProvider。如果看過引文了,那麼下面的多租戶配置就不多說了吧。

後話
其中還有很多不夠完善的地方,等想好了再修改補充吧。
接下來應該考慮如何按照業務需要來初始化不同的ConnectionProvider了。如果這一切都做好了,還應該考慮考慮多租戶環境下,如何實現用戶的登錄邏輯。

獲取源碼&查看在線Demo請移步《 使用Hibernate多租戶實現SaaS服務》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章