sql >> Base de Datos >  >> RDS >> PostgreSQL

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

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.