如何使用 JPA 和 Hibernate 拆分只读和读写事务

新手上路,请多包涵

我有一个非常繁重的 java webapp,每秒处理数千个请求,它使用一个主 Postgresql 数据库,它使用流式(异步)复制将自身复制到一个辅助(只读)数据库。

因此,考虑到复制时间最短,我使用 URL 将请求从主数据库分离到辅助数据库(只读),以避免对主数据库进行只读调用。

注意我将一个 sessionFactory 与 spring 提供的 RoutingDataSource 一起使用,它根据键查找要使用的数据库。我对多租户很感兴趣,因为我使用的是支持它的休眠 4.3.4。

我有两个问题:

  1. 我认为基于 URL 的拆分效率不高,因为我只能移动 10% 的流量,这意味着只读 URL 不多。我应该考虑什么方法?
  2. 可能是,在 URL 的基础上,我在两个节点之间实现了某种程度的分布,但是我将如何处理我的石英作业(甚至有单独的 JVM)?我应该采取什么务实的方法?

我知道我可能不会在这里得到一个完美的答案,因为这真的很广泛,但我只是想听听你对上下文的看法。

我团队中的伙计们:

  • 春天4
  • 休眠4
  • 石英2.2
  • Java7 / Tomcat7

请注意。提前致谢。

原文由 Sachin Verma 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 1.1k
2 个回答

弹簧事务路由

首先,我们将创建一个 DataSourceType Java Enum 来定义我们的事务路由选项:

 public enum  DataSourceType {
    READ_WRITE,
    READ_ONLY
}

要将读写事务路由到主节点并将只读事务路由到副本节点,我们可以定义一个连接到主节点的 ReadOnlyDataSource ReadWriteDataSource 一个连接到主节点的—副本节点。

读写和只读事务路由由 Spring AbstractRoutingDataSource 抽象完成,由 TransactionRoutingDatasource 实现,如下图所示:

使用 Spring 的读写和只读事务路由

TransactionRoutingDataSource 非常容易实现,如下所示:

 public class TransactionRoutingDataSource
        extends AbstractRoutingDataSource {

    @Nullable
    @Override
    protected Object determineCurrentLookupKey() {
        return TransactionSynchronizationManager
            .isCurrentTransactionReadOnly() ?
            DataSourceType.READ_ONLY :
            DataSourceType.READ_WRITE;
    }
}

基本上,我们检查存储当前事务上下文的 Spring TransactionSynchronizationManager 类,以检查当前运行的 Spring 事务是否是只读的。

determineCurrentLookupKey 方法返回将用于选择读写或只读 JDBC DataSource 的鉴别器值。

Spring读写和只读JDBC DataSource配置

DataSource 配置如下所示:

 @Configuration
@ComponentScan(
    basePackages = "com.vladmihalcea.book.hpjp.util.spring.routing"
)
@PropertySource(
    "/META-INF/jdbc-postgresql-replication.properties"
)
public class TransactionRoutingConfiguration
        extends AbstractJPAConfiguration {

    @Value("${jdbc.url.primary}")
    private String primaryUrl;

    @Value("${jdbc.url.replica}")
    private String replicaUrl;

    @Value("${jdbc.username}")
    private String username;

    @Value("${jdbc.password}")
    private String password;

    @Bean
    public DataSource readWriteDataSource() {
        PGSimpleDataSource dataSource = new PGSimpleDataSource();
        dataSource.setURL(primaryUrl);
        dataSource.setUser(username);
        dataSource.setPassword(password);
        return connectionPoolDataSource(dataSource);
    }

    @Bean
    public DataSource readOnlyDataSource() {
        PGSimpleDataSource dataSource = new PGSimpleDataSource();
        dataSource.setURL(replicaUrl);
        dataSource.setUser(username);
        dataSource.setPassword(password);
        return connectionPoolDataSource(dataSource);
    }

    @Bean
    public TransactionRoutingDataSource actualDataSource() {
        TransactionRoutingDataSource routingDataSource =
            new TransactionRoutingDataSource();

        Map<Object, Object> dataSourceMap = new HashMap<>();
        dataSourceMap.put(
            DataSourceType.READ_WRITE,
            readWriteDataSource()
        );
        dataSourceMap.put(
            DataSourceType.READ_ONLY,
            readOnlyDataSource()
        );

        routingDataSource.setTargetDataSources(dataSourceMap);
        return routingDataSource;
    }

    @Override
    protected Properties additionalProperties() {
        Properties properties = super.additionalProperties();
        properties.setProperty(
            "hibernate.connection.provider_disables_autocommit",
            Boolean.TRUE.toString()
        );
        return properties;
    }

    @Override
    protected String[] packagesToScan() {
        return new String[]{
            "com.vladmihalcea.book.hpjp.hibernate.transaction.forum"
        };
    }

    @Override
    protected String databaseType() {
        return Database.POSTGRESQL.name().toLowerCase();
    }

    protected HikariConfig hikariConfig(
            DataSource dataSource) {
        HikariConfig hikariConfig = new HikariConfig();
        int cpuCores = Runtime.getRuntime().availableProcessors();
        hikariConfig.setMaximumPoolSize(cpuCores * 4);
        hikariConfig.setDataSource(dataSource);

        hikariConfig.setAutoCommit(false);
        return hikariConfig;
    }

    protected HikariDataSource connectionPoolDataSource(
            DataSource dataSource) {
        return new HikariDataSource(hikariConfig(dataSource));
    }
}

