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.