我有一个非常繁重的 java webapp,每秒处理数千个请求,它使用一个主 Postgresql 数据库,它使用流式(异步)复制将自身复制到一个辅助(只读)数据库。
因此,考虑到复制时间最短,我使用 URL 将请求从主数据库分离到辅助数据库(只读),以避免对主数据库进行只读调用。
注意: 我将一个 sessionFactory 与 spring 提供的 RoutingDataSource 一起使用,它根据键查找要使用的数据库。我对多租户很感兴趣,因为我使用的是支持它的休眠 4.3.4。
我有两个问题:
- 我认为基于 URL 的拆分效率不高,因为我只能移动 10% 的流量,这意味着只读 URL 不多。我应该考虑什么方法?
- 可能是,在 URL 的基础上,我在两个节点之间实现了某种程度的分布,但是我将如何处理我的石英作业(甚至有单独的 JVM)?我应该采取什么务实的方法?
我知道我可能不会在这里得到一个完美的答案,因为这真的很广泛,但我只是想听听你对上下文的看法。
我团队中的伙计们:
- 春天4
- 休眠4
- 石英2.2
- Java7 / Tomcat7
请注意。提前致谢。
原文由 Sachin Verma 发布,翻译遵循 CC BY-SA 4.0 许可协议
弹簧事务路由
首先,我们将创建一个
DataSourceType
Java Enum 来定义我们的事务路由选项:要将读写事务路由到主节点并将只读事务路由到副本节点,我们可以定义一个连接到主节点的
ReadOnlyDataSource
ReadWriteDataSource
一个连接到主节点的—副本节点。读写和只读事务路由由 Spring
AbstractRoutingDataSource
抽象完成,由TransactionRoutingDatasource
实现,如下图所示:TransactionRoutingDataSource
非常容易实现,如下所示:基本上,我们检查存储当前事务上下文的 Spring
TransactionSynchronizationManager
类,以检查当前运行的 Spring 事务是否是只读的。determineCurrentLookupKey
方法返回将用于选择读写或只读 JDBCDataSource
的鉴别器值。Spring读写和只读JDBC DataSource配置
DataSource
配置如下所示:/META-INF/jdbc-postgresql-replication.properties
资源文件提供了读写和只读JDBCDataSource
组件的配置:jdbc.url.primary
属性定义主节点的 URL,而jdbc.url.replica
定义副本节点的 URL。The
readWriteDataSource
Spring component defines the read-write JDBCDataSource
while thereadOnlyDataSource
component define the read-only JDBCDataSource
.actualDataSource
充当读写和只读数据源的外观,并使用TransactionRoutingDataSource
实用程序实现。The
readWriteDataSource
is registered using theDataSourceType.READ_WRITE
key and thereadOnlyDataSource
using theDataSourceType.READ_ONLY
key.So, when executing a read-write
@Transactional
method, thereadWriteDataSource
will be used while when executing a@Transactional(readOnly = true)
method, thereadOnlyDataSource
will被使用。构建 JPA 所需的其余 Spring 组件
EntityManagerFactory
由AbstractJPAConfiguration
基类定义。基本上,
actualDataSource
由 DataSource-Proxy 进一步包装并提供给 JPAEntityManagerFactory
。您可以查看 GitHub 上的源代码以 获取更多详细信息。测试时间
要检查事务路由是否有效,我们将通过在
postgresql.conf
配置文件中设置以下属性来启用 PostgreSQL 查询日志:log_min_duration_statement
属性设置用于记录所有 PostgreSQL 语句,而第二个将数据库名称添加到 SQL 日志。因此,当调用
newPost
和findAllPostsByTitle
方法时,如下所示:我们可以看到 PostgreSQL 记录了以下消息:
使用
high_performance_java_persistence
前缀的日志语句在主节点上执行,而使用high_performance_java_persistence_replica
的日志语句在副本节点上执行。GitHub 资料库
这不仅仅是理论。这一切都在 GitHub 上,并且非常有效。使用 此测试用例 作为参考。
因此,您可以将它用作事务路由解决方案的起点,因为您有一个功能齐全的示例。
二级缓存
一旦你使用了复制,你就在分布式环境中运行,所以你需要使用分布式缓存解决方案,比如 Infinispan 。
由于我们使用复制将流量分配到更多数据库节点,很明显我们还有多个应用程序节点必须连接到这些数据库节点。
因此,在这样的环境中使用
READ_WRITE
CacheConcurrencyStrategy
是一个可怕的反模式,因为每个分布式节点都会保留自己的缓存条目副本,导致一致性问题,即使你没有使用事务路由。更不用说如果您对应用程序节点采用自动缩放,您将面临的冷缓存问题,因为它们会放大数据库流量,因为新节点将从冷缓存开始。
结论
您需要确保为连接池设置正确的大小,因为这会产生巨大的差异。为此,我建议使用 Flexy Pool 。
您需要非常勤奋并确保相应地标记所有只读事务。只有 10% 的交易是只读的,这很不寻常。可能是您有这样一个写入最多的应用程序,或者您正在使用只发出查询语句的写入事务?
对于批处理,您肯定需要读写事务,因此请确保启用 JDBC 批处理,如下所示:
对于批处理,您还可以使用单独的
DataSource
,它使用连接到主节点的不同连接池。只要确保所有连接池的总连接大小小于 PostgreSQL 配置的连接数即可。
每个批处理作业都必须使用专用事务,因此请确保使用合理的批处理大小。
此外,您希望持有锁并尽快完成交易。如果批处理器正在使用并发处理 workers,请确保关联的连接池大小等于 workers 的数量,这样他们就不会等待其他人释放连接。