PostgreSQL
 sql >> база данни >  >> RDS >> PostgreSQL

Как да разделяте транзакции само за четене и за четене-запис с JPA и Hibernate

Пролетно маршрутизиране на транзакции

Първо, ще създадем DataSourceType Java Enum, който дефинира нашите опции за маршрутизиране на транзакции:

public enum  DataSourceType {
    READ_WRITE,
    READ_ONLY
}

За да насочим транзакциите за четене-запис към основния възел и транзакциите само за четене към възела Replica, можем да дефинираме ReadWriteDataSource който се свързва с първичния възел и ReadOnlyDataSource които се свързват с възела Replica.

Маршрутизирането на транзакциите за четене-запис и само за четене се извършва от Spring AbstractRoutingDataSource абстракция, която се реализира от TransactionRoutingDatasource , както е илюстрирано от следната диаграма:

TransactionRoutingDataSource е много лесен за изпълнение и изглежда по следния начин:

public class TransactionRoutingDataSource 
        extends AbstractRoutingDataSource {

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

По принцип проверяваме Spring TransactionSynchronizationManager клас, който съхранява текущия транзакционен контекст, за да провери дали текущо изпълняваната Spring транзакция е само за четене или не.

determineCurrentLookupKey методът връща стойността на дискриминатора, която ще се използва за избор на четене-запис или само за четене JDBC DataSource .

Пролетна конфигурация на JDBC DataSource за четене-запис и само за четене

DataSource конфигурацията изглежда по следния начин:

@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 ресурсния файл предоставя конфигурацията за JDBC DataSource за четене-запис и само за четене компоненти:

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 свойството дефинира URL адреса на основния възел, докато jdbc.url.replica дефинира URL адреса на възела Replica.

readWriteDataSource Компонентът Spring дефинира JDBC DataSource за четене и запис докато readOnlyDataSource компонент дефинира JDBC DataSource само за четене .

Имайте предвид, че както източниците на данни за четене-запис, така и само за четене използват HikariCP за обединяване на връзки.

actualDataSource действа като фасада за източниците на данни за четене-запис и само за четене и се реализира с помощта на TransactionRoutingDataSource полезност.

readWriteDataSource се регистрира с помощта на DataSourceType.READ_WRITE ключ и readOnlyDataSource използвайки DataSourceType.READ_ONLY ключ.

Така че, когато изпълнявате четене-запис @Transactional метод, readWriteDataSource ще се използва, докато се изпълнява @Transactional(readOnly = true) метод, readOnlyDataSource вместо това ще се използва.

Имайте предвид, че additionalProperties методът дефинира hibernate.connection.provider_disables_autocommit Свойство Hibernate, което добавих към Hibernate, за да отложа придобиването на база данни за RESOURCE_LOCAL JPA транзакции.

Не само, че hibernate.connection.provider_disables_autocommit ви позволява да използвате по-добре връзките към базата данни, но това е единственият начин, по който можем да накараме този пример да работи, тъй като без тази конфигурация връзката се придобива преди извикване на determineCurrentLookupKey метод TransactionRoutingDataSource .

Останалите компоненти на Spring, необходими за изграждане на JPA EntityManagerFactory се дефинират от AbstractJPAConfiguration базов клас.

По принцип actualDataSource допълнително се обвива от DataSource-Proxy и се предоставя на JPA EntityManagerFactory . Можете да проверите изходния код на GitHub за повече подробности.

Време за тестване

За да проверим дали маршрутизирането на транзакциите работи, ще активираме дневника на заявките на PostgreSQL, като зададем следните свойства в postgresql.conf конфигурационен файл:

log_min_duration_statement = 0
log_line_prefix = '[%d] '

log_min_duration_statement настройката на свойството е за регистриране на всички PostgreSQL изрази, докато вторият добавя името на базата данни към SQL дневника.

И така, при извикване на newPost и findAllPostsByTitle методи, като този:

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

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

Можем да видим, че PostgreSQL регистрира следните съобщения:

[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

Регистрационните оператори, използващи high_performance_java_persistence префиксът се изпълняваше на първичния възел, докато тези, използващи high_performance_java_persistence_replica на възела Replica.

Така че всичко работи като чар!

Целият изходен код може да бъде намерен в моето високопроизводително хранилище на Java Persistence GitHub, така че можете да го изпробвате и вие.

Заключение

Трябва да сте сигурни, че сте задали правилния размер за вашите пулове за връзки, защото това може да направи огромна разлика. За това препоръчвам да използвате Flexy Pool.

Трябва да сте много усърдни и да се уверите, че маркирате съответно всички транзакции само за четене. Необичайно е, че само 10% от вашите транзакции са само за четене. Възможно ли е да имате такова приложение за най-много записване или да използвате транзакции за запис, при които издавате само изрази за заявка?

За пакетна обработка определено се нуждаете от транзакции за четене и запис, така че не забравяйте да активирате пакетирането на JDBC, като това:

<property name="hibernate.order_updates" value="true"/>
<property name="hibernate.order_inserts" value="true"/>
<property name="hibernate.jdbc.batch_size" value="25"/>

За пакетиране можете също да използвате отделен DataSource който използва различен пул за връзки, който се свързва с основния възел.

Просто се уверете, че общият ви размер на връзката на всички пулове за връзки е по-малък от броя на връзките, с които е конфигуриран PostgreSQL.

Всяко групово задание трябва да използва специална транзакция, така че се уверете, че използвате разумен размер на партидата.

Нещо повече, искате да задържите заключванията и да завършите транзакциите възможно най-бързо. Ако пакетният процесор използва едновременно обработващи работници, уверете се, че размерът на асоциирания пул за връзки е равен на броя работници, така че те да не чакат другите да освободят връзките.



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. PostgreSQL не използва частичен индекс

  2. Как да потърся json колона за празни обекти?

  3. Как да добавите PostgreSQL драйвер като зависимост в Maven?

  4. Комбинации от заявки с вложен масив от записи в тип данни JSON

  5. Мога ли да върна обратно транзакция, която вече съм ангажирал? (загуба на данни)