由于单主数据库复制体系结构不仅提供了容错能力和更高的可用性,而且使我们能够通过添加更多从节点来扩展读取操作,由此形成对主数据库进行写入操作,而对复制主数据库的从数据库进行只读操作。
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<Post> 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<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限定读写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上查看源代码以获取更多详细信息。