Java 多租户应用程序中的连接池。共享池与每个租户的池

Java 多租户应用程序中的连接池。共享池与每个租户的池,java,spring,amazon-web-services,amazon-rds,hikaricp,Java,Spring,Amazon Web Services,Amazon Rds,Hikaricp,我正在用Spring2.x、Hibernate5.x、SpringDataREST和MySQL5.7构建一个多租户REST服务器应用程序。 Spring2.x使用Hikari进行连接池 我将使用每个租户的数据库方法,这样每个租户都有自己的数据库 我以以下方式创建了MultiTenantConnectionProvider: @Component @Profile("prod") public class MultiTenantConnectionProviderImpl implements M

我正在用Spring2.x、Hibernate5.x、SpringDataREST和MySQL5.7构建一个多租户REST服务器应用程序。 Spring2.x使用Hikari进行连接池

我将使用每个租户的数据库方法,这样每个租户都有自己的数据库

我以以下方式创建了MultiTenantConnectionProvider:

@Component
@Profile("prod")
public class MultiTenantConnectionProviderImpl implements MultiTenantConnectionProvider {
    private static final long serialVersionUID = 3193007611085791247L;
    private Logger log = LogManager.getLogger();

    private Map<String, HikariDataSource> dataSourceMap = new ConcurrentHashMap<String, HikariDataSource>();

    @Autowired
    private TenantRestClient tenantRestClient;

    @Autowired
    private PasswordEncrypt passwordEncrypt;

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

    @Override
    public Connection getAnyConnection() throws SQLException {
        Connection connection = getDataSource(TenantIdResolver.TENANT_DEFAULT).getConnection();
        return connection;

    }

    @Override
    public Connection getConnection(String tenantId) throws SQLException {
        Connection connection = getDataSource(tenantId).getConnection();
        return connection;
    }

    @Override
    public void releaseConnection(String tenantId, Connection connection) throws SQLException {
        log.info("releaseConnection " + tenantId);
        connection.close();
    }

    @Override
    public boolean supportsAggressiveRelease() {
        return false;
    }

    @Override
    public boolean isUnwrappableAs(Class unwrapType) {
        return false;
    }

    @Override
    public <T> T unwrap(Class<T> unwrapType) {
        return null;
    }

    public HikariDataSource getDataSource(@NotNull String tentantId) throws SQLException {
        if (dataSourceMap.containsKey(tentantId)) {
            return dataSourceMap.get(tentantId);
        } else {
            HikariDataSource dataSource = createDataSource(tentantId);
            dataSourceMap.put(tentantId, dataSource);
            return dataSource;
        }
    }

    public HikariDataSource createDataSource(String tenantId) throws SQLException {
        log.info("Create Datasource for tenant {}", tenantId);
        try {
            Database database = tenantRestClient.getDatabase(tenantId);
            DatabaseInstance databaseInstance = tenantRestClient.getDatabaseInstance(tenantId);
            if (database != null && databaseInstance != null) {
                HikariConfig hikari = new HikariConfig();
                String driver = "";
                String options = "";
                switch (databaseInstance.getType()) {
                case MYSQL:
                    driver = "jdbc:mysql://";
                    options = "?useLegacyDatetimeCode=false&serverTimezone=UTC&useUnicode=yes&characterEncoding=UTF-8&useSSL=false";
                    break;

                default:
                    driver = "jdbc:mysql://";
                    options = "?useLegacyDatetimeCode=false&serverTimezone=UTC&useUnicode=yes&characterEncoding=UTF-8&useSSL=false";
                }

                hikari.setJdbcUrl(driver + databaseInstance.getHost() + ":" + databaseInstance.getPort() + "/" + database.getName() + options);
                hikari.setUsername(database.getUsername());
                hikari.setPassword(passwordEncrypt.decryptPassword(database.getPassword()));

                // MySQL optimizations, see
                // https://github.com/brettwooldridge/HikariCP/wiki/MySQL-Configuration
                hikari.addDataSourceProperty("cachePrepStmts", true);
                hikari.addDataSourceProperty("prepStmtCacheSize", "250");
                hikari.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
                hikari.addDataSourceProperty("useServerPrepStmts", "true");
                hikari.addDataSourceProperty("useLocalSessionState", "true");
                hikari.addDataSourceProperty("useLocalTransactionState", "true");
                hikari.addDataSourceProperty("rewriteBatchedStatements", "true");
                hikari.addDataSourceProperty("cacheResultSetMetadata", "true");
                hikari.addDataSourceProperty("cacheServerConfiguration", "true");
                hikari.addDataSourceProperty("elideSetAutoCommits", "true");
                hikari.addDataSourceProperty("maintainTimeStats", "false");
                hikari.setMinimumIdle(3);
                hikari.setMaximumPoolSize(5);

                hikari.setIdleTimeout(30000);
                hikari.setPoolName("JPAHikari_" + tenantId);
                // mysql wait_timeout 600seconds
                hikari.setMaxLifetime(580000);
                hikari.setLeakDetectionThreshold(60 * 1000);

                HikariDataSource dataSource = new HikariDataSource(hikari);


                return dataSource;

            } else {
                throw new SQLException(String.format("DB not found for tenant %s!", tenantId));
            }
        } catch (Exception e) {
            throw new SQLException(e.getMessage());
        }
    }

}
我应该在池中有2个Core*2+1=5个连接

