【问题标题】:How to split read-only and read-write transactions with JPA and Hibernate如何使用 JPA 和 Hibernate 拆分只读和读写事务
【发布时间】:2014-11-12 17:12:19
【问题描述】:

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

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

注意我使用一个 sessionFactory 和 spring 提供的 RoutingDataSource,它根据一个键查找要使用的 db。我对多租户感兴趣,因为我使用的是支持它的 hibernate 4.3.4。

我有两个问题:

  1. 我不认为基于 URL 的拆分是有效的 只移动 10% 的流量意味着没有很多只读的 网址。我应该考虑什么方法?
  2. 可能,不知何故,基于 URL,我实现了某种程度的 两个节点之间的分布,但我会用我的石英做什么 工作(甚至有单独的 JVM)?我应该采取什么务实的态度 拿吗?

我知道我在这里可能无法得到完美的答案,因为这确实很广泛,但我只是想听听您对上下文的看法。

我的团队中的伙计们:

  • 弹簧4
  • 休眠4
  • Quartz2.2
  • Java7 / Tomcat7

请注意。提前致谢。

【问题讨论】:

  • 我有两个持久性单元——一个用于只读,一个用于读写,工作。只读的可能指向支持多个 PostgreSQL 副本的 PgBouncer。然后我会根据对我的数据访问抽象对象和其他相关上下文调用的特定方法来选择使用哪个。但是,如果您这样做,您必须非常仔细地考虑逻辑一致性,并避免读取/修改/写入循环。
  • 用户跟踪是一个可以优化的领域,如果尚未完成的话:分离为 R/O + R+W 表,会话保持缓存,写出。 归档表只接收新记录,但记录是不可变的,也可以在 R/O 和 R+W 中拆分,可能使用 DB 触发器。

标签: java postgresql hibernate jpa replication


【解决方案1】:

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

10% 阅读

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

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

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

90% 写入

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

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

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

石英

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

数据库分区

如果您每秒 1,000 个请求正在写入大量数据,请查看 partitioning 您的数据库。您可能会发现您的表格一直在增长。分区是一种无需归档数据即可解决此问题的方法。

有时您几乎不需要更改应用程序代码。

存档显然是另一种选择

免责声明:任何像这样的问题都是特定于应用程序的。总是尽量让你的架构尽可能简单。

【讨论】:

    【解决方案2】:

    Spring 事务路由

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

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

    基本上,我们检查存储当前事务上下文的 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。

    readWriteDataSource Spring 组件定义了读写 JDBC DataSource,而readOnlyDataSource 组件定义了只读 JDBC DataSource

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

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

    readWriteDataSource 使用DataSourceType.READ_WRITE 键注册,readOnlyDataSource 使用DataSourceType.READ_ONLY 键注册。

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

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

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

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

    基本上,actualDataSource 由 DataSource-Proxy 进一步包装并提供给 JPA EntityManagerFactory。您可以查看source code on 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前缀的日志语句在Primary节点上执行,而使用high_performance_java_persistence_replica的日志语句在Replica节点上执行。

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

    所有的源代码都可以在我的High-Performance 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 配置的连接数。

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

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

    【讨论】:

    • 二级缓存的表现如何?
    • 二级缓存是线程安全的,设计为并发更改,所以我不明白为什么这个解决方案会干扰它
    • 通常,只读数据库滞后于读/写数据库(异步复制)。您可以想象在读写事务期间将新条目加载到二级缓存中。之后,只读事务可能会使用 RO 数据库中尚不存在的新缓存条目,最终导致后续查询中缺少实体引用。
    • 如果 RW Tx 将更新的条目推送到缓存中,那么你就是这意味着实体将从缓存中找到,而不是从滞后的 RO DB 中找到。因此,在您的示例中,缓存的使用实际上解决了滞后问题,因此为您的系统提供了额外的好处。
    • 如果完整的实体图被标记为二级缓存(并且所有实体共享相同的过期策略),您所描述的内容将起作用。大多数时候,情况并非如此。例如。实体 A 依赖于实体 B。实体 A 被标记为可缓存,而不是 B。RW 创建 A + B。RO 事务从缓存中加载 A,然后尝试直接从滞后的 RO 数据库中读取 B。 RO 事务找不到 B,因为 B 尚未在 RO 数据库中。
    【解决方案3】:

    如果我理解正确的话,90% 的对您的 web 应用程序的 HTTP 请求都涉及至少一次写入,并且必须在主数据库上进行操作。您可以将只读事务定向到复制数据库,但改进只会影响 10% 的全局数据库操作,甚至那些只读操作也会影响数据库。

    这里的常见架构是使用良好的数据库缓存(Infinispan 或 Ehcache)。如果您可以提供足够大的缓存,您可以希望数据库读取的大部分只命中缓存并成为仅内存操作,无论是否属于只读事务的一部分。缓存调整是一项微妙的操作,但恕我直言,这是实现高性能增益所必需的。这些缓存甚至允许分布式前端,即使在这种情况下配置有点困难(如果你想使用 Ehcache,你可能需要寻找 Terracotta 集群)。

    目前,数据库复制主要用于保护数据,并且仅当您拥有仅读取数据的大部分信息系统时才将其用作并发改进机制 - 这不是您所描述的。

    【讨论】:

      【解决方案4】:

      由于复制是异步的,因此接受的解决方案将导致难以调试和难以重现二级缓存的错误。 最简洁的方法是为每个 DataSource 设置一个 EntityManagerFactory。

      【讨论】:

        猜你喜欢
        • 2012-03-01
        • 2021-06-07
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-09-17
        • 2021-05-23
        • 2021-07-03
        • 2011-09-04
        相关资源
        最近更新 更多