Транзакции в Spring: сила управления данными

от автора

Привет, Хабр! Сегодня разберемся с транзакциями в Spring так, чтобы всё стало ясно и понятно: зачем они нужны, как работают и как их настроить так, чтобы данные были под контролем.

Начнем с самого начала. Транзакция — это единица работы, которая должна быть выполнена полностью или не выполнена вовсе. Представьте банковскую операцию: перевод денег с одного счета на другой. Если деньги списаны с первого счета, но не зачислены на второй, у нас проблемы. Именно для таких ситуаций нужны транзакции.

В Spring управление транзакциями стало простым и интуитивно понятным благодаря хорошим инструментам и абстракциям. Рассмотрим, как это всё работает.

Настройка проекта

Начнем с базовой настройки проекта. Предположим, есть Spring Boot приложение с использованием JPA и базы данных PostgreSQL. В pom.xml будут такие зависимости:

<dependencies>     <!-- Spring Boot Starter Data JPA -->     <dependency>         <groupId>org.springframework.boot</groupId>         <artifactId>spring-boot-starter-data-jpa</artifactId>     </dependency>          <!-- PostgreSQL Driver -->     <dependency>         <groupId>org.postgresql</groupId>         <artifactId>postgresql</artifactId>         <version>42.5.0</version>     </dependency>          <!-- Spring Boot Starter Web -->     <dependency>         <groupId>org.springframework.boot</groupId>         <artifactId>spring-boot-starter-web</artifactId>     </dependency>          <!-- Lombok для удобства -->     <dependency>         <groupId>org.projectlombok</groupId>         <artifactId>lombok</artifactId>         <optional>true</optional>     </dependency> </dependencies>

Не забываем настроить application.properties для подключения к БД:

spring.datasource.url=jdbc:postgresql://localhost:5432/mydb spring.datasource.username=postgres spring.datasource.password=yourpassword spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true

Основы транзакций в Spring

Spring имеет два основных способа управления транзакциями: декларативный и программный. Сосредоточимся на декларативном подходе, который обычно более предпочтителен благодаря своей простоте (но про программный тоже не забудем).

Декларативное управление транзакциями

Декларативный подход позволяет определить границы транзакций с помощью аннотаций. Основная аннотация — @Transactional. Посмотрим, как это работает на примере.

Предположим, есть сервис для управления банковскими счетами:

@Service public class AccountService {      private final AccountRepository accountRepository;      public AccountService(AccountRepository accountRepository) {         this.accountRepository = accountRepository;     }      @Transactional     public void transfer(Long fromAccountId, Long toAccountId, BigDecimal amount) {         Account fromAccount = accountRepository.findById(fromAccountId)                 .orElseThrow(() -&gt; new RuntimeException("Исходный счет не найден"));         Account toAccount = accountRepository.findById(toAccountId)                 .orElseThrow(() -&gt; new RuntimeException("Целевой счет не найден"));                  fromAccount.debit(amount);         toAccount.credit(amount);                  accountRepository.save(fromAccount);         accountRepository.save(toAccount);     } }

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

А тепреь представим, что Spring создает прокси для нашего AccountService. Прокси перехватывает вызов метода transfer, открывает транзакцию, выполняет метод, и затем коммитит или откатывает транзакцию в зависимости от результата.

public class TransactionProxy implements InvocationHandler {      private final Object target;     private final PlatformTransactionManager transactionManager;      public TransactionProxy(Object target, PlatformTransactionManager transactionManager) {         this.target = target;         this.transactionManager = transactionManager;     }      @Override     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {         if (method.isAnnotationPresent(Transactional.class)) {             TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());             try {                 Object result = method.invoke(target, args);                 transactionManager.commit(status);                 return result;             } catch (Exception e) {                 transactionManager.rollback(status);                 throw e;             }         } else {             return method.invoke(target, args);         }     } }

Это, конечно, упрощенная версия, но она показывает суть: открытие транзакции, выполнение метода и коммит или откат транзакции.

Настройка транзакционного менеджера

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

Если вы используете Spring Boot и подключили spring-boot-starter-data-jpa, Spring автоматом настроит JpaTransactionManager. То есть не нужно ничего дополнительно конфигурировать:

