Співбесіда Spring Boot: Поширення Транзакцій

Опануйте поширення транзакцій у Spring Boot: REQUIRED, REQUIRES_NEW, NESTED тощо. 12 питань зі співбесід з кодом та поширеними пастками.

Spring Boot Поширення Транзакцій: питання співбесід та практичні приклади

Поширення транзакцій є фундаментальною концепцією у Spring Boot, яку регулярно перевіряють під час технічних співбесід. Розуміння того, як транзакції взаємодіють між методами з анотацією @Transactional, допомагає уникати тонких помилок у продакшені та проєктувати надійні архітектури.

Порада щодо підготовки

Інтерв'юери оцінюють здатність обирати правильний рівень поширення відповідно до бізнес-контексту. Уміння пояснити, чому REQUIRES_NEW замість REQUIRED у конкретному випадку, робить різницю.

Основи поширення транзакцій

Питання 1: Що таке поширення транзакцій у Spring?

Поширення визначає поведінку транзакційного методу, коли його викликають у контексті існуючої транзакції. Воно відповідає на питання: «Що відбувається, коли метод @Transactional викликає інший метод, який також має анотацію?»

OrderService.javajava
// Демонстрація концепції поширення
@Service
public class OrderService {

    private final PaymentService paymentService;
    private final OrderRepository orderRepository;

    // Батьківська транзакція - починає нову транзакцію
    @Transactional
    public void createOrder(OrderRequest request) {
        // Зберігає замовлення в поточній транзакції
        Order order = orderRepository.save(new Order(request));

        // Виклик іншого методу @Transactional
        // Поширення визначає: та сама транзакція чи нова?
        paymentService.processPayment(order.getId(), request.getAmount());
    }
}

@Service
public class PaymentService {

    // Поширення за замовчуванням: REQUIRED
    // Приєднується до існуючої транзакції з createOrder()
    @Transactional
    public void processPayment(Long orderId, BigDecimal amount) {
        // Виконується в ТІЙ САМІЙ транзакції, що й createOrder()
        // Якщо цей метод зазнає невдачі, замовлення також буде відкочено
    }
}

Spring надає сім рівнів поширення, кожен з яких підходить для специфічних бізнес-потреб. Вибір безпосередньо впливає на узгодженість даних і продуктивність.

Питання 2: Опишіть поведінку REQUIRED (поширення за замовчуванням)

REQUIRED — це поширення за замовчуванням. Якщо транзакція існує, метод приєднується до неї. Інакше створюється нова. Це найпоширеніша та найінтуїтивніша поведінка.

UserService.javajava
// REQUIRED: поведінка за замовчуванням
@Service
public class UserService {

    private final UserRepository userRepository;
    private final AuditService auditService;

    @Transactional(propagation = Propagation.REQUIRED)
    public void updateUser(Long userId, UserUpdateRequest request) {
        // Починає транзакцію, якщо жодної не існує
        User user = userRepository.findById(userId).orElseThrow();
        user.setEmail(request.getEmail());
        userRepository.save(user);

        // Аудит приєднується до тієї самої транзакції
        auditService.logUpdate(userId, "EMAIL_CHANGED");
    }
}

@Service
public class AuditService {

    private final AuditLogRepository auditLogRepository;

    @Transactional(propagation = Propagation.REQUIRED)
    public void logUpdate(Long userId, String action) {
        // Приєднується до транзакції updateUser()
        // Commit або rollback разом
        auditLogRepository.save(new AuditLog(userId, action, Instant.now()));
    }
}

Діаграма нижче ілюструє транзакційний потік:

text
updateUser() починає TX-1
    ├── save(user)          → TX-1
    └── logUpdate()         → приєднується до TX-1 (REQUIRED)
           └── save(audit)  → TX-1

Якщо logUpdate() зазнає невдачі → rollback TX-1 → user І audit скасовано

Питання 3: Коли використовувати REQUIRES_NEW замість REQUIRED?

REQUIRES_NEW призупиняє існуючу транзакцію та створює нову незалежну транзакцію. Корисно, коли операція повинна бути зафіксована незалежно від результату батьківської транзакції.

PaymentService.javajava
// REQUIRES_NEW: незалежна транзакція
@Service
public class PaymentService {

    private final PaymentRepository paymentRepository;
    private final PaymentAuditService auditService;

    @Transactional
    public void processPayment(Long orderId, BigDecimal amount) {
        Payment payment = new Payment(orderId, amount);
        paymentRepository.save(payment);

        // Аудит МАЄ бути збережений, навіть якщо платіж пізніше зазнає невдачі
        auditService.logPaymentAttempt(orderId, amount);

        // Симуляція помилки після аудиту
        if (amount.compareTo(BigDecimal.ZERO) < 0) {
            throw new InvalidAmountException("Negative amount");
        }
    }
}

@Service
public class PaymentAuditService {

    private final PaymentAuditRepository auditRepository;

    // REQUIRES_NEW: фіксує незалежно від батьківської транзакції
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logPaymentAttempt(Long orderId, BigDecimal amount) {
        // Створено нову транзакцію TX-2
        // TX-1 (processPayment) призупинено
        auditRepository.save(new PaymentAudit(orderId, amount, "ATTEMPTED"));
        // TX-2 фіксується тут, незалежно від TX-1
    }
}

Транзакційний потік з REQUIRES_NEW:

text
processPayment() починає TX-1
    ├── save(payment)              → TX-1
    ├── logPaymentAttempt()        → TX-1 ПРИЗУПИНЕНО
    │       └── починає TX-2       → нова транзакція
    │       └── save(audit)        → TX-2
    │       └── COMMIT TX-2        → audit збережено
    │       └── TX-1 ПРОДОВЖУЄ
    └── throw InvalidAmountException
        └── ROLLBACK TX-1          → платіж скасовано, але audit збережено
Стережіться deadlock'ів

REQUIRES_NEW може спричинити deadlock'и, якщо нова транзакція звертається до тих самих ресурсів, заблокованих призупиненою транзакцією. Уникайте використання REQUIRES_NEW для модифікації тих самих таблиць, що й батьківська транзакція.

Питання 4: Поясніть поширення NESTED і чим воно відрізняється від REQUIRES_NEW

NESTED створює savepoint у поточній транзакції. Якщо метод зазнає невдачі, відкочуються лише зміни з моменту savepoint, а не вся батьківська транзакція.

BatchProcessingService.javajava
// NESTED: savepoint у батьківській транзакції
@Service
public class BatchProcessingService {

    private final ItemRepository itemRepository;
    private final ItemProcessor itemProcessor;

    @Transactional
    public BatchResult processBatch(List<Item> items) {
        BatchResult result = new BatchResult();

        for (Item item : items) {
            try {
                // Кожен елемент обробляється з savepoint
                itemProcessor.processItem(item);
                result.addSuccess(item.getId());
            } catch (ProcessingException e) {
                // Rollback лише цього елемента, не всього batch
                result.addFailure(item.getId(), e.getMessage());
            }
        }

        return result; // Commit успішних елементів
    }
}

@Service
public class ItemProcessor {

    private final ItemRepository itemRepository;

    // NESTED: створює savepoint, частковий rollback можливий
    @Transactional(propagation = Propagation.NESTED)
    public void processItem(Item item) {
        item.setStatus("PROCESSING");
        itemRepository.save(item);

        // Бізнес-валідація
        if (!isValid(item)) {
            throw new ProcessingException("Invalid item");
            // Rollback до savepoint → лише цей елемент
        }

        item.setStatus("COMPLETED");
        itemRepository.save(item);
    }
}

Порівняння NESTED та REQUIRES_NEW:

