使用Spring,Hibernate和C3P0管理多租户Web应用程序中的连接池

Kho*_*row 27 postgresql spring hibernate c3p0 multi-tenant

我正在尝试设置一个多租户Web应用程序,同时具有(理想情况下)数据库分离和模式分离方法的可能性.虽然我将从Schema分离开始.我们目前正在使用:

  • Spring 4.0.0
  • Hibernate 4.2.8
  • Hibernate-c3p0 4.2.8(使用c3p0-0.9.2.1)
  • 和PostgreSQL 9.3(我怀疑它对整体架构真的很重要)

大部分时间我都遵循这个主题(因为解决方案@Transactional).但我在执行方面有点迷失MultiTenantContextConnectionProvider.在这里也提到了类似的问题,但有些方面我无法弄清楚:

1)连接池会发生什么?我的意思是,它是由Spring还是Hibernate管理的?我猜ConnectionProviderBuilder- 或者按照建议 - 任何实现,Hibernate都是管理它的人.
2)Spring不管理连接池是一种好方法吗?或者Spring是否可以管理它?
3)这是未来实现数据库和模式分离的正确途径吗?

任何评论或描述都完全赞赏.

应用程序的context.xml

<beans>
    ...
    <bean id="dataSource" class="org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy">
        <property name="targetDataSource" ref="c3p0DataSource" />
    </bean>

    <bean id="c3p0DataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
        <property name="driverClass" value="org.postgresql.Driver" />
        ... other C3P0 related config
    </bean>

    <bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
        <property name="packagesToScan" value="com.webapp.domain.model" />

        <property name="hibernateProperties">
            <props>
                <prop key="hibernate.dialect">org.hibernate.dialect.PostgreSQLDialect</prop>
                <prop key="hibernate.default_schema">public</prop>

                <prop key="hibernate.multiTenancy">SCHEMA</prop>
                <prop key="hibernate.tenant_identifier_resolver">com.webapp.persistence.utility.CurrentTenantContextIdentifierResolver</prop>
                <prop key="hibernate.multi_tenant_connection_provider">com.webapp.persistence.utility.MultiTenantContextConnectionProvider</prop>
            </props>
        </property>
    </bean>

    <bean id="transactionManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager">
        <property name="autodetectDataSource" value="false" />
        <property name="sessionFactory" ref="sessionFactory" />
    </bean>

   ...
</beans>
Run Code Online (Sandbox Code Playgroud)

CurrentTenantContextIdentifierResolver.java

public class CurrentTenantContextIdentifierResolver implements CurrentTenantIdentifierResolver {
    @Override
    public String resolveCurrentTenantIdentifier() {
        return CurrentTenantIdentifier;  // e.g.: public, tid130, tid456, ...
    }

    @Override
    public boolean validateExistingCurrentSessions() {
        return true;
    }
}
Run Code Online (Sandbox Code Playgroud)

MultiTenantContextConnectionProvider.java

public class MultiTenantContextConnectionProvider extends AbstractMultiTenantConnectionProvider {
    // Do I need this and its configuratrion?
    //private C3P0ConnectionProvider connectionProvider = null;

    @Override
    public ConnectionProvider getAnyConnectionProvider() {
        // the main question is here.
    }

    @Override
    public ConnectionProvider selectConnectionProvider(String tenantIdentifier) {
        // and of course here.
    }
}
Run Code Online (Sandbox Code Playgroud)



编辑

关于@ ben75 的答案:

这是一个新的实现MultiTenantContextConnectionProvider.它不再延伸AbstractMultiTenantConnectionProvider.它宁愿实现MultiTenantConnectionProvider,能够返回[Connection][4]而不是[ConnectionProvider][5]

public class MultiTenantContextConnectionProvider implements MultiTenantConnectionProvider, ServiceRegistryAwareService {
    private DataSource lazyDatasource;;

    @Override
    public void injectServices(ServiceRegistryImplementor serviceRegistry) {
        Map lSettings = serviceRegistry.getService(ConfigurationService.class).getSettings();

        lazyDatasource = (DataSource) lSettings.get( Environment.DATASOURCE );
    }

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

    @Override
    public Connection getConnection(String tenantIdentifier) throws SQLException {
        final Connection connection = getAnyConnection();

        try {
            connection.createStatement().execute("SET SCHEMA '" + tenantIdentifier + "'");
        }
        catch (SQLException e) {
            throw new HibernateException("Could not alter JDBC connection to specified schema [" + tenantIdentifier + "]", e);
        }

        return connection;
    }
}
Run Code Online (Sandbox Code Playgroud)

