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

kcugc4gi  于 2022-11-14  发布在  其他
关注(0)|答案(5)|浏览(248)

我有一个相当重的java web应用程序,它每秒处理数千个请求,它使用一个主Postgresql数据库,该数据库使用流(异步)复制将自身复制到一个辅助(只读)数据库。
因此,考虑到复制时间最短,我使用URL将请求从主数据库分离到辅助数据库(只读),以避免对bug主数据库的只读调用。

注意:* 我使用了一个带有Spring提供的RoutingDataSource的sessionFactory,它根据键查找要使用的数据库。我对多租户感兴趣,因为我使用的是支持多租户的Hibernate 4.3.4。*

我有两个问题:
1.我不认为基于URL的拆分是有效的,因为我只能移动10%的流量,这意味着没有太多的只读URL。我应该考虑什么方法?
1.也许,不知何故,基于URL,我在两个节点之间实现了某种程度的分布,但我该如何处理我的quartz作业(甚至有单独的JVM)?我应该采取什么务实的方法?
我知道我可能不会得到一个完美的答案在这里,因为这真的是广泛的,但我只是想要你的意见的背景。
我的团队成员:

  • Spring4
  • 休眠状态4
  • 石英2.2
  • Java7 /Tomcat 7语言

请关注。提前感谢。

6psbrbz9

6psbrbz91#

Spring事务路由

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

public enum  DataSourceType {
    READ_WRITE,
    READ_ONLY
}

要将读写事务路由到主节点并将只读事务路由到副本节点,我们可以定义一个连接到主节点的ReadWriteDataSource和一个连接到副本节点的ReadOnlyDataSource
读写和只读事务路由由Spring AbstractRoutingDataSource抽象完成,该抽象由TransactionRoutingDatasource实现,如下图所示:

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

public class TransactionRoutingDataSource 
        extends AbstractRoutingDataSource {

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

基本上,我们检查存储当前事务上下文的Springx 1 m6n1x类,以检查当前运行的Spring事务是否为只读。
determineCurrentLookupKey方法返回鉴别器值,该值将用于选择读写或只读JDBC DataSource

Spring读写和只读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));
    }
}

/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。
readWriteDataSource Spring组件定义读写JDBC DataSource,而readOnlyDataSource组件定义只读JDBC DataSource
请注意,读写和只读数据源都使用HikariCP进行连接池。
actualDataSource充当读写和只读数据源的外观,并使用TransactionRoutingDataSource实用程序实现。
使用DataSourceType.READ_WRITE密钥注册readWriteDataSource,使用DataSourceType.READ_ONLY密钥注册readOnlyDataSource
因此,当执行读写@Transactional方法时,将使用readWriteDataSource,而当执行@Transactional(readOnly = true)方法时,将使用readOnlyDataSource
请注意,additionalProperties方法定义了hibernate.connection.provider_disables_autocommit Hibernate属性,我将该属性添加到Hibernate中以推迟RESOURCE_LOCAL JPA事务的数据库获取。
hibernate.connection.provider_disables_autocommit不仅允许您更好地使用数据库连接,而且这是我们使这个示例工作的唯一方法,因为如果没有这个配置,连接将在调用determineCurrentLookupKey方法TransactionRoutingDataSource之前获得。
构建JPA EntityManagerFactory所需的其余Spring组件由AbstractJPAConfiguration基类定义。
基本上,actualDataSource被DataSource-Proxy进一步 Package 并提供给JPA EntityManagerFactory

测试时间

为了检查事务路由是否正常工作,我们将通过在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上,工作起来就像一个魔咒。使用this test case作为参考。
因此,您可以将其作为事务路由解决方案的起点,因为您有一个功能齐全的示例。

二级缓存

