PostgreSQL
 sql >> Datenbank >  >> RDS >> PostgreSQL

So teilen Sie schreibgeschützte und schreibgeschützte Transaktionen mit JPA und Hibernate auf

Frühjahrs-Transaktionsrouting

Zuerst erstellen wir einen DataSourceType Java Enum, das unsere Transaktions-Routing-Optionen definiert:

public enum  DataSourceType {
    READ_WRITE,
    READ_ONLY
}

Um die Lese-Schreib-Transaktionen an den primären Knoten und schreibgeschützte Transaktionen an den Replikatknoten weiterzuleiten, können wir eine ReadWriteDataSource definieren die mit dem primären Knoten und einer ReadOnlyDataSource verbunden ist die eine Verbindung zum Replikatknoten herstellen.

Das Lese-Schreib- und Nur-Lese-Transaktionsrouting wird von Spring AbstractRoutingDataSource durchgeführt Abstraktion, die durch die TransactionRoutingDatasource implementiert wird , wie im folgenden Diagramm dargestellt:

Die TransactionRoutingDataSource ist sehr einfach zu implementieren und sieht wie folgt aus:

public class TransactionRoutingDataSource 
        extends AbstractRoutingDataSource {

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

Grundsätzlich untersuchen wir den Spring TransactionSynchronizationManager Klasse, die den aktuellen Transaktionskontext speichert, um zu prüfen, ob die aktuell laufende Spring-Transaktion schreibgeschützt ist oder nicht.

Der determineCurrentLookupKey -Methode gibt den Diskriminatorwert zurück, der verwendet wird, um entweder die Read-Write- oder die Read-Only-JDBC-DataSource auszuwählen .

Spring read-write und read-only JDBC DataSource Konfiguration

Die DataSource Konfiguration sieht wie folgt aus:

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

Die /META-INF/jdbc-postgresql-replication.properties Die Ressourcendatei stellt die Konfiguration für die Read-Write- und Read-Only-JDBC-DataSource bereit Komponenten:

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

Die jdbc.url.primary Die Eigenschaft definiert die URL des primären Knotens, während die jdbc.url.replica definiert die URL des Replica-Knotens.

Die readWriteDataSource Die Spring-Komponente definiert die Read-Write-JDBC DataSource während die readOnlyDataSource Komponente definieren die schreibgeschützte JDBC DataSource .

Beachten Sie, dass sowohl die Lese-Schreib- als auch die Nur-Lese-Datenquellen HikariCP für das Verbindungspooling verwenden.

Die actualDataSource fungiert als Fassade für die Read-Write- und Read-Only-Datenquellen und wird mithilfe von TransactionRoutingDataSource implementiert Dienstprogramm.

Die readWriteDataSource wird mit DataSourceType.READ_WRITE registriert key und die readOnlyDataSource mit dem DataSourceType.READ_ONLY Schlüssel.

Also beim Ausführen eines Read-Write @Transactional Methode, die readWriteDataSource wird beim Ausführen eines @Transactional(readOnly = true) verwendet Methode, die readOnlyDataSource wird stattdessen verwendet.

Beachten Sie, dass die additionalProperties -Methode definiert den hibernate.connection.provider_disables_autocommit Hibernate-Eigenschaft, die ich zu Hibernate hinzugefügt habe, um den Datenbankerwerb für RESOURCE_LOCAL JPA-Transaktionen zu verschieben.

Nicht nur das der hibernate.connection.provider_disables_autocommit ermöglicht es Ihnen, Datenbankverbindungen besser zu nutzen, aber nur so können wir dieses Beispiel zum Laufen bringen, da ohne diese Konfiguration die Verbindung vor dem Aufruf von determineCurrentLookupKey abgerufen wird Methode TransactionRoutingDataSource .

Die verbleibenden Spring-Komponenten, die zum Erstellen der JPA EntityManagerFactory benötigt werden werden durch die AbstractJPAConfiguration definiert Basisklasse.

Grundsätzlich die actualDataSource wird von DataSource-Proxy weiter verpackt und der JPA EntityManagerFactory bereitgestellt . Weitere Einzelheiten finden Sie im Quellcode auf GitHub.

Testzeit

Um zu überprüfen, ob das Transaktionsrouting funktioniert, aktivieren wir das PostgreSQL-Abfrageprotokoll, indem wir die folgenden Eigenschaften in der postgresql.conf festlegen Konfigurationsdatei:

log_min_duration_statement = 0
log_line_prefix = '[%d] '

Die log_min_duration_statement Die Eigenschaftseinstellung dient zum Protokollieren aller PostgreSQL-Anweisungen, während die zweite den Datenbanknamen zum SQL-Protokoll hinzufügt.

Also beim Aufruf von newPost und findAllPostsByTitle Methoden wie diese:

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

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

Wir können sehen, dass PostgreSQL die folgenden Nachrichten protokolliert:

[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

Die Protokollanweisungen, die high_performance_java_persistence verwenden Präfix wurden auf dem primären Knoten ausgeführt, während diejenigen, die high_performance_java_persistence_replica verwenden auf dem Replikatknoten.

Also, alles funktioniert wie am Schnürchen!

Den gesamten Quellcode finden Sie in meinem High-Performance Java Persistence GitHub-Repository, sodass Sie ihn auch ausprobieren können.

Schlussfolgerung

Sie müssen sicherstellen, dass Sie die richtige Größe für Ihre Verbindungspools festlegen, da dies einen großen Unterschied machen kann. Dafür empfehle ich die Verwendung von Flexy Pool.

Sie müssen sehr sorgfältig sein und sicherstellen, dass Sie alle schreibgeschützten Transaktionen entsprechend markieren. Es ist ungewöhnlich, dass nur 10 % Ihrer Transaktionen schreibgeschützt sind. Könnte es sein, dass Sie eine solche Write-Most-Anwendung haben oder Schreibtransaktionen verwenden, bei denen Sie nur Abfrageanweisungen ausgeben?

Für die Batch-Verarbeitung benötigen Sie auf jeden Fall Lese-Schreib-Transaktionen, stellen Sie also sicher, dass Sie JDBC-Batching aktivieren, wie hier:

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

Für das Batching können Sie auch eine separate DataSource verwenden der einen anderen Verbindungspool verwendet, der eine Verbindung zum primären Knoten herstellt.

Stellen Sie einfach sicher, dass Ihre Gesamtverbindungsgröße aller Verbindungspools kleiner ist als die Anzahl der Verbindungen, mit denen PostgreSQL konfiguriert wurde.

Jeder Batch-Job muss eine dedizierte Transaktion verwenden, stellen Sie also sicher, dass Sie eine angemessene Batch-Größe verwenden.

Außerdem möchten Sie Sperren halten und Transaktionen so schnell wie möglich abschließen. Wenn der Batch-Prozessor Worker für die gleichzeitige Verarbeitung verwendet, stellen Sie sicher, dass die Größe des zugeordneten Verbindungspools der Anzahl der Worker entspricht, damit sie nicht darauf warten, dass andere Verbindungen freigeben.