Java >> Java tutorial >  >> Tag >> Spring

Læse-skrive og skrivebeskyttet transaktionsrouting med Spring

Introduktion

I denne artikel vil jeg forklare, hvordan du kan implementere en læse-skrive- og skrivebeskyttet transaktionsroutingmekanisme ved hjælp af Spring-rammen.

Dette krav er meget nyttigt, da arkitekturen for enkelt-primær databasereplikering ikke kun giver fejltolerance og bedre tilgængelighed, men den giver os mulighed for at skalere læseoperationer ved at tilføje flere replika-noder.

Forår @Transactional annotation

I en Spring-applikation er nettet @Controller kalder en @Service metode, som er kommenteret ved hjælp af @Transactional anmærkning.

Som standard er Spring-transaktioner læse-skrive, men du kan eksplicit konfigurere dem til at blive udført i en skrivebeskyttet kontekst via read-only attribut for @Transactional anmærkning.

For eksempel følgende ForumServiceImpl komponent definerer to servicemetoder:

  • newPost , som kræver en læse-skrive-transaktion, der skal udføres på databasens Primære node, og
  • findAllPostsByTitle , som kræver en skrivebeskyttet transaktion, der kan udføres på en database Replica node, hvilket reducerer belastningen på den primære node
@Service
public class ForumServiceImpl 
        implements ForumService {

    @PersistenceContext
    private EntityManager entityManager;

    @Override
    @Transactional
    public Post newPost(String title, String... tags) {
        Post post = new Post();
        post.setTitle(title);

        post.getTags().addAll(
            entityManager.createQuery("""
                select t
                from Tag t
                where t.name in :tags
                """, Tag.class)
            .setParameter("tags", Arrays.asList(tags))
            .getResultList()
        );

        entityManager.persist(post);

        return post;
    }

    @Override
    @Transactional(readOnly = true)
    public List<Post> findAllPostsByTitle(String title) {
        return entityManager.createQuery("""
            select p
            from Post p
            where p.title = :title
            """, Post.class)
        .setParameter("title", title)
        .getResultList();
    }
}

Siden readOnly attributten for @Transactional annotation er indstillet til false som standard er newPost metoden bruger en læs-skriv transaktionskontekst.

Det er god praksis at definere @Transactional(readOnly = true) annotering på klasseniveau og kun tilsidesætte den for læse-skrive-metoder. På denne måde kan vi sikre, at skrivebeskyttede metoder udføres som standard på replika-knuderne. Og hvis vi glemmer at tilføje @Transactional annotering på en læse-skrive-metode, vil vi få en undtagelse, da læs-skrive-transaktioner kun kan udføres på den primære node.

Derfor en meget bedre @Service klasse ser således ud:

@Service
@Transactional(readOnly = true)
public class ForumServiceImpl 
        implements ForumService {

    @PersistenceContext
    private EntityManager entityManager;

    @Override
    @Transactional
    public Post newPost(String title, String... tags) {
        Post post = new Post();
        post.setTitle(title);

        post.getTags().addAll(
            entityManager.createQuery("""
                select t
                from Tag t
                where t.name in :tags
                """, Tag.class)
            .setParameter("tags", Arrays.asList(tags))
            .getResultList()
        );

        entityManager.persist(post);

        return post;
    }

    @Override
    public List<Post> findAllPostsByTitle(String title) {
        return entityManager.createQuery("""
            select p
            from Post p
            where p.title = :title
            """, Post.class)
        .setParameter("title", title)
        .getResultList();
    }
}

Bemærk, at findAllPostsByTitle behøver ikke længere at definere @Transactional(readOnly = true) annotering, da den er nedarvet fra annoteringen på klasseniveau.

Forårstransaktionsruting

For at dirigere læse-skrive-transaktionerne til den primære node og skrivebeskyttede transaktioner til replika-noden, kan vi definere en ReadWriteDataSource der forbinder til den primære node og en ReadOnlyDataSource der forbinder til replika-noden.

Læse-skrive- og skrivebeskyttet transaktionsruting udføres af Spring AbstractRoutingDataSource abstraktion, som er implementeret af TransactionRoutingDatasource , som illustreret af følgende diagram:

TransactionRoutingDataSource er meget nem at implementere og ser ud som følger:

public class TransactionRoutingDataSource 
        extends AbstractRoutingDataSource {

    @Nullable
    @Override
    protected Object determineCurrentLookupKey() {
        return TransactionSynchronizationManager
            .isCurrentTransactionReadOnly() ?
                DataSourceType.READ_ONLY :
                DataSourceType.READ_WRITE;
    }
}

