Saltar al contenido

Cómo dividir transacciones de solo lectura y lectura-escritura con JPA e Hibernate

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:

Enrutamiento de transacciones de lectura-escritura y solo lectura con Spring

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

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 el hibernate.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 al determineCurrentLookupKey método TransactionRoutingDataSource.

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.

Calificaciones y comentarios

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *