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

Поширення транзакцій є фундаментальною концепцією у Spring Boot, яку регулярно перевіряють під час технічних співбесід. Розуміння того, як транзакції взаємодіють між методами з анотацією @Transactional, допомагає уникати тонких помилок у продакшені та проєктувати надійні архітектури.
Інтерв'юери оцінюють здатність обирати правильний рівень поширення відповідно до бізнес-контексту. Уміння пояснити, чому REQUIRES_NEW замість REQUIRED у конкретному випадку, робить різницю.
Основи поширення транзакцій
Питання 1: Що таке поширення транзакцій у Spring?
Поширення визначає поведінку транзакційного методу, коли його викликають у контексті існуючої транзакції. Воно відповідає на питання: «Що відбувається, коли метод @Transactional викликає інший метод, який також має анотацію?»
// Демонстрація концепції поширення
@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 — це поширення за замовчуванням. Якщо транзакція існує, метод приєднується до неї. Інакше створюється нова. Це найпоширеніша та найінтуїтивніша поведінка.
// 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()));
}
}Діаграма нижче ілюструє транзакційний потік:
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 призупиняє існуючу транзакцію та створює нову незалежну транзакцію. Корисно, коли операція повинна бути зафіксована незалежно від результату батьківської транзакції.
// 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:
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 збереженоREQUIRES_NEW може спричинити deadlock'и, якщо нова транзакція звертається до тих самих ресурсів, заблокованих призупиненою транзакцією. Уникайте використання REQUIRES_NEW для модифікації тих самих таблиць, що й батьківська транзакція.
Питання 4: Поясніть поширення NESTED і чим воно відрізняється від REQUIRES_NEW
NESTED створює savepoint у поточній транзакції. Якщо метод зазнає невдачі, відкочуються лише зміни з моменту savepoint, а не вся батьківська транзакція.
// 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:
NESTED:
├── Використовує savepoint у батьківській TX
├── При помилці → rollback до savepoint
├── Якщо батьківська TX робить rollback → NESTED також відкочується
└── Більш продуктивний (без нового з'єднання)
REQUIRES_NEW:
├── Створює повністю незалежну транзакцію
├── При помилці → rollback лише дочірньої TX
├── Якщо батьківська TX робить rollback → дочірня TX ВЖЕ ЗАФІКСОВАНА
└── Потребує нового з'єднанняПоширення NESTED потребує підтримки JDBC savepoints. Більшість сучасних баз даних (PostgreSQL, MySQL, Oracle) підтримує це. Перевірте сумісність перед використанням.
Розширені типи поширення
Питання 5: Коли використовувати SUPPORTS і NOT_SUPPORTED?
SUPPORTS виконується в існуючій транзакції, якщо вона є, інакше без транзакції. NOT_SUPPORTED призупиняє будь-яку існуючу транзакцію та виконується без транзакції.
// 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 продовжує після
}
}// Приклад комбінованого використання
@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 вимагає відсутності транзакції та викидає виняток, якщо вона існує.
// 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);
}
}// Правильне використання 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, відключаючи управління транзакціями.
// КЛАСИЧНА ПОМИЛКА: 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);
}
}Рішення для уникнення цієї пастки:
// Рішення 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.
// Поведінка 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 попри виняток
}
}Рекомендована конфігурація для бізнес-випадків:
// Узгоджена транзакційна конфігурація
@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: Як ізоляція транзакцій працює з поширенням?
Ізоляція та поширення доповнюють одне одного. Ізоляція визначає видимість даних між паралельними транзакціями.
// Поєднання ізоляції + поширення
@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);
}
}Зведена таблиця рівнів ізоляції:
| Ізоляція | Dirty Read | Non-Repeatable | Phantom |
|------------------|------------|----------------|---------|
| READ_UNCOMMITTED | Можливо | Можливо | Можливо |
| READ_COMMITTED | Ні | Можливо | Можливо |
| REPEATABLE_READ | Ні | Ні | Можливо |
| SERIALIZABLE | Ні | Ні | Ні |Чим суворіша ізоляція, тим більше продуктивність може страждати від блокувань. SERIALIZABLE може спричинити значну конкуренцію в продакшені з високим трафіком.
Розширені патерни
Питання 10: Як реалізувати патерн «transactional outbox»?
Патерн outbox гарантує узгодженість між модифікаціями бази даних та надсиланням повідомлень/подій, навіть у разі збою.
// Патерн 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: Як тестувати різні поведінки поширення?
Тести поширення вимагають особливої уваги для перевірки очікуваної транзакційної поведінки.
// Тестування поведінок поширення
@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");
}
}// Інтеграційний тест із реальним 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: Які найкращі практики конфігурації транзакцій?
Узгоджена транзакційна конфігурація на рівні проєкту уникає сюрпризів і полегшує підтримку.
// Централізована конфігурація транзакцій
@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) {
// Операція з максимальною ізоляцією та коротким таймаутом
}
}Чек-лист найкращих практик:
Конфігурація транзакцій - Чек-лист
✅ 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 і управління даними. Ці концепції є важливими для проєктування надійних додатків і успішного проходження технічних співбесід.
Починай практикувати!
Перевір свої знання з нашими симуляторами співбесід та технічними тестами.
Теги
Поділитися
Пов'язані статті

30 Питань на Співбесіді зі Spring Boot: Повний Гід для Java-розробників
Підготуйтеся до співбесід зі Spring Boot із 30 ключовими питаннями про авто-конфігурацію, стартери, Spring Data JPA, безпеку й тестування.

Spring Modulith: Архітектура модульного моноліта
Опануйте Spring Modulith для побудови модульних монолітів на Java. Архітектура, модулі, асинхронні події та тестування зі Spring Boot 3.

Співбесіда Spring Batch 5: Партиціювання, Чанки та Відмовостійкість
Опануйте співбесіди Spring Batch 5: 15 ключових питань про партиціювання, обробку чанків і відмовостійкість з прикладами на Java 21.