Grundlæggende inspicerer vi fjederen TransactionSynchronizationManager klasse, der gemmer den aktuelle transaktionskontekst for at kontrollere, om den aktuelt kørende Spring-transaktion er skrivebeskyttet eller ej.

determineCurrentLookupKey metoden returnerer diskriminatorværdien, der vil blive brugt til at vælge enten read-write eller read-only JDBC DataSource .

DataSourceType er blot en grundlæggende Java Enum, der definerer vores muligheder for transaktionsruting:

public enum  DataSourceType {
    READ_WRITE,
    READ_ONLY
}

Forår læse-skrive og skrivebeskyttet JDBC DataSource-konfiguration

DataSource konfigurationen ser ud som følger:

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

/META-INF/jdbc-postgresql-replication.properties ressourcefil giver konfigurationen for læse-skrive- og skrivebeskyttet JDBC DataSource komponenter:

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 egenskaben definerer URL'en for den primære node, mens jdbc.url.replica definerer URL'en for replika-noden.

readWriteDataSource Fjederkomponent definerer læse-skrive JDBC DataSource mens readOnlyDataSource komponent definerer den skrivebeskyttede JDBC DataSource .

Bemærk, at både læse-skrive- og skrivebeskyttede datakilder bruger HikariCP til forbindelsespooling. For flere detaljer om fordelene ved at bruge databaseforbindelsespooling, se denne artikel.

actualDataSource fungerer som en facade for læse-skrive- og skrivebeskyttede datakilder og implementeres ved hjælp af TransactionRoutingDataSource værktøj.

readWriteDataSource er registreret ved hjælp af DataSourceType.READ_WRITE nøglen og readOnlyDataSource ved hjælp af DataSourceType.READ_ONLY nøgle.

Så når du udfører en læse-skrive @Transactional metode, readWriteDataSource vil blive brugt, mens der udføres en @Transactional(readOnly = true) metode, readOnlyDataSource vil blive brugt i stedet.

Bemærk, at additionalProperties metoden definerer hibernate.connection.provider_disables_autocommit Hibernate-egenskab, som jeg føjede til Hibernate for at udsætte databaseanskaffelsen for RESOURCE_LOCAL JPA-transaktioner.

Ikke kun det hibernate.connection.provider_disables_autocommit giver dig mulighed for at gøre bedre brug af databaseforbindelser, men det er den eneste måde, vi kan få dette eksempel til at fungere, da forbindelsen, uden denne konfiguration, opnås, før du kalder determineCurrentLookupKey metode TransactionRoutingDataSource .

For flere detaljer om hibernate.connection.provider_disables_autocommit konfiguration, tjek denne artikel.

De resterende fjederkomponenter, der er nødvendige for at bygge JPA EntityManagerFactory er defineret af AbstractJPAConfiguration basisklasse.

Grundlæggende er actualDataSource er yderligere pakket ind af DataSource-Proxy og leveret til JPA ENtityManagerFactory . Du kan tjekke kildekoden på GitHub for flere detaljer.

Testtid

For at kontrollere, om transaktionsroutingen virker, vil vi aktivere PostgreSQL-forespørgselsloggen ved at indstille følgende egenskaber i postgresql.conf konfigurationsfil:

log_min_duration_statement = 0
log_line_prefix = '[%d] '

Ved at indstille log_min_duration_statement egenskabsværdi til 0 , vi beder PostgreSQL om at logge alle udsagn.

log_line_prefix egenskabsværdi instruerer PostgreSQL om at inkludere databasekataloget, når en given SQL-sætning logges.

Så når du ringer til newPost og findAllPostsByTitle metoder som denne:

Post post = forumService.newPost(
    "High-Performance Java Persistence",
    "JDBC", "JPA", "Hibernate"
);

List<Post> posts = forumService.findAllPostsByTitle(
    "High-Performance Java Persistence"
);

Vi kan se, at PostgreSQL logger følgende meddelelser:

[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

Log-udsagn ved hjælp af high_performance_java_persistence præfiks blev udført på den primære node, mens dem, der brugte high_performance_java_persistence_replica på replika-noden.

Så alt fungerer som en charme!

Al kildekoden kan findes i mit High-Performance Java Persistence GitHub-lager, så du kan også prøve det.

Konklusion

AbstractRoutingDataSource Spring-værktøjet er meget nyttigt, når du implementerer en læse-skrive- og skrivebeskyttet transaktionsdirigeringsmekanisme.

Ved at bruge dette routingmønster kan du omdirigere den skrivebeskyttede trafik til replika-noder, så den primære node bedre kan håndtere læse-skrive-transaktionerne.


Java tag