Por fin luego de mucho trabajar ya dimos con la respuesta de esta traba que algunos usuarios de nuestro espacio han tenido. Si tienes algún dato que compartir no dejes de compartir tu comentario.
Solución:
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 un ReadOnlyDataSource
que se conectan al nodo Replica.
El enrutamiento de transacciones de lectura-escritura y solo lectura lo realiza Spring AbstractRoutingDataSource
abstracción, que es implementada por el TransactionRoutingDatasource
, como se ilustra en el siguiente diagrama:
los 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 la primavera 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.
los determineCurrentLookupKey
El método devuelve el valor del discriminador que se utilizará para elegir el JDBC de lectura-escritura o de solo lectura. DataSource
.
Configuración de Spring Read-Write y JDBC DataSource de solo lectura
los DataSource
la configuración tiene el siguiente aspecto:
@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
los /META-INF/jdbc-postgresql-replication.properties
El archivo de recursos proporciona la configuración para JDBC de lectura-escritura y solo lectura. DataSource
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
los jdbc.url.primary
La propiedad define la URL del nodo principal mientras que jdbc.url.replica
define la URL del nodo réplica.
los readWriteDataSource
El componente Spring define el JDBC de lectura y escritura. DataSource
mientras que la readOnlyDataSource
componente define el JDBC de solo lectura DataSource
.
Tenga en cuenta que tanto las fuentes de datos de lectura-escritura como las de solo lectura utilizan HikariCP para la agrupación de conexiones.
los actualDataSource
actúa como una fachada para las fuentes de datos de lectura-escritura y solo lectura y se implementa utilizando el TransactionRoutingDataSource
utilidad.
los readWriteDataSource
está registrado usando el DataSourceType.READ_WRITE
clave y la readOnlyDataSource
utilizando el DataSourceType.READ_ONLY
llave.
Entonces, al ejecutar una lectura-escritura @Transactional
método, el readWriteDataSource
se utilizará mientras se ejecuta un @Transactional(readOnly = true)
método, el readOnlyDataSource
se utilizará en su lugar.
Tenga en cuenta que el
additionalProperties
El método define elhibernate.connection.provider_disables_autocommit
Propiedad de Hibernate, que agregué a Hibernate para posponer la adquisición de la base de datos para las 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 aldetermineCurrentLookupKey
métodoTransactionRoutingDataSource
.
Los componentes restantes de Spring necesarios para construir el JPA EntityManagerFactory
están definidos por el AbstractJPAConfiguration
clase base.
Básicamente, el actualDataSource
está más envuelto por DataSource-Proxy y se proporciona a la 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 el postgresql.conf
archivo de configuración:
log_min_duration_statement = 0
log_line_prefix = '[%d] '
los 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 SQL.
Entonces, al llamar al newPost
y findAllPostsByTitle
métodos, como este:
Post post = forumService.newPost(
"High-Performance Java Persistence",
"JDBC", "JPA", "Hibernate"
);
List posts = forumService.findAllPostsByTitle(
"High-Performance Java Persistence"
);
Podemos ver que PostgreSQL registra los siguientes mensajes:
[high_performance_java_persistence] LOG: execute :
BEGIN
[high_performance_java_persistence] DETAIL:
parameters: $1 = 'JDBC', $2 = 'JPA', $3 = 'Hibernate'
[high_performance_java_persistence] LOG: execute :
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 :
select nextval ('hibernate_sequence')
[high_performance_java_persistence] DETAIL:
parameters: $1 = 'High-Performance Java Persistence', $2 = '4'
[high_performance_java_persistence] LOG: execute :
insert into post (title, id) values ($1, $2)
[high_performance_java_persistence] DETAIL:
parameters: $1 = '4', $2 = '1'
[high_performance_java_persistence] LOG: execute :
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 :
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 :
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 :
BEGIN
[high_performance_java_persistence_replica] DETAIL:
parameters: $1 = 'High-Performance Java Persistence'
[high_performance_java_persistence_replica] LOG: execute :
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 utilizan high_performance_java_persistence
prefix se ejecutaron en el nodo primario mientras que los que usaban el high_performance_java_persistence_replica
en el nodo de réplica.
Entonces, ¡todo funciona a las mil maravillas!
Todo el código fuente se puede encontrar en mi repositorio de 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 tenga una aplicación de escritura de este tipo o esté utilizando transacciones de escritura en las que solo emite declaraciones de consulta?
Para el procesamiento por lotes, definitivamente necesita transacciones de lectura y escritura, así que asegúrese de habilitar el procesamiento por lotes JDBC, así:
Para el procesamiento por lotes también puede utilizar un DataSource
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 ha configurado PostgreSQL.
Cada trabajo por lotes debe utilizar una transacción dedicada, así que asegúrese de utilizar 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 al número de trabajadores, para que no esperen a que otros liberen las conexiones.
Está diciendo que las URL de su aplicación son solo un 10% de solo lectura, por lo que el otro 90% tiene al menos alguna forma de escritura en la base de datos.
10% LEER
Puede pensar en utilizar un diseño CQRS que pueda mejorar el rendimiento de lectura de su base de datos. Sin duda, puede leer desde la base de datos secundaria y posiblemente hacerse más eficiente diseñando las consultas y los modelos de dominio específicamente para la capa de lectura / visualización.
No ha dicho si las solicitudes del 10% son caras o no (por ejemplo, ejecutar informes)
Preferiría usar una sessionFactory separada si tuviera que seguir el diseño de CQRS, ya que los objetos que se cargan / almacenan en caché probablemente serán diferentes a los que se escriben.
90% ESCRIBIR
En lo que respecta al otro 90%, no querrá leer desde la base de datos secundaria (mientras escribe en la primaria) durante alguna lógica de escritura, ya que no querrá que se involucren datos potencialmente obsoletos.
Es probable que algunas de estas lecturas estén mirando hacia arriba “static”datos. Si el almacenamiento en caché de Hibernate no reduce los accesos a la base de datos para las lecturas, consideraría una caché en memoria como Memcached o Redis para este tipo de datos. Esta misma caché podría ser utilizada por procesos de lectura al 10% y escritura al 90%.
Para lecturas que no son static (es decir, leer datos que ha escrito recientemente) Hibernate debería contener datos en su caché de objetos si tiene el tamaño adecuado. ¿Puede determinar el rendimiento de aciertos / errores de caché?
CUARZO
Si sabe con certeza que un trabajo programado no afectará el mismo conjunto de datos que otro trabajo, puede ejecutarlos en diferentes bases de datos; sin embargo, en caso de duda, siempre realice actualizaciones por lotes en un servidor (primario) y replique los cambios. Es mejor ser lógicamente correcto que introducir problemas de replicación.
PARTICIONAMIENTO DB
Si sus 1000 solicitudes por segundo están escribiendo una gran cantidad de datos, observe cómo particionar su base de datos. Es posible que descubra que tiene tablas cada vez mayores. El particionamiento es una forma de solucionar este problema sin tener que archivar datos.
A veces, necesita poco o ningún cambio en el código de su aplicación.
El archivo es, obviamente, otra opción
Descargo de responsabilidad: cualquier pregunta como esta siempre será específica de la aplicación. Intente siempre mantener su arquitectura lo más simple posible.