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

896d9bfb6cbaf95c1d1050422f2fd92d.png

Привет, Хабр! Сегодня разберемся с транзакциями в 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 позволяет управлять этими аспектами через уровни изоляции.

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

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

© Habrahabr.ru