弹簧交易路由
首先,我们将创建一个 Java 枚举来定义我们的事务路由选项:DataSourceType
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类,以检查当前运行的Spring事务是否为只读。TransactionSynchronizationManager
该方法返回鉴别器值,该值将用于选择读写或只读 JDBC 。determineCurrentLookupKey
DataSource
弹簧读写和只读 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.properties
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
该属性定义主节点的 URL,而 定义副本节点的 URL。jdbc.url.primary
jdbc.url.replica
Spring 组件定义读写 JDBC,而组件定义只读 JDBC。readWriteDataSource
DataSource
readOnlyDataSource
DataSource
请注意,读写数据源和只读数据源都使用 HikariCP 进行连接池。
它们充当读写和只读数据源的外观,并使用该实用工具实现。actualDataSource
TransactionRoutingDataSource
使用密钥注册 ,并使用密钥注册。readWriteDataSource
DataSourceType.READ_WRITE
readOnlyDataSource
DataSourceType.READ_ONLY
因此,在执行读写方法时,将使用 ,而在执行方法时,将使用。@Transactional
readWriteDataSource
@Transactional(readOnly = true)
readOnlyDataSource
请注意,该方法定义了 Hibernate 属性,我将其添加到 Hibernate 中以推迟RESOURCE_LOCAL JPA 事务的数据库获取。additionalProperties
hibernate.connection.provider_disables_autocommit
hibernate.connection.provider_disables_autocommit
不仅允许您更好地利用数据库连接,而且这是我们使此示例工作的唯一方法,因为如果没有此配置,则在调用方法之前获取连接。determineCurrentLookupKey
TransactionRoutingDataSource
构建 JPA 所需的其余 Spring 组件由 AbstractJPAConfiguration
基类定义。EntityManagerFactory
基本上,它由DataSource-Proxy进一步包装并提供给JPA。您可以在 GitHub 上查看源代码以获取更多详细信息。actualDataSource
EntityManagerFactory
测试时间
为了检查事务路由是否有效,我们将通过在配置文件中设置以下属性来启用PostgreSQL查询日志:postgresql.conf
log_min_duration_statement = 0
log_line_prefix = '[%d] '
属性设置用于记录所有 PostgreSQL 语句,而第二个设置则将数据库名称添加到 SQL 日志中。log_min_duration_statement
因此,在调用 和 方法时,如下所示: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
所以,一切都像一个魅力!
所有的源代码都可以在我的高性能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配置的连接数即可。
每个批处理作业都必须使用专用事务,因此请确保使用合理的批处理大小。
此外,您希望保持锁定并尽快完成事务。如果批处理处理器使用的是并发处理工作线程,请确保关联的连接池大小等于工作线程数,这样它们就不会等待其他人释放连接。