Routage des transactions de printemps
Tout d'abord, nous allons créer un DataSourceType
Java Enum qui définit nos options de routage des transactions :
public enum DataSourceType {
READ_WRITE,
READ_ONLY
}
Pour acheminer les transactions en lecture-écriture vers le nœud principal et les transactions en lecture seule vers le nœud de réplication, nous pouvons définir un ReadWriteDataSource
qui se connecte au nœud principal et à un ReadOnlyDataSource
qui se connectent au nœud de réplique.
Le routage des transactions en lecture-écriture et en lecture seule est effectué par Spring AbstractRoutingDataSource
abstraction, qui est implémentée par le TransactionRoutingDatasource
, comme illustré par le schéma suivant :
Le TransactionRoutingDataSource
est très facile à mettre en œuvre et se présente comme suit :
public class TransactionRoutingDataSource
extends AbstractRoutingDataSource {
@Nullable
@Override
protected Object determineCurrentLookupKey() {
return TransactionSynchronizationManager
.isCurrentTransactionReadOnly() ?
DataSourceType.READ_ONLY :
DataSourceType.READ_WRITE;
}
}
Fondamentalement, nous inspectons Spring TransactionSynchronizationManager
classe qui stocke le contexte transactionnel actuel pour vérifier si la transaction Spring en cours d'exécution est en lecture seule ou non.
Le determineCurrentLookupKey
La méthode renvoie la valeur du discriminateur qui sera utilisée pour choisir soit le DataSource
JDBC en lecture-écriture ou en lecture seule .
Configuration JDBC DataSource Spring en lecture-écriture et en lecture seule
Le DataSource
la configuration ressemble à ceci :
@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));
}
}
Le /META-INF/jdbc-postgresql-replication.properties
Le fichier de ressources fournit la configuration pour le DataSource
JDBC en lecture-écriture et en lecture seule composants :
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
Le jdbc.url.primary
La propriété définit l'URL du nœud principal tandis que la propriété jdbc.url.replica
définit l'URL du nœud de réplique.
Le readWriteDataSource
Le composant Spring définit le DataSource
JDBC en lecture-écriture tandis que le readOnlyDataSource
le composant définit le DataSource
JDBC en lecture seule .
Notez que les sources de données en lecture-écriture et en lecture seule utilisent HikariCP pour le regroupement des connexions.
Le actualDataSource
agit comme une façade pour les sources de données en lecture-écriture et en lecture seule et est implémenté à l'aide de TransactionRoutingDataSource
utilitaire.
Le readWriteDataSource
est enregistré à l'aide du DataSourceType.READ_WRITE
key et le readOnlyDataSource
en utilisant le DataSourceType.READ_ONLY
clé.
Ainsi, lors de l'exécution d'un @Transactional
en lecture-écriture méthode, la readWriteDataSource
sera utilisé lors de l'exécution d'un @Transactional(readOnly = true)
méthode, le readOnlyDataSource
sera utilisé à la place.
Notez que les
additionalProperties
la méthode définit lehibernate.connection.provider_disables_autocommit
Propriété Hibernate, que j'ai ajoutée à Hibernate pour reporter l'acquisition de la base de données pour les transactions RESOURCE_LOCAL JPA.Non seulement que le
hibernate.connection.provider_disables_autocommit
vous permet de mieux utiliser les connexions à la base de données, mais c'est la seule façon de faire fonctionner cet exemple puisque, sans cette configuration, la connexion est acquise avant d'appeler ledetermineCurrentLookupKey
méthodeTransactionRoutingDataSource
.
Les composants Spring restants nécessaires à la construction de la JPA EntityManagerFactory
sont définis par la AbstractJPAConfiguration
classe de base.
Fondamentalement, le actualDataSource
est en outre enveloppé par DataSource-Proxy et fourni à la JPA EntityManagerFactory
. Vous pouvez vérifier le code source sur GitHub pour plus de détails.
Temps de test
Pour vérifier si le routage des transactions fonctionne, nous allons activer le journal des requêtes PostgreSQL en définissant les propriétés suivantes dans le postgresql.conf
fichier de configuration :
log_min_duration_statement = 0
log_line_prefix = '[%d] '
La log_min_duration_statement
Le paramètre de propriété sert à enregistrer toutes les instructions PostgreSQL tandis que le second ajoute le nom de la base de données au journal SQL.
Ainsi, lors de l'appel du newPost
et findAllPostsByTitle
méthodes, comme celle-ci :
Post post = forumService.newPost(
"High-Performance Java Persistence",
"JDBC", "JPA", "Hibernate"
);
List<Post> posts = forumService.findAllPostsByTitle(
"High-Performance Java Persistence"
);
Nous pouvons voir que PostgreSQL enregistre les messages suivants :
[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
Les instructions de journal utilisant la high_performance_java_persistence
préfixe ont été exécutés sur le nœud principal tandis que ceux utilisant le high_performance_java_persistence_replica
sur le nœud de réplique.
Donc, tout fonctionne comme un charme !
Tout le code source se trouve dans mon référentiel GitHub Java Persistence haute performance, vous pouvez donc l'essayer également.
Conclusion
Vous devez vous assurer de définir la bonne taille pour vos pools de connexion, car cela peut faire une énorme différence. Pour cela, je recommande d'utiliser Flexy Pool.
Vous devez être très diligent et vous assurer de marquer toutes les transactions en lecture seule en conséquence. Il est inhabituel que seulement 10 % de vos transactions soient en lecture seule. Se pourrait-il que vous disposiez d'une telle application en écriture ou que vous utilisiez des transactions d'écriture dans lesquelles vous n'émettez que des instructions de requête ?
Pour le traitement par lots, vous avez certainement besoin de transactions en lecture-écriture, alors assurez-vous d'activer le traitement par lots JDBC, comme ceci :
<property name="hibernate.order_updates" value="true"/>
<property name="hibernate.order_inserts" value="true"/>
<property name="hibernate.jdbc.batch_size" value="25"/>
Pour le traitement par lots, vous pouvez également utiliser un DataSource
séparé qui utilise un pool de connexion différent qui se connecte au nœud principal.
Assurez-vous simplement que la taille totale de vos connexions de tous les pools de connexions est inférieure au nombre de connexions avec lesquelles PostgreSQL a été configuré.
Chaque travail par lots doit utiliser une transaction dédiée, alors assurez-vous d'utiliser une taille de lot raisonnable.
De plus, vous souhaitez détenir des verrous et terminer les transactions le plus rapidement possible. Si le traitement par lots utilise des travailleurs de traitement simultané, assurez-vous que la taille du pool de connexions associée est égale au nombre de travailleurs, afin qu'ils n'attendent pas que d'autres libèrent des connexions.