/META-INF/jdbc-postgresql-replication.properties 资源文件提供了读写和只读JDBC DataSource 组件的配置:

 hibernate.dialect=org.hibernate.dialect.PostgreSQL10Dialect

jdbc.url.primary=jdbc:postgresql://localhost:5432/high_performance_java_persistence
jdbc.url.replica=jdbc:postgresql://localhost:5432/high_performance_java_persistence_replica

jdbc.username=postgres
jdbc.password=admin

jdbc.url.primary 属性定义主节点的 URL,而 jdbc.url.replica 定义副本节点的 URL。

The readWriteDataSource Spring component defines the read-write JDBC DataSource while the readOnlyDataSource component define the read-only JDBC DataSource .

请注意,读写和只读数据源都使用 HikariCP 进行连接池。

actualDataSource 充当读写和只读数据源的外观,并使用 TransactionRoutingDataSource 实用程序实现。

The readWriteDataSource is registered using the DataSourceType.READ_WRITE key and the readOnlyDataSource using the DataSourceType.READ_ONLY key.

So, when executing a read-write @Transactional method, the readWriteDataSource will be used while when executing a @Transactional(readOnly = true) method, the readOnlyDataSource will被使用。

请注意, additionalProperties 方法定义了 hibernate.connection.provider_disables_autocommit Hibernate 属性,我将其添加到 Hibernate 以推迟 RESOURCE_LOCAL JPA 事务的数据库获取。

不仅 hibernate.connection.provider_disables_autocommit 允许您更好地利用数据库连接,而且这是我们使该示例工作的唯一方法,因为如果没有此配置,连接是在调用 determineCurrentLookupKey 之前获取的 --- 方法 TransactionRoutingDataSource

构建 JPA 所需的其余 Spring 组件 EntityManagerFactoryAbstractJPAConfiguration 基类定义。

基本上, actualDataSource 由 DataSource-Proxy 进一步包装并提供给 JPA EntityManagerFactory 。您可以查看 GitHub 上的源代码以 获取更多详细信息。

测试时间

要检查事务路由是否有效,我们将通过在 postgresql.conf 配置文件中设置以下属性来启用 PostgreSQL 查询日志:

 log_min_duration_statement = 0
log_line_prefix = '[%d] '

log_min_duration_statement 属性设置用于记录所有 PostgreSQL 语句,而第二个将数据库名称添加到 SQL 日志。

因此,当调用 newPostfindAllPostsByTitle 方法时,如下所示:

 Post post = forumService.newPost(
    "High-Performance Java Persistence",
    "JDBC", "JPA", "Hibernate"
);

List<Post> posts = forumService.findAllPostsByTitle(
    "High-Performance Java Persistence"
);

我们可以看到 PostgreSQL 记录了以下消息:

 [high_performance_java_persistence] LOG:  execute <unnamed>:
    BEGIN

[high_performance_java_persistence] DETAIL:
    parameters: $1 = 'JDBC', $2 = 'JPA', $3 = 'Hibernate'
[high_performance_java_persistence] LOG:  execute <unnamed>:
    select tag0_.id as id1_4_, tag0_.name as name2_4_
    from tag tag0_ where tag0_.name in ($1 , $2 , $3)

[high_performance_java_persistence] LOG:  execute <unnamed>:
    select nextval ('hibernate_sequence')

[high_performance_java_persistence] DETAIL:
    parameters: $1 = 'High-Performance Java Persistence', $2 = '4'
[high_performance_java_persistence] LOG:  execute <unnamed>:
    insert into post (title, id) values ($1, $2)

[high_performance_java_persistence] DETAIL:
    parameters: $1 = '4', $2 = '1'
[high_performance_java_persistence] LOG:  execute <unnamed>:
    insert into post_tag (post_id, tag_id) values ($1, $2)

[high_performance_java_persistence] DETAIL:
    parameters: $1 = '4', $2 = '2'
[high_performance_java_persistence] LOG:  execute <unnamed>:
    insert into post_tag (post_id, tag_id) values ($1, $2)

[high_performance_java_persistence] DETAIL:
    parameters: $1 = '4', $2 = '3'
[high_performance_java_persistence] LOG:  execute <unnamed>:
    insert into post_tag (post_id, tag_id) values ($1, $2)

[high_performance_java_persistence] LOG:  execute S_3:
    COMMIT

