PostgreSQL
 sql >> Database >  >> RDS >> PostgreSQL

Come dividere le transazioni di sola lettura e lettura-scrittura con JPA e Hibernate

Routing delle transazioni primaverili

Per prima cosa creeremo un DataSourceType Java Enum che definisce le nostre opzioni di instradamento delle transazioni:

public enum  DataSourceType {
    READ_WRITE,
    READ_ONLY
}

Per instradare le transazioni di lettura-scrittura al nodo primario e le transazioni di sola lettura al nodo Replica, possiamo definire un ReadWriteDataSource che si connette al nodo primario e a un ReadOnlyDataSource che si connettono al nodo Replica.

L'instradamento delle transazioni in lettura-scrittura e sola lettura viene eseguito da Spring AbstractRoutingDataSource astrazione, implementata da TransactionRoutingDatasource , come illustrato dal diagramma seguente:

Il TransactionRoutingDataSource è molto facile da implementare e si presenta come segue:

public class TransactionRoutingDataSource 
        extends AbstractRoutingDataSource {

    @Nullable
    @Override
    protected Object determineCurrentLookupKey() {
        return TransactionSynchronizationManager
            .isCurrentTransactionReadOnly() ?
            DataSourceType.READ_ONLY :
            DataSourceType.READ_WRITE;
    }
}

Fondamentalmente, ispezioniamo il TransactionSynchronizationManager di Spring classe che memorizza il contesto transazionale corrente per verificare se la transazione Spring attualmente in esecuzione è di sola lettura o meno.

Il determineCurrentLookupKey restituisce il valore del discriminatore che verrà utilizzato per scegliere il JDBC in lettura-scrittura o in sola lettura DataSource .

Configurazione dell'origine dati JDBC in lettura, scrittura e sola lettura

Il DataSource la configurazione appare come segue:

@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));
    }
}

Il /META-INF/jdbc-postgresql-replication.properties file di risorse fornisce la configurazione per il DataSource JDBC in lettura-scrittura e sola lettura componenti:

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

Il jdbc.url.primary la proprietà definisce l'URL del nodo primario mentre il jdbc.url.replica definisce l'URL del nodo Replica.

Il readWriteDataSource Il componente Spring definisce il DataSource JDBC in lettura e scrittura mentre readOnlyDataSource il componente definisce il JDBC di sola lettura DataSource .

Si noti che sia le origini dati di lettura-scrittura che di sola lettura utilizzano HikariCP per il pool di connessioni.

Il actualDataSource funge da facciata per le origini dati in lettura-scrittura e sola lettura ed è implementato utilizzando il TransactionRoutingDataSource utilità.

Il readWriteDataSource è registrato utilizzando il DataSourceType.READ_WRITE chiave e il readOnlyDataSource utilizzando il DataSourceType.READ_ONLY chiave.

Quindi, quando si esegue un @Transactional di lettura e scrittura metodo, il readWriteDataSource verrà utilizzato durante l'esecuzione di un @Transactional(readOnly = true) metodo, il readOnlyDataSource verrà invece utilizzato.

Nota che le additionalProperties il metodo definisce il hibernate.connection.provider_disables_autocommit Proprietà Hibernate, che ho aggiunto a Hibernate per posticipare l'acquisizione del database per le transazioni RESOURCE_LOCAL JPA.

Non solo hibernate.connection.provider_disables_autocommit consente di utilizzare meglio le connessioni al database, ma è l'unico modo in cui possiamo far funzionare questo esempio poiché, senza questa configurazione, la connessione viene acquisita prima di chiamare la determineCurrentLookupKey metodo TransactionRoutingDataSource .

I restanti componenti Spring necessari per la creazione di JPA EntityManagerFactory sono definiti da AbstractJPAConfiguration classe base.

Fondamentalmente, il actualDataSource viene ulteriormente avvolto da DataSource-Proxy e fornito a JPA EntityManagerFactory . Puoi controllare il codice sorgente su GitHub per maggiori dettagli.

Tempo di prova

Per verificare se l'instradamento delle transazioni funziona, abiliteremo il log delle query PostgreSQL impostando le seguenti proprietà in postgresql.conf file di configurazione:

log_min_duration_statement = 0
log_line_prefix = '[%d] '

Il log_min_duration_statement l'impostazione della proprietà è per la registrazione di tutte le istruzioni PostgreSQL mentre la seconda aggiunge il nome del database al log SQL.

Quindi, quando chiami il newPost e findAllPostsByTitle metodi, come questo:

Post post = forumService.newPost(
    "High-Performance Java Persistence",
    "JDBC", "JPA", "Hibernate"
);

List<Post> posts = forumService.findAllPostsByTitle(
    "High-Performance Java Persistence"
);

Possiamo vedere che PostgreSQL registra i seguenti messaggi:

[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

Le istruzioni di registro che utilizzano high_performance_java_persistence prefisso sono stati eseguiti sul nodo primario mentre quelli che utilizzano il high_performance_java_persistence_replica sul nodo Replica.

Quindi, tutto funziona come un incantesimo!

Tutto il codice sorgente può essere trovato nel mio repository GitHub Java Persistence ad alte prestazioni, quindi puoi provarlo anche tu.

Conclusione

Devi assicurarti di impostare la dimensione giusta per i tuoi pool di connessioni perché ciò può fare un'enorme differenza. Per questo, consiglio di utilizzare Flexy Pool.

Devi essere molto diligente e assicurarti di contrassegnare tutte le transazioni di sola lettura di conseguenza. È insolito che solo il 10% delle tue transazioni sia di sola lettura. Potrebbe essere che tu disponga di un'applicazione di questo tipo o stai utilizzando transazioni di scrittura in cui emetti solo istruzioni di query?

Per l'elaborazione in batch, hai sicuramente bisogno di transazioni di lettura e scrittura, quindi assicurati di abilitare il batching JDBC, in questo modo:

<property name="hibernate.order_updates" value="true"/>
<property name="hibernate.order_inserts" value="true"/>
<property name="hibernate.jdbc.batch_size" value="25"/>

Per il batch puoi anche utilizzare un DataSource separato che utilizza un pool di connessioni diverso che si connette al nodo primario.

Assicurati solo che la dimensione totale della tua connessione di tutti i pool di connessione sia inferiore al numero di connessioni con cui è stato configurato PostgreSQL.

Ogni processo batch deve utilizzare una transazione dedicata, quindi assicurati di utilizzare una dimensione batch ragionevole.

Inoltre, vuoi mantenere i blocchi e completare le transazioni il più velocemente possibile. Se il processore batch utilizza processi di elaborazione simultanei, assicurati che la dimensione del pool di connessioni associato sia uguale al numero di lavoratori, in modo che non aspettino che altri rilasci le connessioni.