Привет, Хабр! Сегодня разберемся с транзакциями в 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(() -> new RuntimeException("Исходный счет не найден")); Account toAccount = accountRepository.findById(toAccountId) .orElseThrow(() -> 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(() -> new RuntimeException("Исходный счет не найден")); Account toAccount = accountRepository.findById(toAccountId) .orElseThrow(() -> 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 позволяет управлять этими аспектами через уровни изоляции.
Уровни изоляции
-
READ_UNCOMMITTED: позволяет читать незавершенные изменения других транзакций (грязное чтение).
-
READ_COMMITTED: гарантирует, что будут прочитаны только завершенные изменения.
-
REPEATABLE_READ: гарантирует, что повторные чтения внутри транзакции дадут тот же результат.
-
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 -> { 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/
Добавить комментарий