一旦使用了复制,您就在分布式环境中运行,因此需要使用分布式缓存解决方案,如Infinispan
由于我们使用复制将流量分发到更多的数据库节点,因此很明显,我们还有多个应用程序节点必须连接到这些数据库节点。
因此,在这样的环境中使用READ_WRITECacheConcurrencyStrategy是一个可怕的反模式,因为每个分布式节点都将保留其自己的缓存条目副本,即使您没有使用事务路由,也会导致一致性问题。
更不用说为应用程序节点采用自动扩展时会面临的冷缓存问题,因为新节点将从冷缓存开始,这会放大数据库流量。
因此,如果您计划将事务路由与二级缓存机制一起使用,那么您可以做得更好。
NONSTRICT_READ_WRITE缓存并发策略与二级缓存提供程序一起使用,二级缓存提供程序可以将缓存数据存储在分布式节点系统中,即使在创建新的应用程序节点时,这些节点也随时可用。
结论
您需要确保为连接池设置了正确的大小,因为这会产生巨大的差异。
您需要非常谨慎,并确保相应地标记所有只读事务。只有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,该DataSource使用连接到主节点的不同连接池。
只需确保所有连接池的总连接大小小于PostgreSQL已配置的连接数。
每个批处理作业都必须使用专用事务,因此请确保使用合理的批处理大小。
如果批处理器使用并发处理工作线程,请确保关联的连接池大小等于工作线程的数量,这样它们就不会等待其他线程释放连接。

jv2fixgn

jv2fixgn2#

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

10%读取

您可以考虑使用CQRS design来提高数据库的读取性能,它当然可以从辅助数据库读取数据,而且通过专门为读取/视图层设计查询和域模型,它可能会变得更高效。
您还没有说明10%的请求是否昂贵(例如运行报告)
如果您要遵循CQRS设计,我更愿意使用单独的sessionFactory,因为加载/缓存的对象很可能与写入的对象不同。

90%写入

至于其他90%,您不希望在某些写逻辑期间从辅助数据库读取(同时写入主数据库),因为您不希望涉及潜在的陈旧数据。
其中一些读操作可能是在查找“静态”数据。如果Hibernate的缓存不能减少数据库读操作的命中率,我会考虑使用Memcached或Redis这样的内存缓存来处理这类数据。10%的读操作和90%的写操作都可以使用同一个缓存。
对于非静态的读取(即阅读您最近写入的数据),如果Hibernate的对象缓存大小合适,则它应该将数据保存在对象缓存中。您可以确定缓存命中/未命中性能吗?

石英色

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

数据库分区

如果每秒1,000个请求正在写入大量数据,请查看partitioning数据库。您可能会发现您曾经有过不断增长的表。分区是一种无需存档数据即可解决此问题的方法。
有时候,您只需要对应用程序代码进行很少的更改,甚至不需要更改。
归档显然是另一种选择

  • 免责声明:任何类似的问题都是特定于应用程序的。总是尽量保持架构尽可能简单。*
mnemlml8

mnemlml83#

由于复制是异步的,因此可接受的解决方案将导致难以调试和难以再现二级缓存的错误。
This automated test表明这可能导致操作不完整的实体图。
最清晰的路径是每个数据源都有一个EntityManagerFactory。

ctehm74n

ctehm74n4#

如果我没理解错的话,90%的HTTP请求都涉及到至少一个写操作,并且必须在主数据库上操作。您可以将只读事务定向到副本数据库,但改进只会影响10%的全局数据库操作,甚至那些只读操作也会命中数据库。
这里的通用体系结构是使用一个好的数据库缓存如果你能提供一个足够大的高速缓存,你可以希望数据库的大部分读取只命中该高速缓存,并成为内存操作,无论是只读事务的一部分或没有。高速缓存调优是一个微妙的操作,但是IMHO对于获得高性能增益是必要的。这些缓存甚至允许分布式前端,即使在这种情况下配置有点困难(如果你想使用Ehcache,你可能必须寻找Terracotta集群)。
目前,数据库复制主要用于保护数据,并且仅在信息系统的大部分仅读取数据的情况下才用作并发改进机制-这不是您所描述的。

qmelpv7a

qmelpv7a5#

您也可以在DB节点前运行proxySQL(可以是galera集群设置),并设置查询读写拆分规则,代理将根据定义的规则分配流量。例如:SELECT查询路由到读节点,而UPDATE查询或读写事务路由到写节点。

相关问题