![](/img/trans.png)
[英]Why do spring/hibernate read-only database transactions run slower than read-write?
[英]How to split read-only and read-write transactions with JPA and Hibernate
我有一個非常重的 java webapp,每秒處理數千個請求,它使用一個主 Postgresql db,它使用流式(異步)復制將自身復制到一個輔助(只讀)數據庫。
因此,考慮到復制時間最短,我使用 URL 將請求從主數據庫分離到輔助數據庫(只讀),以避免對主數據庫進行只讀調用。
注意:我將一個 sessionFactory 與 spring 提供的 RoutingDataSource 一起使用,它根據鍵查找要使用的數據庫。 我對多租戶很感興趣,因為我正在使用支持它的 hibernate 4.3.4。
我有兩個問題:
我知道我可能不會在這里得到一個完美的答案,因為這真的很廣泛,但我只是想聽聽你對上下文的看法。
我團隊中的伙計們:
請注意。 提前致謝。
首先,我們將創建一個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
的鑒別器值。
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
屬性定義了 Primary 節點的 URL,而jdbc.url.replica
定義了 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
方法TransactionRoutingDataSource
之前獲取的。
構建 JPA EntityManagerFactory
所需的其余 Spring 組件由AbstractJPAConfiguration
基類定義。
基本上,實際數據actualDataSource
DataSource-Proxy 進一步包裝並提供給 JPA EntityManagerFactory
。 您可以查看GitHub 上的源代碼以獲取更多詳細信息。
為了檢查事務路由是否有效,我們將通過在postgresql.conf
配置文件中設置以下屬性來啟用 PostgreSQL 查詢日志:
log_min_duration_statement = 0
log_line_prefix = '[%d] '
log_min_duration_statement
屬性設置用於記錄所有 PostgreSQL 語句,而第二個將數據庫名稱添加到 SQL 日志中。
因此,在調用newPost
和findAllPostsByTitle
方法時,如下所示:
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
副本節點上。
所以,一切都像魅力一樣!
所有源代碼都可以在我的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
,該DataSource
使用連接到主節點的不同連接池。
只需確保所有連接池的總連接大小小於 PostgreSQL 配置的連接數。
每個批處理作業都必須使用專用事務,因此請確保使用合理的批處理大小。
更重要的是,您希望持有鎖並盡快完成事務。 如果批處理器正在使用並發處理 worker,請確保關聯的連接池大小等於 worker 的數量,這樣它們就不會等待其他人釋放連接。
你是說你的應用程序 URL 只有 10% 是只讀的,所以其他 90% 至少有某種形式的數據庫寫入。
10% 閱讀
您可以考慮使用可以提高數據庫讀取性能的CQRS 設計。 它當然可以從輔助數據庫讀取,並且可能通過專門為讀取/查看層設計查詢和域模型來提高效率。
你還沒有說 10% 的請求是否昂貴(例如運行報告)
如果您要遵循 CQRS 設計,我更願意使用單獨的 sessionFactory,因為正在加載/緩存的對象很可能與正在編寫的對象不同。
90% 寫
至於其他 90% 的情況,您不希望在某些寫入邏輯期間從輔助數據庫讀取(在寫入主數據庫時),因為您不希望涉及潛在的陳舊數據。
其中一些讀取可能會查找“靜態”數據。 如果 Hibernate 的緩存沒有減少讀取的數據庫命中,我會考慮使用內存緩存,如Memcached或 Redis 來處理此類數據。 10% 讀取和 90% 寫入進程都可以使用相同的緩存。
對於非靜態的讀取(即讀取您最近寫入的數據),如果數據大小合適,Hibernate 應該將數據保存在其對象緩存中。 你能確定你的緩存命中/未命中性能嗎?
石英
如果您確定計划的作業不會影響與另一個作業相同的數據集,您可以針對不同的數據庫運行它們,但是如果有疑問,請始終對一個(主)服務器執行批量更新並將更改復制出去。 邏輯上正確比引入復制問題要好。
數據庫分區
如果每秒 1,000 個請求寫入大量數據,請查看對數據庫進行分區。 您可能會發現您的表一直在增長。 分區是一種無需存檔數據即可解決此問題的方法。
有時,您幾乎不需要更改應用程序代碼。
存檔顯然是另一種選擇
免責聲明:任何像這樣的問題總是特定於應用程序的。 始終嘗試使您的架構盡可能簡單。
如果我理解正確的話,對您的 web 應用程序的 90% 的 HTTP 請求涉及至少一次寫入並且必須對主數據庫進行操作。 您可以將只讀事務定向到復制數據庫,但改進只會影響全局數據庫操作的 10%,即使是那些只讀操作也會命中數據庫。
這里的常見架構是使用好的數據庫緩存(Infinispan 或 Ehcache)。 如果您可以提供足夠大的緩存,您可以希望數據庫讀取的很大一部分只命中緩存並成為僅內存操作,無論是否屬於只讀事務。 緩存調整是一項微妙的操作,但恕我直言,這是實現高性能增益所必需的。 這些緩存甚至允許分布式前端,即使在這種情況下配置有點困難(如果你想使用 Ehcache,你可能需要尋找 Terracotta 集群)。
目前,數據庫復制主要用於保護數據,並且僅當您擁有僅讀取數據的大部分信息系統時才用作並發改進機制 - 這不是您所描述的。
由於復制是異步的,因此接受的解決方案將導致難以調試和難以重現二級緩存的錯誤。 最干凈的路徑是每個數據源有一個 EntityManagerFactory。
您也可以在您的數據庫節點前運行一個proxySQL(可以是galera集群設置),並設置查詢讀寫拆分規則,代理將根據定義的規則分配流量。 例如:SELECT 查詢路由到讀取節點,而 UPDATE 查詢或讀寫事務轉到寫入節點。
我認為這個問題很籠統,不確定為什么首選答案會將其引導到 Spring 內部? 無論如何,您可能想看看Apache ShardingSphere ,它具有以下功能:
Read/write Splitting
---------------------
Read/write splitting can be used to cope with business access with high stress. ShardingSphere provides flexible read/write splitting capabilities and can achieve read access load balancing based on the understanding of SQL semantics and the ability to perceive the underlying database topology.
我擔心的一件事是“對 SQL 語義的理解”聲明,因為如果: select myfunct(1) from dual
更改數據,那么任何庫將如何“理解”,或者不。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.