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

时间:2014-01-19 23:09:15

标签: spring hibernate postgresql multi-tenant c3p0

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

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

我主要关注this thread(因为@Transactional的解决方案)。但是我在实施MultiTenantContextConnectionProvider时迷失了方向。还有this similar question在这里问过SO,但有些方面我无法弄清楚:

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>

CurrentTenantContextIdentifierResolver.java

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

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

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.
    }
}



修改

关于@ ben75的the answer

这是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;
    }
}

2 个答案:

答案 0 :(得分:33)

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

关于MultiTenantConnectionProvider.getAnyConnection()

的一般评论 hibernate需要

getAnyConnection()来收集元数据并设置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=...
    //     ...) 
}

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

此案例与关于ConnectionProvider实施的第一个策略非常相似,因为您也可以使用AbstractMultiTenantConnectionProvider作为基类来实现MultiTenantConnectionProvider

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

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

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

使用postgresql,您可以执行以下操作:

 SET search_path to <schema_name_for_tenant>;

或使用别名

 SET schema <schema_name_for_tenant>;

所以这是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;
}

有用的参考是here(官方文件)

其他有用的链接C3POConnectionProvider.java


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


修改

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

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

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

答案 1 :(得分:0)

恕我直言,连接池管理将由Sql Server本身默认处理,但是某些编程语言(如C#)确实提供了一些控制池的方法。请参阅here

为租户选择(1)架构或(2)单独的数据库取决于您可以为租户预期的数据量。但是,以下考虑值得研究

  1. 为试用客户和低级客户创建共享架构模型 批量客户,这可以通过数量来识别 您在租赁过程中向租户提供的功能 招待客户

  2. 创建或加载可能的企业级客户时 拥有大量的交易数据,非常适合单独使用 数据库中。

  3. 架构模型可能具有不同的SQL Server实现 和MySQL服务器不同的一个,你应该考虑。

  4. 在选择选项时,请考虑客户[租户]可能愿意在相当长的时间和系统使用后扩展的事实。如果您的应用中不支持适当的横向扩展选项,则必须加以打扰。

  5. 分享您对以上几点的评论,进一步讨论