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;
}
}
基本上,我们检查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
方法
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
的则在副本节点上执行。
GitHub 代码库
这不仅是理论。所有内容都在 GitHub 上,并且可以完美运行。可以使用 this test case 作为参考。
因此,您可以将其用作事务路由解决方案的起点,因为您拥有一个完全功能的示例。
二级缓存
一旦您使用复制功能,就会在分布式环境中运行,因此您需要使用分布式缓存解决方案,例如 Infinispan。
由于我们使用复制来将流量分发到更多的数据库节点,所以很明显我们还有多个应用程序节点需要连接到这些数据库节点。
因此,在这种环境中使用
READ_WRITE
CacheConcurrencyStrategy
是一个可怕的反模式,因为每个分布式节点将保留其自己的缓存条目副本,即使您没有使用事务路由,也会导致一致性问题。
更不用说如果您为应用程序节点使用了自动扩展,那么您将面临冷缓存问题,因为新节点将以冷缓存开始,从而放大数据库流量。
所以,如果您计划在二级缓存机制中使用事务路由,那么您可以做得更好。使用
NONSTRICT_READ_WRITE
缓存并发策略,并使用可以将缓存数据存储在分布式节点系统中的二级缓存提供程序,即使您创建新的应用程序节点,缓存数据也是随时可用的。
结论
您需要确保为连接池设置正确的大小,因为这可能会产生巨大的差异。为此,我建议使用
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 连接数。”
“每个批处理作业必须使用专用事务,因此请确保使用合理的批量大小。”
“此外,您希望尽快持有锁并完成事务。如果批处理器正在使用并发处理工作程序,请确保关联的连接池大小等于工作程序数量,以便它们不必等待其他人释放连接。”