从我得到的信息来看,这应该是池中的最大连接数,以最大限度地提高该DB实例的性能

第一个解决方案

所以我的第一个问题很简单:我如何为每个租户创建一个单独的连接池?我总共应该使用5个连接

在我看来这是不可能的。即使我为每个租户分配2个连接,我也会有200个到DBMS的连接

根据,在一个
db.r4.large
实例上,我最多可以有1300个连接,因此该实例应该能够很好地承受负载。 但根据我前面提到的文章,使用数百个db连接似乎是一种不好的做法:

如果您有10000个前端用户,那么拥有10000个连接池就太疯狂了。仍然很可怕。即使是100个连接,也太过分了。您最多需要一个包含几十个连接的小池,并且希望池中的其余应用程序线程在等待连接时被阻塞

第二种解决方案

我想到的第二个解决方案是为同一DMB上的租户共享一个连接池。这意味着所有100个租户将使用相同的Hikari池(5个连接)(老实说,对我来说,这似乎很低)

这是最大限度地提高性能和缩短应用程序响应时间的正确方法吗


您对如何使用Spring、Hibernate、Mysql(托管在AWS RDS Aurora上)管理此场景有更好的了解吗?

最明显的是,为每个租户打开连接是一个非常糟糕的主意。您所需要的只是一个跨所有用户共享的连接池

  • 因此,第一步是根据一些预测找到负荷或预测负荷

  • 确定可接受的延迟时间、突发峰值时间流量等

  • 最后来看看您需要的连接数,并决定所需的实例数。例如,如果您的峰值时间使用率为10k/s,并且每个查询需要10ms,那么您将需要100个打开的连接,等待1s

  • 实现它时不需要对用户进行任何绑定。i、 e.所有人共享同一个池。除非你有一个案例来分组,比如说高级/基本用户,比如说有一套两个池等等

  • 最后,当您在AWS中执行此操作时,如果您需要基于第3点的多个实例,请查看是否可以根据负载自动缩放以节省成本

  • 查看这些比较指标

    就需求激增而言,这一次可能是最有趣的

    还有一些

    遵循为多租户环境选择的策略将(令人惊讶地)为每个租户使用连接池

    策略2:每个租户在单个数据库中都有自己的模式和连接池

    策略2更灵活、更安全:每个租户不能使用超过给定数量的连接(如果需要,可以为每个租户配置此数量)

    我建议将HikariCP的公式放在一边,使用更少的租户数量作为10(动态大小?),连接池大小为2

    请注意,
    HikariCP pool size
    中的10条连接池大小注释可能就足够了:

    10是个不错的整数。看起来很低?试一试,我们敢打赌,在这样的设置下,您可以轻松处理3000名前端用户,以6000 TPS的速度运行简单查询

    另请参见,这表明100个实例太多

    ,但这将是一个巨大的负载,需要100秒


    @EssexBoy

    感谢您的回复。对于我的应用程序是如何制作的,我必须使用策略1。在这种情况下,您的答复仍然有效吗?@drenda它有不同的数据库,但连接池仍然不同,因此,如果有100个租户,我将至少有200个连接,最多有1000个连接到数据库。考虑到r4.1大型实例最多可以处理1300个连接,每个租户的成本似乎相当高。如果我有1000个租户,我应该如何管理这些负载?10 DB实例?你有一些有用的链接到一些真实多租户应用程序的案例研究吗?Thanks@drenda我将这个问题添加到Hikari group@drenda brettwooldridge,直接回答:谢谢你的回复。您能解释一下为什么对租户使用池连接是个坏主意吗?谢谢,因为您的连接不会被充分利用。如果您只是想一想,在使用应用程序的过程中,您访问数据库的次数是多少,那么它只会是很小的一部分。打开不必要的连接也是一个额外的负担。因此,您可能会发现,即使使用一些连接,您也可以非常顺利地运行,因为您将分发它的使用情况。例如,大多数查询只需要几毫秒到10毫秒的剩余时间,这些连接将处于空闲状态。
    connections = ((core_count * 2) + effective_spindle_count)