← Назад к вопросам

Как всегда открывать метод в новой транзакции

2.0 Middle🔥 111 комментариев
#Spring Framework

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как всегда открывать метод в новой транзакции

Проблема

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

@Service
public class PaymentService {
    @Transactional
    public void processPayment(String paymentId) {
        // Это происходит в транзакции
        Payment payment = paymentRepository.findById(paymentId);
        
        // Вызываем другой метод
        sendNotification(paymentId);  // Каких транзакций?
        // ❌ Если sendNotification() вызовётся, будет та же транзакция
        // ❌ Если notification упадёт, откатится вся processPayment()
    }
}

Решение: @Transactional(propagation = Propagation.REQUIRES_NEW)

@Service
public class NotificationService {
    
    /**
     * Отправляет уведомление ВСЕГДА в новой транзакции
     * Даже если вызывающий метод в транзакции
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void sendPaymentNotification(String paymentId) {
        // 1. Текущая транзакция приостанавливается (если она есть)
        // 2. Запускается НОВАЯ транзакция
        // 3. Код выполняется в новой транзакции
        // 4. Новая транзакция коммитится/откатывается независимо
        // 5. Старая транзакция возобновляется
        
        Notification notification = new Notification();
        notification.setPaymentId(paymentId);
        notification.setSentAt(Instant.now());
        
        notificationRepository.save(notification);
    }
}

@Service
public class PaymentService {
    private final NotificationService notificationService;
    
    @Transactional
    public void processPayment(String paymentId) {
        // Транзакция 1 (основная)
        Payment payment = paymentRepository.findById(paymentId);
        payment.setStatus("PROCESSED");
        paymentRepository.save(payment);
        
        try {
            // Вызываем метод с REQUIRES_NEW
            notificationService.sendPaymentNotification(paymentId);
            // Транзакция 2 (независимая) выполнилась и закоммитилась
        } catch (Exception ex) {
            // Даже если notification упал, processPayment() не откатится
            log.warn("Failed to send notification", ex);
        }
        
        // Основная транзакция продолжает работу
    }
}

Диаграмма: как работает REQUIRES_NEW

Транзакция 1 (основная):
|
+-- processPayment() начало
|   |
|   +-- update Payment <- сохранение в БД
|   |
|   +-- Вызов sendNotification()
|   |   |
|   |   +-- [REQUIRES_NEW: приостановить Транзакцию 1]
|   |   |
|   |   +-- Транзакция 2 (новая, независимая):
|   |   |   |
|   |   |   +-- insert Notification <- в новой транзакции
|   |   |   +-- commit Транзакция 2
|   |   |   └─ [Готово! Независимо от Транзакции 1]
|   |   |
|   |   +-- [Возобновить Транзакцию 1]
|   |
|   +-- Остальная логика processPayment()
|   |
|   +-- commit Транзакция 1
|
Все изменения сохранены в БД (обе транзакции)

Режимы Propagation

public enum Propagation {
    // ✅ 1. REQUIRED (по умолчанию)
    // Если есть транзакция -> используй её
    // Если нет -> создай новую
    @Transactional  // По умолчанию Propagation.REQUIRED
    public void save() { }
    
    // ✅ 2. REQUIRES_NEW
    // ВСЕГДА создавай новую транзакцию
    // Если есть старая -> приостанови, создай новую, возобнови старую
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void sendNotification() { }
    
    // ✅ 3. NESTED
    // Создай вложенную транзакцию (savepoint)
    // Если вложенная упала -> откат только вложенной
    @Transactional(propagation = Propagation.NESTED)
    public void logEvent() { }
    
    // ✅ 4. SUPPORTS
    // Если есть транзакция -> используй
    // Если нет -> выполняй без неё
    @Transactional(propagation = Propagation.SUPPORTS)
    public void read() { }
    
    // ✅ 5. NOT_SUPPORTED
    // Выполняй БЕЗ транзакции
    // Если есть транзакция -> приостанови
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void nonTransactionalOperation() { }
    
    // ✅ 6. NEVER
    // Выполняй БЕЗ транзакции
    // Если есть транзакция -> выброси ошибку
    @Transactional(propagation = Propagation.NEVER)
    public void mustNotHaveTransaction() { }
}

Практические примеры

Пример 1: Логирование независимо

@Service
public class AuditService {
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logUserAction(String userId, String action) {
        // Логируем в новой транзакции
        // Даже если основной метод откатится,
        // лог будет сохранён
        
        AuditLog log = new AuditLog();
        log.setUserId(userId);
        log.setAction(action);
        log.setTimestamp(Instant.now());
        
        auditRepository.save(log);
    }
}

@Service
public class UserService {
    private final AuditService auditService;
    
    @Transactional
    public void deactivateUser(String userId) {
        User user = userRepository.findById(userId);
        user.setStatus("INACTIVE");
        userRepository.save(user);
        
        // Логируем в независимой транзакции
        auditService.logUserAction(userId, "DEACTIVATED");
        
        // Даже если что-то упадёт дальше,
        // лог о деактивации будет сохранён
    }
}

Пример 2: Отправка письма независимо

@Service
public class EmailService {
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void sendEmail(String email, String subject, String body) {
        // Отправляем письмо в новой транзакции
        // Если письмо не отправилось, это не должно откатить основной процесс
        
        Email emailEntity = new Email();
        emailEntity.setTo(email);
        emailEntity.setSubject(subject);
        emailEntity.setBody(body);
        emailEntity.setStatus("SENT");
        emailEntity.setSentAt(Instant.now());
        
        emailRepository.save(emailEntity);
        
        try {
            smtpService.send(email, subject, body);
        } catch (MailException ex) {
            log.warn("Failed to send email to: {}", email, ex);
            emailEntity.setStatus("FAILED");
            emailRepository.save(emailEntity);
        }
    }
}

@Service
public class OrderService {
    private final EmailService emailService;
    
    @Transactional
    public void createOrder(Order order) {
        orderRepository.save(order);
        
        // Отправляем письмо в независимой транзакции
        // Если письмо не отправилось, заказ всё равно создан
        emailService.sendEmail(
            order.getCustomerEmail(),
            "Order Confirmation",
            "Your order #" + order.getId() + " has been created"
        );
    }
}

Пример 3: Очистка кеша независимо

@Service
public class CacheService {
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void invalidateCache(String cacheKey) {
        // Очищаем кеш в новой транзакции
        // Это гарантирует, что кеш очищен даже если основной процесс откатится
        
        cacheRepository.delete(cacheKey);
        redisService.remove(cacheKey);
    }
}

@Service
public class ProductService {
    private final CacheService cacheService;
    
    @Transactional
    public void updateProduct(Product product) {
        productRepository.save(product);
        
        // Очищаем кеш в новой транзакции
        cacheService.invalidateCache("product:" + product.getId());
        
        // Остальная логика...
    }
}

Ошибка: забыли создать отдельный бин

// ❌ НЕПРАВИЛЬНО: вызов через this
@Service
public class PaymentService {
    @Transactional
    public void processPayment(String paymentId) {
        // ...
        this.sendNotification(paymentId);
        // this.sendNotification() НЕ выполнится в новой транзакции!
        // Потому что Spring не может проксировать вызовы через this
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void sendNotification(String paymentId) { }
}

// ✅ ПРАВИЛЬНО: вызов через внедённый бин
@Service
public class PaymentService {
    private final NotificationService notificationService;  // Внедряем бин
    
    public PaymentService(NotificationService notificationService) {
        this.notificationService = notificationService;
    }
    
    @Transactional
    public void processPayment(String paymentId) {
        // ...
        notificationService.sendNotification(paymentId);
        // Правильно! Spring перехватит вызов и создаст новую транзакцию
    }
}

@Service
public class NotificationService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void sendNotification(String paymentId) { }
}

REQUIRES_NEW vs NESTED

REQUIRES_NEW:
├─ Транзакция 1 (основная)
│  ├─ Запрос 1
│  ├─ [Приостановить Транзакцию 1]
│  │
│  ├─ Транзакция 2 (новая, независимая)
│  │  ├─ Запрос 2
│  │  └─ commit
│  │
│  ├─ [Возобновить Транзакцию 1]
│  ├─ Запрос 3
│  └─ commit

НЕСТЕД (если БД поддерживает savepoint):
├─ Транзакция 1
│  ├─ Запрос 1
│  ├─ [Savepoint A]
│  │
│  ├─ Вложенная транзакция
│  │  ├─ Запрос 2
│  │  ├─ [Откат к Savepoint A, если ошибка]
│  │  └─ Commit savepoint
│  │
│  ├─ Запрос 3
│  └─ commit (вся транзакция 1)

Итого: когда использовать REQUIRES_NEW

Используй REQUIRES_NEW для:

  • Логирования (аудит должен сохраниться всегда)
  • Отправки уведомлений (письма, SMS)
  • Очистки кеша
  • Статистики и метрик
  • Других операций, которые должны быть независимы

⚠️ Будь осторожен с REQUIRES_NEW:

  • Это замедляет приложение (две транзакции вместо одной)
  • Может привести к deadlock'ам
  • Используй, только если действительно нужна независимость

Правило: внедри сервис через конструктор и вызовай через внедренный бин, иначе @Transactional(propagation = Propagation.REQUIRES_NEW) не сработает!

Как всегда открывать метод в новой транзакции | PrepBro