← Назад к вопросам
Как всегда открывать метод в новой транзакции
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) не сработает!