text
NESTED:
├── Використовує savepoint у батьківській TX
├── При помилці → rollback до savepoint
├── Якщо батьківська TX робить rollback → NESTED також відкочується
└── Більш продуктивний (без нового з'єднання)

REQUIRES_NEW:
├── Створює повністю незалежну транзакцію
├── При помилці → rollback лише дочірньої TX
├── Якщо батьківська TX робить rollback → дочірня TX ВЖЕ ЗАФІКСОВАНА
└── Потребує нового з'єднання
Підтримка NESTED

Поширення NESTED потребує підтримки JDBC savepoints. Більшість сучасних баз даних (PostgreSQL, MySQL, Oracle) підтримує це. Перевірте сумісність перед використанням.

Розширені типи поширення

Питання 5: Коли використовувати SUPPORTS і NOT_SUPPORTED?

SUPPORTS виконується в існуючій транзакції, якщо вона є, інакше без транзакції. NOT_SUPPORTED призупиняє будь-яку існуючу транзакцію та виконується без транзакції.

ReportingService.javajava
// SUPPORTS: опціональна транзакція
@Service
public class ReportingService {

    private final ReportRepository reportRepository;

    // SUPPORTS: працює з транзакцією або без неї
    // Корисно для читань, які не потребують транзакційних гарантій
    @Transactional(propagation = Propagation.SUPPORTS)
    public Report generateReport(Long reportId) {
        // Якщо викликається з методу @Transactional → використовує його TX
        // Якщо викликається безпосередньо → без транзакції (читання OK)
        return reportRepository.generateComplexReport(reportId);
    }
}

@Service
public class ExternalNotificationService {

    private final ExternalApiClient apiClient;

    // NOT_SUPPORTED: ніколи в транзакції
    // Уникає блокування TX під час повільного зовнішнього виклику
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void sendExternalNotification(String message) {
        // Батьківська TX призупинена під час виклику
        apiClient.send(message); // Потенційно повільний HTTP-виклик
        // Батьківська TX продовжує після
    }
}
OrderService.javajava
// Приклад комбінованого використання
@Service
public class OrderService {

    private final OrderRepository orderRepository;
    private final ReportingService reportingService;
    private final ExternalNotificationService notificationService;

    @Transactional
    public void completeOrder(Long orderId) {
        Order order = orderRepository.findById(orderId).orElseThrow();
        order.setStatus("COMPLETED");
        orderRepository.save(order);

        // Генерація звіту в тій самій TX (SUPPORTS)
        Report report = reportingService.generateReport(orderId);

        // Зовнішнє сповіщення ПОЗА транзакцією (NOT_SUPPORTED)
        // Уникає таймауту TX, якщо зовнішній API повільний
        notificationService.sendExternalNotification(
            "Order " + orderId + " completed"
        );
    }
}

Питання 6: Поясніть MANDATORY і NEVER

MANDATORY вимагає існуючої транзакції та інакше викидає виняток. NEVER вимагає відсутності транзакції та викидає виняток, якщо вона існує.

AuditService.javajava
// MANDATORY: має викликатися з транзакції
@Service
public class AuditService {

    private final AuditRepository auditRepository;

    // MANDATORY: відмовляється виконуватися без існуючої транзакції
    // Гарантує, що аудит завжди атомарний з аудитованою операцією
    @Transactional(propagation = Propagation.MANDATORY)
    public void logCriticalAction(String action, Long entityId) {
        // Якщо викликається без транзакції → IllegalTransactionStateException
        auditRepository.save(new AuditLog(action, entityId, Instant.now()));
    }
}

// CacheService.java
// NEVER: ніколи не повинен бути в транзакції
@Service
public class CacheService {

    private final CacheManager cacheManager;

    // NEVER: кеш не повинен брати участь у транзакціях
    // Уникає невідповідностей між кешем і БД після rollback
    @Transactional(propagation = Propagation.NEVER)
    public void invalidateCache(String cacheKey) {
        // Якщо викликається з @Transactional → IllegalTransactionStateException
        cacheManager.getCache("entities").evict(cacheKey);
    }
}
SecurityService.javajava
// Правильне використання MANDATORY
@Service
public class SecurityService {

    private final AuditService auditService;

    @Transactional
    public void changeUserPassword(Long userId, String newPassword) {
        // Чутлива операція...
        updatePassword(userId, newPassword);

        // Аудит МАЄ бути в тій самій транзакції
        // MANDATORY забезпечує це архітектурне обмеження
        auditService.logCriticalAction("PASSWORD_CHANGE", userId);
    }

    // ПОМИЛКА: прямий виклик без транзакції
    public void badUsage() {
        // Викидає IllegalTransactionStateException, оскільки немає TX
        auditService.logCriticalAction("TEST", 1L);
    }
}

Готовий до співбесід з Spring Boot?

Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.

Поширені пастки на співбесіді

Питання 7: Чому @Transactional не працює з внутрішніми викликами?

Одна з найпоширеніших пасток. Внутрішні виклики методів (self-invocation) обходять Spring proxy, відключаючи управління транзакціями.

BrokenService.javajava
// КЛАСИЧНА ПОМИЛКА: self-invocation
@Service
public class BrokenService {

    private final ItemRepository itemRepository;

    public void processItems(List<Long> itemIds) {
        for (Long id : itemIds) {
            // ПАСТКА: внутрішній виклик → обходить proxy
            // @Transactional на processItem() ІГНОРУЄТЬСЯ
            processItem(id);
        }
    }

    @Transactional
    public void processItem(Long itemId) {
        // Ця транзакція НІКОЛИ не створюється при внутрішніх викликах
        Item item = itemRepository.findById(itemId).orElseThrow();
        item.setStatus("PROCESSED");
        itemRepository.save(item);
    }
}

Рішення для уникнення цієї пастки:

java
// Рішення 1: Self-injection
@Service
public class FixedServiceWithSelfInjection {

    private final ItemRepository itemRepository;

    @Lazy
    @Autowired
    private FixedServiceWithSelfInjection self; // Self-injection

    public void processItems(List<Long> itemIds) {
        for (Long id : itemIds) {
            // Виклик через proxy → @Transactional працює
            self.processItem(id);
        }
    }

    @Transactional
    public void processItem(Long itemId) {
        // Транзакція створена правильно
        Item item = itemRepository.findById(itemId).orElseThrow();
        item.setStatus("PROCESSED");
        itemRepository.save(item);
    }
}

// Рішення 2: Розділити на два сервіси (рекомендоване)
@Service
public class ItemOrchestrator {

    private final ItemProcessor processor;

    public void processItems(List<Long> itemIds) {
        for (Long id : itemIds) {
            // Виклик до іншого bean → proxy працює
            processor.processItem(id);
        }
    }
}

@Service
public class ItemProcessor {

    private final ItemRepository itemRepository;

    @Transactional
    public void processItem(Long itemId) {
        // Транзакція керується правильно
        Item item = itemRepository.findById(itemId).orElseThrow();
        item.setStatus("PROCESSED");
        itemRepository.save(item);
    }
}

Питання 8: Як обробляти винятки та rollback?

За замовчуванням Spring робить rollback лише на RuntimeException та Error. Checked винятки НЕ викликають автоматичний rollback.

TransactionRollbackDemo.javajava
// Поведінка rollback залежно від типу винятку
@Service
public class TransactionRollbackDemo {

    private final OrderRepository orderRepository;

    // RuntimeException → автоматичний ROLLBACK
    @Transactional
    public void methodWithRuntimeException() {
        orderRepository.save(new Order());
        throw new RuntimeException("Error"); // ROLLBACK
    }

    // Checked Exception → БЕЗ rollback за замовчуванням
    @Transactional
    public void methodWithCheckedException() throws IOException {
        orderRepository.save(new Order());
        throw new IOException("File error"); // Все одно COMMIT!
    }

    // Примусити rollback на checked exception
    @Transactional(rollbackFor = IOException.class)
    public void methodWithRollbackFor() throws IOException {
        orderRepository.save(new Order());
        throw new IOException("Error"); // ROLLBACK завдяки rollbackFor
    }

    // Виключити RuntimeException з rollback
    @Transactional(noRollbackFor = BusinessException.class)
    public void methodWithNoRollbackFor() {
        orderRepository.save(new Order());
        throw new BusinessException("Warning"); // COMMIT попри виняток
    }
}

Рекомендована конфігурація для бізнес-випадків:

BaseTransactionalService.javajava
// Узгоджена транзакційна конфігурація
@Service
public abstract class BaseTransactionalService {

    // Rollback на всіх винятках (checked та unchecked)
    @Transactional(rollbackFor = Exception.class)
    protected void executeInTransaction(Runnable operation) {
        operation.run();
    }
}

// PaymentService.java
@Service
public class PaymentService extends BaseTransactionalService {

    private final PaymentRepository paymentRepository;

    @Transactional(
        rollbackFor = Exception.class,
        noRollbackFor = InsufficientFundsException.class
    )
    public PaymentResult processPayment(PaymentRequest request) {
        Payment payment = new Payment(request);
        paymentRepository.save(payment);

        if (request.getAmount().compareTo(getBalance()) > 0) {
            // НЕ робить rollback - хочемо зберегти запис спроби
            throw new InsufficientFundsException("Insufficient balance");
        }

        return new PaymentResult(payment.getId(), "SUCCESS");
    }
}

Питання 9: Як ізоляція транзакцій працює з поширенням?

Ізоляція та поширення доповнюють одне одного. Ізоляція визначає видимість даних між паралельними транзакціями.

IsolationDemo.javajava
// Поєднання ізоляції + поширення
@Service
public class IsolationDemo {

    private final AccountRepository accountRepository;

    // READ_COMMITTED: бачить дані, зафіксовані іншими транзакціями
    @Transactional(
        isolation = Isolation.READ_COMMITTED,
        propagation = Propagation.REQUIRED
    )
    public BigDecimal getAccountBalance(Long accountId) {
        // Може бачити різні значення при повторному читанні під час TX
        return accountRepository.findById(accountId)
            .map(Account::getBalance)
            .orElse(BigDecimal.ZERO);
    }

    // REPEATABLE_READ: гарантує те саме читання протягом транзакції
    @Transactional(
        isolation = Isolation.REPEATABLE_READ,
        propagation = Propagation.REQUIRED
    )
    public void transferWithConsistentRead(Long fromId, Long toId, BigDecimal amount) {
        Account from = accountRepository.findById(fromId).orElseThrow();
        Account to = accountRepository.findById(toId).orElseThrow();

        // Навіть якщо інша транзакція модифікує 'from' тим часом,
        // ми завжди бачимо початкове значення (snapshot)
        from.debit(amount);
        to.credit(amount);

        accountRepository.save(from);
        accountRepository.save(to);
    }

    // SERIALIZABLE: максимальна ізоляція, без конкурентності
    @Transactional(
        isolation = Isolation.SERIALIZABLE,
        propagation = Propagation.REQUIRES_NEW
    )
    public void criticalOperation(Long accountId) {
        // Блокує будь-яку іншу транзакцію на цих даних
        // Використовуйте економно - вплив на продуктивність
        Account account = accountRepository.findById(accountId).orElseThrow();
        account.performCriticalUpdate();
        accountRepository.save(account);
    }
}

Зведена таблиця рівнів ізоляції:

text
| Ізоляція         | Dirty Read | Non-Repeatable | Phantom |
|------------------|------------|----------------|---------|
| READ_UNCOMMITTED | Можливо    | Можливо        | Можливо |
| READ_COMMITTED   | Ні         | Можливо        | Можливо |
| REPEATABLE_READ  | Ні         | Ні             | Можливо |
| SERIALIZABLE     | Ні         | Ні             | Ні      |
Вплив на продуктивність

Чим суворіша ізоляція, тим більше продуктивність може страждати від блокувань. SERIALIZABLE може спричинити значну конкуренцію в продакшені з високим трафіком.

Розширені патерни

Питання 10: Як реалізувати патерн «transactional outbox»?

Патерн outbox гарантує узгодженість між модифікаціями бази даних та надсиланням повідомлень/подій, навіть у разі збою.

OutboxService.javajava
// Патерн Transactional Outbox
@Service
public class OutboxService {

    private final OutboxRepository outboxRepository;

    // Зберігає подію в тій самій транзакції, що й бізнес-сутність
    @Transactional(propagation = Propagation.MANDATORY)
    public void saveEvent(String aggregateType, Long aggregateId, String eventType, String payload) {
        OutboxEvent event = OutboxEvent.builder()
            .aggregateType(aggregateType)
            .aggregateId(aggregateId)
            .eventType(eventType)
            .payload(payload)
            .status("PENDING")
            .createdAt(Instant.now())
            .build();
        outboxRepository.save(event);
    }
}

@Service
public class OrderService {

    private final OrderRepository orderRepository;
    private final OutboxService outboxService;

    @Transactional
    public Order createOrder(OrderRequest request) {
        // Створити замовлення
        Order order = new Order(request);
        orderRepository.save(order);

        // Подія outbox у ТІЙ САМІЙ транзакції (MANDATORY)
        // Якщо commit успішний → обидва збережено
        // Якщо rollback → жодного не збережено
        outboxService.saveEvent(
            "ORDER",
            order.getId(),
            "ORDER_CREATED",
            toJson(new OrderCreatedEvent(order))
        );

        return order;
    }
}

// OutboxPublisher.java
// Окремий процес, що публікує події
@Service
public class OutboxPublisher {

    private final OutboxRepository outboxRepository;
    private final MessageBroker messageBroker;

    // Незалежна транзакція для кожної публікації
    @Scheduled(fixedDelay = 1000)
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void publishPendingEvents() {
        List<OutboxEvent> events = outboxRepository
            .findByStatusOrderByCreatedAt("PENDING");

        for (OutboxEvent event : events) {
            try {
                messageBroker.publish(event.getEventType(), event.getPayload());
                event.setStatus("PUBLISHED");
                event.setPublishedAt(Instant.now());
            } catch (Exception e) {
                event.setStatus("FAILED");
                event.setError(e.getMessage());
            }
            outboxRepository.save(event);
        }
    }
}

Питання 11: Як тестувати різні поведінки поширення?

Тести поширення вимагають особливої уваги для перевірки очікуваної транзакційної поведінки.

TransactionPropagationTest.javajava
// Тестування поведінок поширення
@SpringBootTest
@Transactional
class TransactionPropagationTest {

    @Autowired
    private OrderService orderService;

    @Autowired
    private PaymentService paymentService;

    @Autowired
    private TestEntityManager entityManager;

    @Test
    void required_shouldShareTransaction() {
        // Given
        Order order = orderService.createOrder(new OrderRequest());

        // When - платіж у тій самій транзакції (REQUIRED)
        paymentService.processPayment(order.getId(), BigDecimal.TEN);

        // Then - обидва видно перед commit
        entityManager.flush();
        assertThat(entityManager.find(Order.class, order.getId())).isNotNull();
        assertThat(entityManager.find(Payment.class, order.getId())).isNotNull();
    }

    @Test
    void requiresNew_shouldCommitIndependently() {
        // Given
        Long orderId = null;

        try {
            // When - audit у REQUIRES_NEW
            orderId = orderService.createOrderWithAudit(new OrderRequest());
            throw new RuntimeException("Simulated failure after audit");
        } catch (RuntimeException e) {
            // Основна транзакція робить rollback
        }

        // Then - audit (REQUIRES_NEW) все ще зафіксовано
        assertThat(findAuditLog(orderId)).isNotNull();
        // Але замовлення відкочено
        assertThat(findOrder(orderId)).isNull();
    }

    @Test
    void mandatory_shouldThrowWithoutTransaction() {
        // Given - немає активної транзакції

        // When/Then - має викинути виняток
        assertThatThrownBy(() -> auditService.logCriticalAction("TEST", 1L))
            .isInstanceOf(IllegalTransactionStateException.class)
            .hasMessageContaining("No existing transaction");
    }
}
PropagationIntegrationTest.javajava
// Інтеграційний тест із реальним rollback та commit
@SpringBootTest
class PropagationIntegrationTest {

    @Autowired
    private OrderService orderService;

    @Autowired
    private OrderRepository orderRepository;

    @Autowired
    private AuditLogRepository auditLogRepository;

    @Test
    void nested_shouldRollbackOnlyNestedOnFailure() {
        // Given
        int initialCount = orderRepository.findAll().size();

        // When - batch-обробка з NESTED
        BatchResult result = orderService.processBatchWithNested(
            List.of(validItem(), invalidItem(), validItem())
        );

        // Then - тільки валідні елементи зберігаються
        assertThat(result.getSuccessCount()).isEqualTo(2);
        assertThat(result.getFailureCount()).isEqualTo(1);
        assertThat(orderRepository.findAll().size()).isEqualTo(initialCount + 2);
    }
}

Питання 12: Які найкращі практики конфігурації транзакцій?

Узгоджена транзакційна конфігурація на рівні проєкту уникає сюрпризів і полегшує підтримку.

TransactionConfig.javajava
// Централізована конфігурація транзакцій
@Configuration
@EnableTransactionManagement
public class TransactionConfig {

    // Таймаут за замовчуванням для всіх транзакцій
    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        DataSourceTransactionManager tm = new DataSourceTransactionManager(dataSource);
        tm.setDefaultTimeout(30); // 30 секунд максимум на транзакцію
        return tm;
    }
}

