sql >> Databasteknik >  >> RDS >> PostgreSQL

Hur man delar skrivskyddade och läs-skrivtransaktioner med JPA och Hibernate

Vår transaktionsdirigering

Först skapar vi en DataSourceType Java Enum som definierar våra transaktionsdirigeringsalternativ:

public enum  DataSourceType {
    READ_WRITE,
    READ_ONLY
}

För att dirigera läs- och skrivtransaktionerna till den primära noden och skrivskyddade transaktioner till replikanoden kan vi definiera en ReadWriteDataSource som ansluter till den primära noden och en ReadOnlyDataSource som ansluter till replikanoden.

Läs-skriv- och skrivskyddad transaktionsdirigering görs av Spring AbstractRoutingDataSource abstraktion, som implementeras av TransactionRoutingDatasource , som illustreras av följande diagram:

TransactionRoutingDataSource är mycket lätt att implementera och ser ut som följer:

public class TransactionRoutingDataSource 
        extends AbstractRoutingDataSource {

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

I grund och botten inspekterar vi Spring TransactionSynchronizationManager klass som lagrar den aktuella transaktionskontexten för att kontrollera om den aktuella Spring-transaktionen är skrivskyddad eller inte.

determineCurrentLookupKey metod returnerar diskriminatorvärdet som kommer att användas för att välja antingen läs-skriv eller skrivskyddad JDBC DataSource .

Vår läs-skriv och skrivskyddad JDBC DataSource-konfiguration

DataSource konfigurationen ser ut som följer:

@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 resursfil tillhandahåller konfigurationen för läs-skriv- och skrivskyddad JDBC DataSource komponenter:

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 egenskapen definierar URL:en för den primära noden medan jdbc.url.replica definierar URL:en för replikanoden.

readWriteDataSource Spring-komponenten definierar läs-skriv JDBC DataSource medan readOnlyDataSource komponenten definierar den skrivskyddade JDBC DataSource .

Observera att både läs-skriv- och skrivskyddad datakälla använder HikariCP för anslutningspoolning.

actualDataSource fungerar som en fasad för läs-skriv- och skrivskyddade datakällor och implementeras med hjälp av TransactionRoutingDataSource verktyg.

readWriteDataSource är registrerad med DataSourceType.READ_WRITE nyckeln och readOnlyDataSource med DataSourceType.READ_ONLY nyckel.

Så när du kör en läs-skriv @Transactional metoden, readWriteDataSource kommer att användas när en @Transactional(readOnly = true) körs metoden, readOnlyDataSource kommer att användas istället.

Observera att additionalProperties metoden definierar hibernate.connection.provider_disables_autocommit Hibernate-egenskap, som jag lade till i Hibernate för att skjuta upp databasförvärvet för RESOURCE_LOCAL JPA-transaktioner.

Inte bara att hibernate.connection.provider_disables_autocommit tillåter dig att bättre använda databasanslutningar, men det är det enda sättet vi kan få det här exemplet att fungera eftersom, utan denna konfiguration, anslutningen förvärvas innan determineCurrentLookupKey anropas metod TransactionRoutingDataSource .

De återstående Spring-komponenterna som behövs för att bygga JPA EntityManagerFactory definieras av AbstractJPAConfiguration basklass.

I grund och botten är actualDataSource lindas vidare av DataSource-Proxy och tillhandahålls till JPA EntityManagerFactory . Du kan kontrollera källkoden på GitHub för mer information.

Testtid

För att kontrollera om transaktionsdirigeringen fungerar, kommer vi att aktivera PostgreSQL-frågeloggen genom att ställa in följande egenskaper i postgresql.conf konfigurationsfil:

log_min_duration_statement = 0
log_line_prefix = '[%d] '

log_min_duration_statement egenskapsinställningen är för att logga alla PostgreSQL-satser medan den andra lägger till databasnamnet till SQL-loggen.

Så när du ringer newPost och findAllPostsByTitle metoder, så här:

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

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

Vi kan se att PostgreSQL loggar följande meddelanden:

[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

Loggsatserna som använder high_performance_java_persistence prefixet kördes på den primära noden medan de som använde high_performance_java_persistence_replica på repliknoden.

Så allt fungerar som en smäck!

All källkod finns i mitt högpresterande Java Persistence GitHub-förråd, så du kan prova det också.

Slutsats

Du måste se till att du ställer in rätt storlek för dina anslutningspooler eftersom det kan göra en enorm skillnad. För detta rekommenderar jag att du använder Flexy Pool.

Du måste vara mycket noggrann och se till att du markerar alla skrivskyddade transaktioner i enlighet med detta. Det är ovanligt att endast 10 % av dina transaktioner är skrivskyddade. Kan det vara så att du har ett sådant skriv-mest-program eller att du använder skrivtransaktioner där du bara utfärdar frågeutlåtanden?

För batchbehandling behöver du definitivt läs-skrivtransaktioner, så se till att du aktiverar JDBC-batchning, så här:

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

För batchning kan du också använda en separat DataSource som använder en annan anslutningspool som ansluter till den primära noden.

Se bara till att din totala anslutningsstorlek för alla anslutningspooler är mindre än antalet anslutningar som PostgreSQL har konfigurerats med.

Varje batchjobb måste använda en dedikerad transaktion, så se till att du använder en rimlig batchstorlek.

Mer, du vill hålla lås och slutföra transaktioner så snabbt som möjligt. Om batchprocessorn använder arbetare för samtidig bearbetning, se till att den tillhörande anslutningspoolstorleken är lika med antalet arbetare, så att de inte väntar på att andra ska släppa anslutningar.



  1. GUI-verktyg för PostgreSQL

  2. Access Class Module och Wrapper Classes

  3. Du, vem äger det där #temp-bordet?

  4. MariaDB JSON_MERGE_PRESERVE() Förklarad