PostgreSQL
 sql >> Baza danych >  >> RDS >> PostgreSQL

Jak podzielić transakcje tylko do odczytu i do odczytu i zapisu za pomocą JPA i Hibernate

Routing transakcji wiosennych

Najpierw utworzymy DataSourceType Java Enum, który definiuje nasze opcje routingu transakcji:

public enum  DataSourceType {
    READ_WRITE,
    READ_ONLY
}

Aby skierować transakcje odczytu i zapisu do węzła podstawowego i transakcje tylko do odczytu do węzła repliki, możemy zdefiniować ReadWriteDataSource który łączy się z węzłem podstawowym i ReadOnlyDataSource które łączą się z węzłem repliki.

Trasowanie transakcji do odczytu i zapisu oraz tylko do odczytu jest realizowane przez Spring AbstractRoutingDataSource abstrakcji, która jest implementowana przez TransactionRoutingDatasource , jak ilustruje poniższy diagram:

TransactionRoutingDataSource jest bardzo łatwy do wdrożenia i wygląda następująco:

public class TransactionRoutingDataSource 
        extends AbstractRoutingDataSource {

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

Zasadniczo sprawdzamy Spring TransactionSynchronizationManager klasa, która przechowuje bieżący kontekst transakcyjny, aby sprawdzić, czy aktualnie uruchomiona transakcja Spring jest tylko do odczytu, czy nie.

determineCurrentLookupKey metoda zwraca wartość dyskryminatora, która zostanie użyta do wyboru JDBC do odczytu-zapisu lub tylko do odczytu DataSource .

Wiosenna konfiguracja JDBC DataSource do odczytu i zapisu oraz tylko do odczytu

DataSource konfiguracja wygląda następująco:

@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 plik zasobów zapewnia konfigurację dla JDBC do odczytu i zapisu oraz tylko do odczytu DataSource komponenty:

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 właściwość definiuje adres URL węzła podstawowego, podczas gdy jdbc.url.replica definiuje adres URL węzła repliki.

readWriteDataSource Komponent Spring definiuje JDBC do odczytu i zapisu DataSource podczas gdy readOnlyDataSource komponent definiuje JDBC tylko do odczytu DataSource .

Zwróć uwagę, że zarówno źródła danych do odczytu i zapisu, jak i tylko do odczytu używają HikariCP do łączenia połączeń.

actualDataSource działa jako fasada dla źródeł danych do odczytu i zapisu oraz tylko do odczytu i jest zaimplementowany przy użyciu TransactionRoutingDataSource narzędzie.

readWriteDataSource jest zarejestrowany przy użyciu DataSourceType.READ_WRITE klucz i readOnlyDataSource za pomocą DataSourceType.READ_ONLY klawisz.

Tak więc, podczas wykonywania odczytu i zapisu @Transactional metoda, readWriteDataSource będzie używany podczas wykonywania @Transactional(readOnly = true) metoda, readOnlyDataSource zostanie użyty zamiast tego.

Zauważ, że additionalProperties metoda definiuje hibernate.connection.provider_disables_autocommit Właściwość Hibernate, którą dodałem do Hibernate, aby odroczyć przejęcie bazy danych dla transakcji RESOURCE_LOCAL JPA.

Nie tylko, że hibernate.connection.provider_disables_autocommit pozwala lepiej wykorzystać połączenia z bazą danych, ale jest to jedyny sposób, w jaki możemy sprawić, by ten przykład działał, ponieważ bez tej konfiguracji połączenie jest nawiązywane przed wywołaniem determineCurrentLookupKey metoda TransactionRoutingDataSource .

Pozostałe komponenty Spring potrzebne do zbudowania JPA EntityManagerFactory są zdefiniowane przez AbstractJPAConfiguration klasa podstawowa.

Zasadniczo actualDataSource jest dodatkowo opakowany przez DataSource-Proxy i dostarczony do JPA EntityManagerFactory . Możesz sprawdzić kod źródłowy na GitHub, aby uzyskać więcej informacji.

Czas testowania

Aby sprawdzić, czy routing transakcji działa, włączymy dziennik zapytań PostgreSQL, ustawiając następujące właściwości w pliku postgresql.conf plik konfiguracyjny:

log_min_duration_statement = 0
log_line_prefix = '[%d] '

log_min_duration_statement ustawienie właściwości służy do rejestrowania wszystkich instrukcji PostgreSQL, podczas gdy druga dodaje nazwę bazy danych do dziennika SQL.

Tak więc, podczas wywoływania newPost i findAllPostsByTitle metody, takie jak:

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

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

Widzimy, że PostgreSQL rejestruje następujące komunikaty:

[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

Instrukcje dziennika przy użyciu high_performance_java_persistence prefiks został wykonany na węźle podstawowym, podczas gdy te używające high_performance_java_persistence_replica w węźle repliki.

Tak więc wszystko działa jak czar!

Cały kod źródłowy można znaleźć w moim repozytorium High-Performance Java Persistence GitHub, więc możesz go również wypróbować.

Wniosek

Musisz upewnić się, że ustawiłeś odpowiedni rozmiar dla swoich pul połączeń, ponieważ może to mieć ogromne znaczenie. W tym celu polecam korzystanie z Flexy Pool.

Musisz być bardzo sumienny i upewnić się, że wszystkie transakcje tylko do odczytu zostały odpowiednio oznaczone. To niezwykłe, że tylko 10% Twoich transakcji jest tylko do odczytu. Czy to możliwe, że masz taką aplikację, która najczęściej zapisuje lub używasz transakcji zapisu, w których wydajesz tylko zapytania?

Do przetwarzania wsadowego zdecydowanie potrzebujesz transakcji odczytu i zapisu, więc upewnij się, że włączyłeś przetwarzanie wsadowe JDBC, tak jak poniżej:

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

Do grupowania możesz również użyć oddzielnego DataSource który używa innej puli połączeń, która łączy się z węzłem podstawowym.

Upewnij się tylko, że całkowity rozmiar połączenia wszystkich pul połączeń jest mniejszy niż liczba połączeń, z którymi skonfigurowano PostgreSQL.

Każde zadanie wsadowe musi korzystać z dedykowanej transakcji, więc upewnij się, że używasz rozsądnego rozmiaru partii.

Co więcej, chcesz trzymać blokady i jak najszybciej kończyć transakcje. Jeśli procesor wsadowy używa współbieżnych procesów roboczych, upewnij się, że wielkość powiązanej puli połączeń jest równa liczbie procesów roboczych, aby nie czekali, aż inni zwolnią połączenia.



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Udostępnij połączenie do postgres db między procesami w Pythonie

  2. PostgreSQL, jak sprawdzić, które zapytania zostały uruchomione

  3. Najlepszy sposób na sprawdzenie pustej lub zerowej wartości

  4. Nowe funkcje zgodności z Oracle w PostgresPlus Advanced Server 9.3Beta

  5. 2. kwadrant na PostgresConf US 2018