// BaseService.java
// Анотації за замовчуванням для сервісів
@Service
@Transactional(
    readOnly = true, // Лише читання за замовчуванням
    rollbackFor = Exception.class // Rollback на будь-якому винятку
)
public abstract class BaseService {
    // Методи читання успадковують readOnly = true
}

// OrderService.java
// Сервіс з узгодженою конфігурацією
@Service
public class OrderService extends BaseService {

    private final OrderRepository orderRepository;

    // Успадковує readOnly = true
    public Order findById(Long id) {
        return orderRepository.findById(id).orElseThrow();
    }

    // Override для записів
    @Transactional(readOnly = false)
    public Order createOrder(OrderRequest request) {
        return orderRepository.save(new Order(request));
    }

    // Явна конфігурація для критичних випадків
    @Transactional(
        readOnly = false,
        propagation = Propagation.REQUIRES_NEW,
        isolation = Isolation.SERIALIZABLE,
        timeout = 10
    )
    public void criticalOperation(Long orderId) {
        // Операція з максимальною ізоляцією та коротким таймаутом
    }
}

Чек-лист найкращих практик:

text
Конфігурація транзакцій - Чек-лист

✅ readOnly = true за замовчуванням, явний override для записів
✅ rollbackFor = Exception.class для включення checked exceptions
✅ Відповідний таймаут залежно від типу операції
✅ Уникати внутрішніх викликів (@Transactional ігнорується)
✅ REQUIRES_NEW лише коли потрібен незалежний commit
✅ MANDATORY для гарантії транзакційного контексту
✅ Явні тести поведінок rollback
✅ Моніторити довготривалі транзакції
✅ Документувати нестандартні вибори поширення

