Enrutamiento de transacciones de primavera
Primero, crearemos un DataSourceType
Java Enum que define nuestras opciones de enrutamiento de transacciones:
public enum DataSourceType {
READ_WRITE,
READ_ONLY
}
Para enrutar las transacciones de lectura y escritura al nodo principal y las transacciones de solo lectura al nodo de réplica, podemos definir un ReadWriteDataSource
que se conecta al nodo principal y a un ReadOnlyDataSource
que se conectan al nodo Réplica.
El enrutamiento de transacciones de lectura-escritura y solo lectura lo realiza Spring AbstractRoutingDataSource
abstracción, que es implementada por TransactionRoutingDatasource
, como se ilustra en el siguiente diagrama:
El TransactionRoutingDataSource
es muy fácil de implementar y tiene el siguiente aspecto:
public class TransactionRoutingDataSource
extends AbstractRoutingDataSource {
@Nullable
@Override
protected Object determineCurrentLookupKey() {
return TransactionSynchronizationManager
.isCurrentTransactionReadOnly() ?
DataSourceType.READ_ONLY :
DataSourceType.READ_WRITE;
}
}
Básicamente, inspeccionamos Spring TransactionSynchronizationManager
clase que almacena el contexto transaccional actual para verificar si la transacción Spring que se está ejecutando actualmente es de solo lectura o no.
El determineCurrentLookupKey
El método devuelve el valor del discriminador que se utilizará para elegir el DataSource
de JDBC de lectura y escritura o de solo lectura. .
Configuración Spring de fuente de datos JDBC de solo lectura y lectura-escritura
El DataSource
la configuración es la siguiente:
@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));
}
}
El /META-INF/jdbc-postgresql-replication.properties
El archivo de recursos proporciona la configuración para el DataSource
de JDBC de lectura, escritura y solo lectura. componentes:
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
define la URL del nodo principal mientras que jdbc.url.replica
define la URL del nodo Réplica.
El readWriteDataSource
El componente Spring define el JDBC de lectura y escritura DataSource
mientras que readOnlyDataSource
componente define el JDBC de solo lectura DataSource
.
Tenga en cuenta que tanto las fuentes de datos de lectura y escritura como las de solo lectura utilizan HikariCP para la agrupación de conexiones.
El actualDataSource
actúa como una fachada para las fuentes de datos de lectura-escritura y de solo lectura y se implementa utilizando el TransactionRoutingDataSource
utilidad.
El readWriteDataSource
está registrado usando el DataSourceType.READ_WRITE
clave y el readOnlyDataSource
usando el DataSourceType.READ_ONLY
clave.
Entonces, al ejecutar un comando de lectura y escritura @Transactional
método, el readWriteDataSource
se usará mientras se ejecuta un @Transactional(readOnly = true)
método, el readOnlyDataSource
se usará en su lugar.
Tenga en cuenta que las additionalProperties
define el método hibernate.connection.provider_disables_autocommit
Propiedad de Hibernate, que agregué a Hibernate para posponer la adquisición de la base de datos para transacciones RESOURCE_LOCAL JPA.
No solo que el hibernate.connection.provider_disables_autocommit
le permite hacer un mejor uso de las conexiones de la base de datos, pero es la única forma en que podemos hacer que este ejemplo funcione ya que, sin esta configuración, la conexión se adquiere antes de llamar a determineCurrentLookupKey
método TransactionRoutingDataSource
.
Los componentes restantes de Spring necesarios para construir JPA EntityManagerFactory
están definidos por AbstractJPAConfiguration
clase base.
Básicamente, el actualDataSource
es envuelto por DataSource-Proxy y proporcionado a JPA EntityManagerFactory
. Puede consultar el código fuente en GitHub para obtener más detalles.
Tiempo de prueba
Para verificar si el enrutamiento de transacciones funciona, habilitaremos el registro de consultas de PostgreSQL configurando las siguientes propiedades en postgresql.conf
archivo de configuración:
log_min_duration_statement = 0
log_line_prefix = '[%d] '
log_min_duration_statement
la configuración de la propiedad es para registrar todas las declaraciones de PostgreSQL, mientras que la segunda agrega el nombre de la base de datos al registro de SQL.
Entonces, al llamar al newPost
y findAllPostsByTitle
métodos, como este:
Post post = forumService.newPost(
"High-Performance Java Persistence",
"JDBC", "JPA", "Hibernate"
);
List<Post> posts = forumService.findAllPostsByTitle(
"High-Performance Java Persistence"
);
Podemos ver que PostgreSQL registra los siguientes mensajes:
[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
Las declaraciones de registro que usan high_performance_java_persistence
El prefijo se ejecutó en el nodo principal mientras que los que usaban high_performance_java_persistence_replica
en el nodo Réplica.
Entonces, ¡todo funciona a la perfección!
Todo el código fuente se puede encontrar en mi repositorio GitHub de persistencia de Java de alto rendimiento, por lo que también puede probarlo.
Conclusión
Debe asegurarse de establecer el tamaño correcto para sus grupos de conexiones porque eso puede marcar una gran diferencia. Para esto, recomiendo usar Flexy Pool.
Debe ser muy diligente y asegurarse de marcar todas las transacciones de solo lectura en consecuencia. Es inusual que solo el 10% de sus transacciones sean de solo lectura. ¿Podría ser que tiene una aplicación de escritura mayoritaria o que está utilizando transacciones de escritura en las que solo emite instrucciones de consulta?
Para el procesamiento por lotes, definitivamente necesita transacciones de lectura y escritura, así que asegúrese de habilitar el procesamiento por lotes de JDBC, como este:
<property name="hibernate.order_updates" value="true"/>
<property name="hibernate.order_inserts" value="true"/>
<property name="hibernate.jdbc.batch_size" value="25"/>
Para el procesamiento por lotes, también puede usar un DataSource
separado que utiliza un grupo de conexiones diferente que se conecta al nodo principal.
Solo asegúrese de que el tamaño total de su conexión de todos los grupos de conexiones sea menor que la cantidad de conexiones con las que se configuró PostgreSQL.
Cada trabajo por lotes debe usar una transacción dedicada, así que asegúrese de usar un tamaño de lote razonable.
Además, desea mantener bloqueos y finalizar las transacciones lo más rápido posible. Si el procesador por lotes utiliza trabajadores de procesamiento simultáneo, asegúrese de que el tamaño del grupo de conexiones asociado sea igual a la cantidad de trabajadores, para que no esperen a que otros liberen las conexiones.