[high_performance_java_persistence_replica] LOG:  execute <unnamed>:
    BEGIN

[high_performance_java_persistence_replica] DETAIL:
    parameters: $1 = 'High-Performance Java Persistence'
[high_performance_java_persistence_replica] LOG:  execute <unnamed>:
    select post0_.id as id1_0_, post0_.title as title2_0_
    from post post0_ where post0_.title=$1

[high_performance_java_persistence_replica] LOG:  execute S_1:
    COMMIT

使用 high_performance_java_persistence 前缀的日志语句在主节点上执行,而使用 high_performance_java_persistence_replica 的日志语句在副本节点上执行。

GitHub 资料库

这不仅仅是理论。这一切都在 GitHub 上,并且非常有效。使用 此测试用例 作为参考。

因此,您可以将它用作事务路由解决方案的起点,因为您有一个功能齐全的示例。

二级缓存

一旦你使用了复制,你就在分布式环境中运行,所以你需要使用分布式缓存解决方案,比如 Infinispan

由于我们使用复制将流量分配到更多数据库节点,很明显我们还有多个应用程序节点必须连接到这些数据库节点。

因此,在这样的环境中使用 READ_WRITE CacheConcurrencyStrategy 是一个可怕的反模式,因为每个分布式节点都会保留自己的缓存条目副本,导致一致性问题,即使你没有使用事务路由。

更不用说如果您对应用程序节点采用自动缩放,您将面临的冷缓存问题,因为它们会放大数据库流量,因为新节点将从冷缓存开始。

所以,如果你打算使用带有二级缓存机制的事务路由,那么你可以做得比这更好。

NONSTRICT_READ_WRITE 缓存并发策略与二级缓存提供程序一起使用,该二级缓存提供程序可以将缓存数据存储在分布式节点系统中,即使您创建新的应用程序节点时这些节点也随时可用。

结论

您需要确保为连接池设置正确的大小,因为这会产生巨大的差异。为此,我建议使用 Flexy Pool

您需要非常勤奋并确保相应地标记所有只读事务。只有 10% 的交易是只读的,这很不寻常。可能是您有这样一个写入最多的应用程序,或者您正在使用只发出查询语句的写入事务?

对于批处理,您肯定需要读写事务,因此请确保启用 JDBC 批处理,如下所示:

 <property name="hibernate.order_updates" value="true"/>
<property name="hibernate.order_inserts" value="true"/>
<property name="hibernate.jdbc.batch_size" value="25"/>

对于批处理,您还可以使用单独的 DataSource ,它使用连接到主节点的不同连接池。

只要确保所有连接池的总连接大小小于 PostgreSQL 配置的连接数即可。

每个批处理作业都必须使用专用事务,因此请确保使用合理的批处理大小。

此外,您希望持有锁并尽快完成交易。如果批处理器正在使用并发处理 workers,请确保关联的连接池大小等于 workers 的数量,这样他们就不会等待其他人释放连接。

原文由 Vlad Mihalcea 发布,翻译遵循 CC BY-SA 4.0 许可协议

您是说您的应用程序 URL 只有 10% 是只读的,所以其他 90% 至少有某种形式的数据库写入。

10% 阅读

您可以考虑使用可能会提高数据库读取性能的 CQRS 设计。它当然可以从辅助数据库读取,并且可以通过专门为读取/查看层设计查询和域模型来提高效率。

您还没有说 10% 的请求是否昂贵(例如运行报告)

如果您要遵循 CQRS 设计,我更愿意使用单独的 sessionFactory,因为加载/缓存的对象很可能与编写的对象不同。

90% 写

就其他 90% 而言,您不希望在某些写入逻辑期间从辅助数据库读取(同时写入主数据库),因为您不希望涉及潜在的陈旧数据。

其中一些读取可能正在查找“静态”数据。如果 Hibernate 的缓存没有减少读取的数据库命中率,我会考虑为此类数据使用内存缓存,如 Memcached 或 Redis。 10% 读取和 90% 写入进程都可以使用相同的缓存。

对于非静态的读取(即读取您最近写入的数据),Hibernate 应该在其对象缓存中保存数据(如果其大小合适)。你能确定你的缓存命中/未命中性能吗?

石英

如果您确定计划的作业不会影响与另一个作业相同的数据集,则可以针对不同的数据库运行它们,但是如果有疑问,请始终对一个(主)服务器执行批量更新并复制更改。最好在逻辑上正确,而不是引入复制问题。

数据库分区

如果每秒 1,000 个请求写入大量数据,请考虑对数据库进行 分区。您可能会发现您的桌子不断增加。分区是在不归档数据的情况下解决这个问题的一种方法。

有时您需要对您的应用程序代码进行很少或根本不需要更改。

存档显然是另一种选择

免责声明:像这样的任何问题总是特定于应用程序的。始终尝试使您的架构尽可能简单。

原文由 Brad 发布,翻译遵循 CC BY-SA 3.0 许可协议

推荐问题