Как Spring работает с транзакциями
Spring отделяет прикладной код от конкретного transaction API. Общий контракт задаёт PlatformTransactionManager, а declarative-вариант обычно добавляет transaction interceptor через proxy.
caller
→ Spring proxy
→ TransactionInterceptor
→ transaction manager
→ target method
→ commit или rollbackНачало вызова
Interceptor читает настройки @Transactional:
- propagation;
- isolation;
- timeout;
- read-only;
- rollback rules;
- имя transaction manager.
Затем transaction manager либо присоединяется к существующей транзакции, либо начинает новую. Для JDBC он получает connection и выключает auto-commit; для JPA координирует EntityManager и resource transaction либо JTA.
Ресурс привязывается к текущему потоку. Поэтому repository внутри того же thread получает connection/EntityManager текущей транзакции без передачи его аргументом.
Завершение
метод вернулся
→ flush при необходимости
→ commit
исключение требует rollback
→ rollback
→ исходное исключение выходит наружуПо умолчанию declarative transaction откатывается на RuntimeException и Error. Для checked exception правило задают явно:
@Transactional(rollbackFor = IOException.class)
public void importFile(Path path) throws IOException {
// ...
}Если исключение перехвачено и метод вернулся нормально, interceptor не узнает о сбое. Реальный случай: Кейс исключение было проглочено внутри транзакции.
Почему нужен proxy-вызов
@Service
class ImportService {
public void start() {
saveBatch(); // self-invocation
}
@Transactional
public void saveBatch() {
// ...
}
}Внутренний вызов this.saveBatch() не проходит через Spring proxy, поэтому новая transaction advice для этого метода обычно не запускается.
Решения:
- вынести transaction boundary в другой bean;
- вызывать публичный use case через proxy снаружи;
- использовать
TransactionTemplate, если граница действительно должна находиться внутри алгоритма.
Propagation
| Режим | Поведение |
|---|---|
REQUIRED | присоединиться или создать |
REQUIRES_NEW | приостановить текущую и создать независимую |
NESTED | savepoint внутри физической транзакции, если manager поддерживает |
SUPPORTS | участвовать, если транзакция есть |
MANDATORY | требовать существующую |
NOT_SUPPORTED | выполнить без транзакции |
NEVER | завершиться ошибкой при наличии транзакции |
NESTED не означает ещё одну независимую database transaction. Это обычно savepoint. REQUIRES_NEW требует отдельного физического transaction/resource и может исчерпать connection pool при неудачном использовании.
Подробнее: Propagation.
Programmatic transaction
TransactionTemplate полезен, когда граница является частью явного control flow:
OrderView view = transactionTemplate.execute(status -> {
Order order = repository.findById(id)
.orElseThrow();
return mapper.toView(order);
});Прямой вызов PlatformTransactionManager нужен реже. Programmatic API не «хуже» declarative; он просто делает границу частью кода и увеличивает связанность с инфраструктурой.
Isolation и read-only
isolation применяется при создании новой транзакции и должен поддерживаться конкретным manager/database. Он не исправляет неверный бизнес-инвариант автоматически: см. Изоляция транзакций.
readOnly = true — подсказка transaction infrastructure/provider и полезное описание намерения. Нельзя считать её универсальным запретом на SQL UPDATE.
Транзакция и внешняя система
Database transaction не включает обычный HTTP-вызов или отправку сообщения в другую систему:
UPDATE в БД
→ внешний вызов прошёл
→ локальный commit упалRollback вернёт локальную БД, но не отменит удалённый эффект. Нужны idempotency, outbox, saga/compensation или другой согласованный protocol.
Вопросы для собеседования
Почему @Transactional не сработал на private method?
Обычный proxy intercepts внешний method call. Private method не является такой точкой входа, а self-invocation proxy обходит.
NESTED и REQUIRES_NEW одинаковы?
Нет. NESTED обычно использует savepoint текущей physical transaction, REQUIRES_NEW создаёт независимую.
Что произойдёт, если поймать exception?
Если не выбросить его наружу и не пометить transaction rollback-only, proxy увидит нормальное завершение и может сделать commit.
Открывает ли transaction controller?
Только если именно вызов controller method проходит через настроенный transaction proxy. Обычно boundary держат в application service.
Откатит ли Spring HTTP-вызов?
Нет.