Транзакции в Spring: сила управления данными
Привет, Хабр! Сегодня разберемся с транзакциями в Spring так, чтобы всё стало ясно и понятно: зачем они нужны, как работают и как их настроить так, чтобы данные были под контролем.
Начнем с самого начала. Транзакция — это единица работы, которая должна быть выполнена полностью или не выполнена вовсе. Представьте банковскую операцию: перевод денег с одного счета на другой. Если деньги списаны с первого счета, но не зачислены на второй, у нас проблемы. Именно для таких ситуаций нужны транзакции.
В Spring управление транзакциями стало простым и интуитивно понятным благодаря хорошим инструментам и абстракциям. Рассмотрим, как это всё работает.
Настройка проекта
Начнем с базовой настройки проекта. Предположим, есть Spring Boot приложение с использованием JPA и базы данных PostgreSQL. В pom.xml
будут такие зависимости:
org.springframework.boot
spring-boot-starter-data-jpa
org.postgresql
postgresql
42.5.0
org.springframework.boot
spring-boot-starter-web
org.projectlombok
lombok
true
Не забываем настроить 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. Также обсудим, почему не стоит локализовывать исключения, и проанализируем исходный код для лучшего понимания процессов. Записаться можно по ссылке.