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

2022-09-01 05:18:50

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

因此,我使用URL将请求从主数据库分离到辅助(只读),以避免对错误主数据库的只读调用,因为复制时间是最小的。

注意我将一个会话工厂与 Spring 提供的 RoutingDataSource 结合使用,该数据源根据键查找 db 以使用。我对多租户感兴趣,因为我使用的是支持它的休眠4.3.4。

我有两个问题:

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

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

我的团队中有个家伙:

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

请饶有兴趣。提前致谢。


答案 1

弹簧交易路由

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

public enum  DataSourceType {
    READ_WRITE,
    READ_ONLY
}

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

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

Read-write and read-only transaction routing with Spring

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

public class TransactionRoutingDataSource 
        extends AbstractRoutingDataSource {

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

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

该方法返回鉴别器值,该值将用于选择读写或只读 JDBC 。determineCurrentLookupKeyDataSource

弹簧读写和只读 JDBC 数据源配置

配置如下所示: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));
    }
}

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

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

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

Spring 组件定义读写 JDBC,而组件定义只读 JDBC。readWriteDataSourceDataSourcereadOnlyDataSourceDataSource

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

它们充当读写和只读数据源的外观,并使用该实用工具实现。actualDataSourceTransactionRoutingDataSource

使用密钥注册 ,并使用密钥注册。readWriteDataSourceDataSourceType.READ_WRITEreadOnlyDataSourceDataSourceType.READ_ONLY

因此,在执行读写方法时,将使用 ,而在执行方法时,将使用。@TransactionalreadWriteDataSource@Transactional(readOnly = true)readOnlyDataSource

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

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

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

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

测试时间

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

log_min_duration_statement = 0
log_line_prefix = '[%d] '

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

因此,在调用 和 方法时,如下所示: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_persistencehigh_performance_java_persistence_replica

所以,一切都像一个魅力!

所有的源代码都可以在我的高性能Java Persistence GitHub存储库中找到,所以你也可以尝试一下。

结论

您需要确保为连接池设置正确的大小,因为这可以产生巨大的差异。为此,我建议使用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配置的连接数即可。

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

此外,您希望保持锁定并尽快完成事务。如果批处理处理器使用的是并发处理工作线程,请确保关联的连接池大小等于工作线程数,这样它们就不会等待其他人释放连接。


答案 2

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

10% 读取

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

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

如果您要遵循CQRS设计,我宁愿使用单独的会话Factory,因为正在加载/缓存的对象很可能与正在编写的对象不同。

90% 写入

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

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

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

石英

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

数据库分区

如果每秒 1,000 个请求写入大量数据,请考虑对数据库进行分区。你可能会发现你一直在增长表。分区是在不存档数据的情况下解决此问题的一种方法。

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

存档显然是另一种选择

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


推荐