Рубрики
Без рубрики

Маршрутизация транзакций для чтения и записи и только для чтения с помощью Spring

Узнайте, как можно реализовать механизм маршрутизации транзакций только для чтения и записи с помощью утилиты Spring framework AbstractRoutingDataSource.

Автор оригинала: Vlad Mihalcea.

Вступление

В этой статье я собираюсь объяснить, как вы можете реализовать механизм маршрутизации транзакций только для чтения и записи с использованием платформы Spring.

Это требование очень полезно, поскольку архитектура репликации базы данных с одним источником не только обеспечивает отказоустойчивость и лучшую доступность, но и позволяет нам масштабировать операции чтения, добавляя больше узлов-реплик.

Весна @Транзакционная аннотация

В приложении Spring веб – @Контроллер вызывает @Сервис метод, который аннотируется с помощью @Транзакционной аннотации.

По умолчанию транзакции Spring доступны для чтения и записи, но вы можете явно настроить их выполнение в контексте, доступном только для чтения, с помощью атрибута только для чтения аннотации @Transactional .

Например, следующий компонент ForumServiceImpl определяет два метода обслуживания:

  • новая запись , для которой требуется транзакция чтения-записи, которая должна выполняться на основном узле базы данных, и
  • найдите все записи по названию , для чего требуется транзакция только для чтения, которая может быть выполнена на узле-реплике базы данных, что снижает нагрузку на основной узел
@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 findAllPostsByTitle(String title) {
        return entityManager.createQuery("""
            select p
            from Post p
            where p.title = :title
            """, Post.class)
        .setParameter("title", title)
        .getResultList();
    }
}

Поскольку атрибут Только для чтения аннотации @Transactional по умолчанию имеет значение false , метод new Post использует транзакционный контекст для чтения и записи.

Рекомендуется определять аннотацию @Transactional(только для чтения) на уровне класса и переопределять ее только для методов чтения и записи. Таким образом, мы можем убедиться, что методы, доступные только для чтения, выполняются по умолчанию на узлах-репликах. И если мы забудем добавить аннотацию @Transactional для метода чтения-записи, мы получим исключение, поскольку транзакции чтения-записи могут выполняться только на Основном узле.

Поэтому гораздо лучший @Service класс будет выглядеть следующим образом:

@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 findAllPostsByTitle(String title) {
        return entityManager.createQuery("""
            select p
            from Post p
            where p.title = :title
            """, Post.class)
        .setParameter("title", title)
        .getResultList();
    }
}

Обратите внимание, что найти все записи по названию больше не нужно определять @Транзакционную(только для чтения) аннотацию, поскольку она унаследована от аннотации уровня класса.

Весенняя маршрутизация транзакций

Чтобы направить транзакции чтения-записи на Основной узел и транзакции только для чтения на узел реплики, мы можем определить ReadWriteDataSource , который подключается к Основному узлу, и ReadOnlyDataSource , которые подключаются к узлу реплики.

Маршрутизация транзакций только для чтения и записи выполняется с помощью абстракции Spring AbstractRoutingDataSource , которая реализуется с помощью 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 источника данных для записи или только для чтения .

Тип Источника данных – это просто базовое перечисление Java, которое определяет параметры маршрутизации транзакций:

public enum  DataSourceType {
    READ_WRITE,
    READ_ONLY
}

Конфигурация источника данных JDBC Spring для чтения и записи и только для чтения

Конфигурация Источника данных выглядит следующим образом:

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

Файл /META-INF/jdbc-postgresql-replication.properties resource содержит конфигурацию для компонентов JDBC Источник данных только для чтения и записи.:

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-адрес узла-реплики.

Компонент Источник данных для записи на чтение Spring определяет JDBC для записи на чтение Источник данных , в то время как компонент источник данных только для чтения определяет JDBC только для чтения Источник данных .

Обратите внимание, что как источники данных для чтения и записи, так и источники данных только для чтения используют HikariCP для объединения соединений. Для получения более подробной информации о преимуществах использования пула подключений к базе данных ознакомьтесь с этой статьей .

Фактический источник данных действует как фасад для источников данных только для чтения и записи и реализуется с помощью утилиты TransactionRoutingDataSource .

Источник данных для чтения и записи регистрируется с использованием Типа источника данных.READ_WRITE ключа и источника данных только для чтения с использованием Типа источника данных.ТОЛЬКО для ЧТЕНИЯ ключа.

Таким образом, при выполнении метода чтения-записи @Transactional будет использоваться источник данных для записи , в то время как при выполнении метода @Transactional(только для чтения) вместо него будет использоваться readOnlyDataSource .

Обратите внимание, что метод дополнительные свойства определяет соединение hibernate..provider_disables_автокомиссия Свойство Hibernate, которое я добавил в Hibernate, чтобы отложить получение базы данных для транзакций JPA RESOURCE_LOCAL.

Мало того, что соединение hibernate.connection.provider_disables_autocommit позволяет вам лучше использовать подключения к базе данных, но это единственный способ, которым мы можем заставить этот пример работать, поскольку без этой конфигурации соединение устанавливается до вызова determineCurrentLookupKey метод TransactionRoutingDataSource .

Для получения более подробной информации о подключении hibernate.provider_disables_autocommit конфигурация, проверьте эту статью .

Остальные компоненты Spring, необходимые для создания JPA EntityManagerFactory , определяются базовым классом AbstractJPAConfiguration .

В принципе, фактический источник данных дополнительно упаковывается DataSourceProxy и предоставляется JPA EntityManagerFactory . Вы можете проверить исходный код на GitHub для получения более подробной информации.

Время тестирования

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

log_min_duration_statement = 0
log_line_prefix = '[%d] '

Установив значение свойства log_min_duration_statement равным 0 , мы говорим PostgreSQL регистрировать все операторы.

Значение свойства log_line_prefix указывает PostgreSQL включать каталог базы данных при регистрации данной инструкции SQL.

Итак, при вызове нового сообщения и найдите все сообщения По названию методами, такими как это:

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

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

Мы видим, что PostgreSQL регистрирует следующие сообщения:

[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

Операторы журнала, использующие префикс high_performance_java_persistence , были выполнены на Основном узле, в то время как операторы, использующие префикс high_performance_java_persistence_replica , на узле-реплике.

Итак, все работает как по волшебству!

Весь исходный код можно найти в моем Высокопроизводительном репозитории Java Persistence GitHub, так что вы тоже можете его опробовать.

Вывод

Утилита AbstractRoutingDataSource Spring очень полезна при реализации механизма маршрутизации транзакций только для чтения и записи.

Используя этот шаблон маршрутизации, вы можете перенаправлять трафик только для чтения на узлы-реплики, чтобы Основной узел мог лучше обрабатывать транзакции чтения-записи.