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.