Автор оригинала: 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 ListfindAllPostsByTitle(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 ListfindAllPostsByTitle(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
Файл /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" ); Listposts = 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 очень полезна при реализации механизма маршрутизации транзакций только для чтения и записи.
Используя этот шаблон маршрутизации, вы можете перенаправлять трафик только для чтения на узлы-реплики, чтобы Основной узел мог лучше обрабатывать транзакции чтения-записи.