由于单主数据库复制体系结构不仅提供了容错能力和更高的可用性,而且使我们能够通过添加更多从节点来扩展读取操作,由此形成对主数据库进行写入操作,而对复制主数据库的从数据库进行只读操作。
Spring @Transactional
在Spring应用程序中,Web @Controller调用一种@Service方法,该方法使用注释进行@Transactional注释。
默认情况下,Spring事务是可读写的,但是您可以通过注释的read-only属性将它们显式配置为在只读上下文中执行。
例如,以下ForumServiceImpl组件定义了两种服务方法:
newPost,这需要在数据库的“主”节点上执行的读写事务,以及
findAllPostsByTitle,它需要可以在数据库副本节点上执行的只读事务,因此减少了主节点上的负载
@Service
public class ForumServiceImpl
implements ForumService {
@PersistenceContext
private EntityManager entityManager;
@Override
@Transactional
public Post newPost(String title, String... tags) {
Post post = new Post();
post.setTitle(title);
post.getTags().addAll(
entityManager.createQuery("""
select t
from Tag t
where t.name in :tags""", Tag.class)
.setParameter("tags", Arrays.asList(tags))
.getResultList()
);
entityManager.persist(post);
return post;
}
@Override
@Transactional(readOnly = true)
public List findAllPostsByTitle(String title) {
return entityManager.createQuery("""
select p
from Post p
where p.title = :title""", Post.class)
.setParameter("title", title)
.getResultList();
}
}
由于@Transactional注释的readOnly属性默认设置为false,因此该newPost方法使用读写事务上下文。
Spring事务路由
目标:将读写事务路由到主节点数据库,将只读事务路由到副本从节点数据库。
我们可以定义一个ReadWriteDataSource连接主节点和ReadOnlyDataSource连接副本节点的。
读写事务路由由Spring AbstractRoutingDataSource抽象完成,由Spring 实现TransactionRoutingDatasource,如下图所示:
TransactionRoutingDataSource实现非常简单,如下所示:
public class TransactionRoutingDataSource
extends AbstractRoutingDataSource {
@Nullable
@Override
protected Object determineCurrentLookupKey() {
return TransactionSynchronizationManager
.isCurrentTransactionReadOnly() ?
DataSourceType.READ_ONLY :
DataSourceType.READ_WRITE;
}
}
基本上,我们检查TransactionSynchronizationManager存储当前事务上下文的Spring 类,以检查当前运行的Spring事务是否为只读。
该determineCurrentLookupKey方法返回鉴别符值,该鉴别符值将用于选择读写JDBC或只读JDBC DataSource。
DataSourceType仅仅是一个基本的Java枚举定义我们的事物路由选项:
public enum DataSourceType {
READ_WRITE,
READ_ONLY
}
Spring读写和只读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 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_persistencejdbc.url.replica=jdbc:postgresql://localhost:5432/high_performance_java_persistence_replicajdbc.username=postgres
jdbc.password=admin
jdbc.url.primary属性定义主节点的URL,而jdbc.url.replica定义副本节点的URL。
readWriteDataSource限定读写JDBC DataSource,而readOnlyDataSource部件限定只读JDBC DataSource。
请注意,读写数据源和只读数据源均使用HikariCP进行连接池。有关使用数据库连接池的好处的更多详细信息,请参阅本文。
这些actualDataSource充当可读写和只读数据源的外观,并使用该TransactionRoutingDataSource实用程序来实现。
在readWriteDataSource使用DataSourceType.READ_WRITE作为key注册,readOnlyDataSource使用的DataSourceType.READ_ONLY作为key注册。
因此,当执行读写@Transactional方法时,readWriteDataSource将使用,而当执行@Transactional(readOnly = true)方法时,readOnlyDataSource将使用。
请注意,该additionalProperties方法定义了hibernate.connection.provider_disables_autocommitHibernate属性,我将其添加到Hibernate中以延迟RESOURCE_LOCAL JPA事务的数据库获取。
不仅hibernate.connection.provider_disables_autocommit使您可以更好地利用数据库连接,而且这是我们使本示例工作的唯一方法,因为如果没有此配置,则必须在调用determineCurrentLookupKeymethod 之前获取连接TransactionRoutingDataSource。
有关hibernate.connection.provider_disables_autocommit配置的更多详细信息,请参阅[url=https://vladmihalcea.com/why-you-should-always-use-hibernate-connection-provider_disables_autocommit-for-resource-local-jpa-transactions/]本文[/url]。
构建JPA所需的其余Spring组件EntityManagerFactory由AbstractJPAConfiguration基类定义。
基本上,actualDataSource进一步由DataSource-Proxy包装,并提供给JPA ENtityManagerFactory。您可以在GitHub上查看源代码以获取更多详细信息。