PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Comment diviser les transactions en lecture seule et en lecture-écriture avec JPA et Hibernate

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 le hibernate.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 le determineCurrentLookupKey méthode TransactionRoutingDataSource .

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.