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

33

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

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

主要是按照此线程(因为解决了@Transactional的问题)进行操作。但是,我在实现MultiTenantContextConnectionProvider方面有点迷失。还有在SO上提出的类似问题,但是有一些方面我无法理解:

1)连接池会发生什么?我的意思是,它是由Spring还是Hibernate管理的?我猜测通过ConnectionProviderBuilder或建议的任何实现,Hibernate是管理它的。
2)Spring不管理连接池是合适的方法吗?或者Spring甚至可以管理它吗?
3)这是未来同时实现基于数据库和模式分离的正确路径吗?

任何意见或说明都非常感激。

application-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的答案

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

1
了解您为什么想要这样做和/或您的需求/关注点(例如合规性、性能、卖点)将非常有用。如今,许多人只需通过启动单独的映像来解决多租户问题,从而实现真正的隔离,这通常是合规性要求所必需的,因此您不需要在DataSource级别上进行多租户设置。另一种选择是拥有一个巨大的模式,该模式具有多租户感知功能,并根据性能原因对应用程序进行分区/分片。 - Adam Gent
@AdamGent 我不使用为每个客户单独创建实例的主要原因是我们的目标客户数在10K-50K+之间。据我所知,拥有如此多的“单独实例”成本远高于通过负载平衡、多租户集群的单实例应用程序进行分割。对于您评论的第二部分,为了灵活性,我们将提供数据层的替代方法。如果客户想要单独的数据库,并且他们愿意为此支付额外的费用/月,那就这样吧。 - Khosrow
2个回答

47

您可以选择3种不同的策略来影响连接轮询。无论哪种情况,您都必须提供一个实现MultiTenantConnectionProvider。您选择的策略当然会影响您的实现。

关于MultiTenantConnectionProvider.getAnyConnection()的一般说明

getAnyConnection()是Hibernate需要收集元数据和设置SessionFactory所需的方法。通常,在多租户架构中,你有一个专门的/主数据库(或模式),没有任何租户使用。它是一种模板数据库(或模式)。如果此方法返回到此数据库(或模式)的连接,则可以使用它。

策略1:每个租户拥有自己的数据库。 (因此,它拥有自己的连接池)

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

每个租户都有自己的C3P0ConnectionProvider,因此您在selectConnectionProvider(tenantIdentifier)中只需返回正确的连接即可。您可以保留一个Map来缓存它们,并使用类似以下内容的lazy-initialize 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:每个租户在单个数据库中拥有自己的架构和连接池

这种情况与第一种策略非常相似,因为您也可以使用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;
}

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

其他有用的链接 C3POConnectionProvider.java


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


编辑

我认为策略2或3之间的选择取决于应用程序上的流量和租户数量。使用单独的连接池:一个租户可用的连接数将大大降低,因此:如果由于某些合法原因一个租户突然需要多个连接,则该特定租户所看到的性能将急剧下降(而其他租户则不受影响)。

另一方面,对于策略3,如果由于某些合法原因一个租户突然需要多个连接:每个租户所看到的性能都会降低。

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


谢谢@ben75。关于策略2和3,有什么优缺点吗? - Khosrow
目前我已经实现了以下步骤:1)通过XML配置定义Spring管理的“LazyConnectionDataSourceProxy”数据源(c3p0)。2)制作MultiTenantConnectionProvider的具体实现,同时还要实现ServiceRegistryAwareService。3)在MultiTenantConnectionProvider中使用数据源,即datasource.getConnection()。这个操作可行吗? - Khosrow
1
关于第1条和第2条:看起来正确。关于第3条:没有看到代码很难说什么。关于您的第一条评论:请查看我的编辑。 - ben75
+1 鼓励您清晰地解释您提出的不同策略。我已经使用新的MultiTenantConnectionProvider实现编辑了问题。 - Khosrow
如果我想根据租户数量的增加来判断我的应用程序的性能,是否有任何方法或逻辑? - Arpit

0

在我看来,连接池管理将由Sql Server自身默认处理,然而一些编程语言如C#提供了一些控制连接池的方法。请参考这里

对于租户来说,选择(1)模式或(2)单独的数据库取决于您预计的租户数据量。然而,以下考虑因素值得一看:

  1. 为试用客户和低容量客户创建共享模式模型,可以通过在客户入门过程中提供给租户的功能数量来确定。

  2. 当您创建或入门大型交易数据的企业级客户时,最好选择单独的数据库。

  3. 模式模型可能对SQL Server和MySQL Server有不同的实现方式,这是您需要考虑的。

  4. 此外,在选择选项时,请考虑到客户[租户]可能愿意在相当长的时间和系统使用后进行扩展。如果您的应用程序不支持适当的扩展选项,那么您将不得不受到困扰。

请分享您对上述观点的评论,以进一步讨论。


谢谢您的回复。但实际上,我已经了解了单独模式和/或数据库的优缺点。其次,我正在寻找使用Java(具体而言是Spring和Hibernate)实现的方法,而不是C#。 - Khosrow

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接