ben*_*n75 37

您可以选择3种不同的策略来影响连接轮询.无论如何,你必须提供一个实现MultiTenantConnectionProvider.您选择的策略当然会影响您的实施.

一般评论 MultiTenantConnectionProvider.getAnyConnection()

getAnyConnection()hibernate需要收集元数据并设置SessionFactory.通常在多租户架构中,您有一个未被任何租户使用的特殊/主数据库(或架构).它是一种模板数据库(或模式).如果此方法返回与此数据库(或架构)的连接,则可以.

策略1:每个租户都拥有自己的数据库.(以及它自己的连接池)

在这种情况下,每个租户都有自己的C3PO管理的连接池,您可以提供MultiTenantConnectionProvider基于的实现AbstractMultiTenantConnectionProvider

每个租户都拥有它C3P0ConnectionProvider,所以你所要做的selectConnectionProvider(tenantIdentifier)就是返回正确的租户.你可以保留一个Map来缓存它们,你可以用以下方法延迟初始化C3POConnectionProvider:

private ConnectionProvider lazyInit(String tenantIdentifier){
    C3P0ConnectionProvider connectionProvider = new C3P0ConnectionProvider();
    connectionProvider.configure(getC3POProperties(tenantIdentifier));
    return connectionProvider;
}

private Map getC3POProperties(String tenantIdentifier){
    // here you have to get the default hibernate and c3po config properties 
    // from a file or from Spring application context (there are good chances
    // that those default  properties point to the special/master database) 
    // and alter them so that the datasource point to the tenant database
    // i.e. : change the property hibernate.connection.url 
    // (and any other tenant specific property in your architecture like :
    //     hibernate.connection.username=tenantIdentifier
    //     hibernate.connection.password=...
    //     ...) 
}
Run Code Online (Sandbox Code Playgroud)

策略2:每个租户都拥有自己的架构,并且它在一个数据库中拥有自己的连接池

这种情况与第一个有关ConnectionProvider实现的策略非常相似,因为您也可以使用它AbstractMultiTenantConnectionProvider作为基类来实现您的MultiTenantConnectionProvider

该实现与策略1的建议实现非常相似,除了您必须在c3po配置中更改模式而不是数据库

策略3:每个租户在单个数据库中拥有自己的架构,但使用共享连接池

这种情况略有不同,因为每个租户都将使用相同的连接提供程序(因此将共享连接池).在这种情况下:连接提供程序必须在使用任何连接之前设置要使用的架构.即你必须实现MultiTenantConnectionProvider.getConnection(String tenantIdentifier)(即提供的默认实现AbstractMultiTenantConnectionProvider将不起作用).

使用postgresql,您可以使用:

 SET search_path to <schema_name_for_tenant>;
Run Code Online (Sandbox Code Playgroud)

或使用别名

 SET schema <schema_name_for_tenant>;
Run Code Online (Sandbox Code Playgroud)

所以这就是你的意思getConnection(tenant_identifier);:

@Override
public Connection getConnection(String tenantIdentifier) throws SQLException {
    final Connection connection = getAnyConnection();
    try {
        connection.createStatement().execute( "SET search_path TO " + tenanantIdentifier );
    }
    catch ( SQLException e ) {
        throw new HibernateException(
                "Could not alter JDBC connection to specified schema [" +
                        tenantIdentifier + "]",
                e
        );
    }
    return connection;
}
Run Code Online (Sandbox Code Playgroud)

有用的参考在这里 (官方文档)

其他有用的链接C3POConnectionProvider.java


您可以在实施中结合策略1和策略2.您只需要一种方法来为当前租户找到正确的连接属性/连接URL.


编辑

我认为策略2或3之间的选择取决于您的应用程序的流量和租户数量.使用单独的连接池:一个租户可用的连接数量将会低很多,因此:如果出于某种合法原因,一个租户突然需要很多连接,这个特定租户所看到的性能将大幅下降(而另一个租户将不会影响).

另一方面,对于策略3,如果出于某种合法的原因,一个租户突然需要很多联系:每个租户看到的表现都会降低.

一般来说,我认为策略2更灵活,更安全:每个租户不能消耗超过给定数量的连接(如果需要,可以为每个租户配置此数量)