Готовий до співбесід з Spring Boot?

Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.

Висновок

Поширення транзакцій — це фундаментальна концепція, яку оцінюють на співбесідах Spring Boot. Ключові моменти для запам'ятовування:

Поширені типи поширення:

  • ✅ REQUIRED (за замовчуванням): приєднується або створює транзакцію
  • ✅ REQUIRES_NEW: незалежна транзакція, окремий commit
  • ✅ NESTED: savepoint для часткового rollback
  • ✅ MANDATORY: вимагає існуючої транзакції

Пастки, яких слід уникати:

  • ✅ Self-invocation: обходить proxy, @Transactional ігнорується
  • ✅ Checked винятки: без rollback за замовчуванням
  • ✅ REQUIRES_NEW на тих самих даних: ризик deadlock
  • ✅ Відсутній таймаут: транзакції заблоковані необмежено

Найкращі практики:

  • ✅ readOnly = true за замовчуванням
  • ✅ rollbackFor = Exception.class систематично
  • ✅ Окремі сервіси для уникнення self-invocation
  • ✅ Явно тестувати поведінки rollback

Опанування поширення транзакцій демонструє глибоке розуміння Spring і управління даними. Ці концепції є важливими для проєктування надійних додатків і успішного проходження технічних співбесід.

Починай практикувати!

Перевір свої знання з нашими симуляторами співбесід та технічними тестами.

Теги

#spring boot
#transactions
#propagation
#java
#interview

Поділитися

Пов'язані статті