@Configuration @EnableTransactionManagement public class TransactionConfig {     // Обычно здесь ничего не нужно }

Если вы хотите настроить транзакционный менеджер вручную, например, для использования нескольких источников данных, можно сделать это следующим образом:

@Configuration @EnableTransactionManagement public class TransactionConfig {      @Bean     public PlatformTransactionManager transactionManager(EntityManagerFactory emf) {         JpaTransactionManager tm = new JpaTransactionManager();         tm.setEntityManagerFactory(emf);         return tm;     } }

Примеры использования @Transactional

Рассмотримх примеров, чтобы понять, как применять @Transactional в разных сценариях.

Простая транзакция

Рассмотрим метод перевода средств между счетами, который мы уже писали выше. Это классика использования транзакции:

@Transactional public void transfer(Long fromAccountId, Long toAccountId, BigDecimal amount) {     Account fromAccount = accountRepository.findById(fromAccountId)             .orElseThrow(() -&gt; new RuntimeException("Исходный счет не найден"));     Account toAccount = accountRepository.findById(toAccountId)             .orElseThrow(() -&gt; new RuntimeException("Целевой счет не найден"));          fromAccount.debit(amount);     toAccount.credit(amount);          accountRepository.save(fromAccount);     accountRepository.save(toAccount); }

Если любой из этапов выполнения метода завершится с ошибкой, все изменения будут откатаны, и баланс счетов останется неизменным.

Транзакции с разными уровнями изоляции

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

@Transactional(isolation = Isolation.SERIALIZABLE) public void criticalOperation() {     //  важная операция }

Уровень изоляции SERIALIZABLE дает уверенность в том, что транзакция будет полностью изолирована от других, что предотвращает фантомные чтения и грязные записи.

Управление распространением транзакций

Иногда методы могут вызываться друг из друга, и нужно контролировать, как транзакции распространяются. Параметр propagation позволяет это делать.

@Transactional(propagation = Propagation.REQUIRED) public void methodA() {     // выполняется в существующей транзакции или создаёт новую }  @Transactional(propagation = Propagation.REQUIRES_NEW) public void methodB() {     // всегда создаёт новую транзакцию }

Если methodA вызывает methodB, то methodB будет выполняться в новой транзакции, независимо от того, существует ли уже транзакция.

Обработка исключений и откат транзакций

По дефолту, транзакция откатывается при возникновении непроверяемого исключения (наследника RuntimeException). Если нужно откатывать транзакцию и при проверяемых исключениях, можно использовать параметр rollbackFor.

@Transactional(rollbackFor = Exception.class) public void someMethod() throws Exception {     // ваш код }

Предположим, есть метод, который выполняет несколько операций, и нужно откатывать транзакцию даже при проверяемых исключениях:

@Transactional(rollbackFor = {IOException.class, SQLException.class}) public void performOperations() throws IOException, SQLException {     // операции, которые могут выбросить IOException или SQLException }

Теперь, если любой из этих исключений будет брошен, транзакция будет откатана.

Изоляция транзакций и проблемы конкурентного доступа

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

Уровни изоляции

  1. READ_UNCOMMITTED: позволяет читать незавершенные изменения других транзакций (грязное чтение).

  2. READ_COMMITTED: гарантирует, что будут прочитаны только завершенные изменения.

  3. REPEATABLE_READ: гарантирует, что повторные чтения внутри транзакции дадут тот же результат.

  4. SERIALIZABLE: самый высокий уровень изоляции, предотвращающий все виды аномалий, но может существенно снизить производительность.

Пример настройки уровня изоляции:

@Transactional(isolation = Isolation.REPEATABLE_READ) public void processOrder(Long orderId) {     // обработка заказа с уровнем изоляции REPEATABLE_READ } 

@Transactional на уровне класса

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

@Service @Transactional public class OrderService {          public void createOrder(Order order) {         // код создания заказа     }          public void updateOrder(Order order) {         // код обновления заказа     } }

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

Прочие нюансы

  • Не используйте @Transactional на уровне приватных методов: Spring использует прокси для управления транзакциями, поэтому приватные методы не будут проксироваться, и аннотация не будет работать.

  • Избегайте аннотирования методов, вызываемых из того же класса: если метод A вызывает метод B внутри того же класса, транзакции для метода B не будут применены, так как вызов происходит напрямую, без прокси.

  • Комбинируйте @Transactional с другими аннотациями Spring: Например, @Service или @Repository для лучшей организации кода.

  • Используйте правильные уровни изоляции: не злоупотребляйте SERIALIZABLE, если это не необходимо, так как это может негативно повлиять на производительность.

Программное управление транзакциями

Иногда декларативного подхода недостаточно, и нужно программное управление транзакциями. Spring имеет для этого TransactionTemplate и PlatformTransactionManager.

@Service public class PaymentService {      private final TransactionTemplate transactionTemplate;     private final PaymentRepository paymentRepository;      public PaymentService(PlatformTransactionManager transactionManager, PaymentRepository paymentRepository) {         this.transactionTemplate = new TransactionTemplate(transactionManager);         this.paymentRepository = paymentRepository;     }      public void processPayment(Payment payment) {         transactionTemplate.execute(status -&gt; {             paymentRepository.save(payment);             // дополнительные операции             return null;         });     } }

Управление транзакциями с несколькими источниками данных

Если приложение использует несколько баз данных, потребуется настроить несколько транзакционных менеджеров.

@Configuration @EnableTransactionManagement public class MultipleDataSourceConfig {      @Bean     @Primary     public DataSource dataSource1() {         // настройка первого источника данных     }      @Bean     public DataSource dataSource2() {         // настройка второго источника данных     }      @Bean     public PlatformTransactionManager transactionManager1(@Qualifier("dataSource1") DataSource ds) {         return new DataSourceTransactionManager(ds);     }      @Bean     public PlatformTransactionManager transactionManager2(@Qualifier("dataSource2") DataSource ds) {         return new DataSourceTransactionManager(ds);     } }

Затем можно использовать аннотацию @Transactional с указанием нужного менеджера транзакций:

@Transactional("transactionManager1") public void methodForDataSource1() {     // работа с первым источником данных }  @Transactional("transactionManager2") public void methodForDataSource2() {     // работа со вторым источником данных }

Заключение

Если у вас остались вопросы или вы хотите поделиться своими историями, пишите в комментариях! Пусть ваши транзакции всегда завершаются успешно!

25 ноября пройдет открытый урок «Интернационализация и локализация в приложениях Spring».

Мы рассмотрим работу с классом Locale, использование MessageSource в Spring Boot и без него, способы хранения и смены локали в веб-приложениях, а также локализацию в шаблонах Thymeleaf и сообщений Bean Validation. Также обсудим, почему не стоит локализовывать исключения, и проанализируем исходный код для лучшего понимания процессов. Записаться можно по ссылке.


ссылка на оригинал статьи https://habr.com/ru/articles/856842/