Tranzakciók
@Transactional, propagation, isolation levels, rollback rules
Tranzakciókezelés
A @Transactional annotáció proxy-alapú AOP-val biztosítja az ACID tulajdonságokat: a propagáció, izoláció és rollback szabályok finomhangolják a tranzakciós viselkedést.
1. Definíció
A tranzakciókezelés biztosítja, hogy adatbázis-műveletek csoportja atomikusan hajtódjon végre: vagy mind sikerül, vagy mind visszagörgethető. A Spring deklaratív tranzakciókezelést ad a @Transactional annotációval, amelyet AOP proxy valósít meg.
A Spring PlatformTransactionManager interfészen keresztül absztrahálja a tranzakciós API-t (JPA, JDBC, JTA, R2DBC). A Spring Boot a spring-boot-starter-data-jpa starter-rel automatikusan konfigurál egy JpaTransactionManager-t.
Service method call → AOP Proxy → begin transaction
→ target method execution
→ commit / rollback
Az ACID tulajdonságok:
- Atomicity: Minden művelet sikerül vagy mind visszagörgetődik
- Consistency: Az adatbázis konzisztens állapotból konzisztensbe kerül
- Isolation: Párhuzamos tranzakciók egymástól elkülönülten futnak
- Durability: Commit után az adat tartósan megmarad
2. Alapfogalmak
@Transactional alapok
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentService paymentService;
@Transactional
public Order createOrder(OrderRequest request) {
Order order = new Order(request);
orderRepository.save(order);
paymentService.processPayment(order); // Ha itt exception → rollback
return order;
}
@Transactional(readOnly = true)
public Order getOrder(Long id) {
return orderRepository.findById(id)
.orElseThrow(() -> new OrderNotFoundException(id));
}
}
A readOnly = true jelzi a Hibernate-nek, hogy ne készítsen dirty checking snapshot-ot — ez teljesítménynövelés, és egyes adatbázisok (PostgreSQL) read-only tranzakciót indítanak.
Propagation — tranzakció terjedés
| Propagáció | Viselkedés |
|---|---|
| REQUIRED (alapértelmezett) | Meglévőhöz csatlakozik, vagy újat indít |
| REQUIRES_NEW | Mindig új tranzakciót indít, a meglévőt felfüggeszti |
| NESTED | Savepoint-tal beágyazott tranzakció |
| SUPPORTS | Tranzakcióban fut ha van, egyébként nélküle |
| NOT_SUPPORTED | Mindig tranzakció nélkül fut |
| MANDATORY | Meglévő tranzakció kötelező, egyébként exception |
| NEVER | Tranzakció nem lehet, egyébként exception |
@Transactional(propagation = Propagation.REQUIRED) // alapértelmezett
public void processOrder(Order order) { ... }
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void sendNotification(Order order) { ... }
@Transactional(propagation = Propagation.MANDATORY)
public void updateInventory(Order order) { ... }
Isolation — izolációs szintek
| Szint | Dirty Read | Non-Repeatable Read | Phantom Read |
|---|---|---|---|
| READ_UNCOMMITTED | ✅ Lehetséges | ✅ Lehetséges | ✅ Lehetséges |
| READ_COMMITTED | ❌ Védett | ✅ Lehetséges | ✅ Lehetséges |
| REPEATABLE_READ | ❌ Védett | ❌ Védett | ✅ Lehetséges |
| SERIALIZABLE | ❌ Védett | ❌ Védett | ❌ Védett |
@Transactional(isolation = Isolation.READ_COMMITTED)
public void transferMoney(Long fromId, Long toId, BigDecimal amount) { ... }
@Transactional(isolation = Isolation.SERIALIZABLE)
public void criticalOperation() { ... }
Az alapértelmezett izolációs szint adatbázis-specifikus (PostgreSQL: READ_COMMITTED, MySQL InnoDB: REPEATABLE_READ).
Rollback szabályok
// Alapértelmezett: RuntimeException → rollback, checked exception → NEM rollback
@Transactional
public void defaultRollback() { ... }
// Explicit rollback szabályok:
@Transactional(rollbackFor = Exception.class)
public void rollbackOnAnyException() { ... }
@Transactional(rollbackFor = PaymentException.class,
noRollbackFor = NotificationException.class)
public void selectiveRollback() { ... }
3. Gyakorlati használat
A proxy mechanizmus
A @Transactional egy AOP proxy-n keresztül működik. A Spring egy CGLIB proxy-t hoz létre a bean köré:
Caller → CGLIB Proxy → TransactionInterceptor → Target method
↓
begin TX / commit / rollback
A self-invocation csapda:
@Service
public class OrderService {
// ❌ ROSSZ: this.internalMethod() megkerüli a proxy-t!
@Transactional
public void process() {
internalMethod(); // NEM tranzakcionális!
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void internalMethod() {
// A REQUIRES_NEW NEM érvényesül!
}
}
Megoldások a self-invocation-re:
// 1. Külön service-be mozgatás
@Service
public class OrderService {
private final InternalOrderService internalService;
@Transactional
public void process() {
internalService.internalMethod(); // Proxy-n megy!
}
}
// 2. Self-injection (nem ideális, de működik)
@Service
public class OrderService {
@Lazy @Autowired
private OrderService self;
@Transactional
public void process() {
self.internalMethod(); // Proxy-n megy!
}
}
Tranzakciós template manuális vezérléshez
@Service
public class OrderService {
private final TransactionTemplate txTemplate;
public OrderService(PlatformTransactionManager txManager) {
this.txTemplate = new TransactionTemplate(txManager);
this.txTemplate.setIsolationLevel(
TransactionDefinition.ISOLATION_READ_COMMITTED);
}
public Order createOrder(OrderRequest request) {
return txTemplate.execute(status -> {
Order order = new Order(request);
orderRepository.save(order);
if (paymentFailed) {
status.setRollbackOnly();
}
return order;
});
}
}
Tesztelés tranzakcióval
@SpringBootTest
@Transactional // Minden teszt végén rollback!
class OrderServiceTest {
@Autowired
private OrderService orderService;
@Test
void shouldCreateOrder() {
Order order = orderService.createOrder(new OrderRequest(...));
assertNotNull(order.getId());
// A teszt végén automatikus rollback → DB nem piszkos
}
@Test
@Commit // Ha explicit commit kell (ritka)
void shouldPersistOrder() { ... }
}
4. Kód példák
REQUIRES_NEW — független tranzakció
@Service
public class AuditService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logAuditEvent(String event, String details) {
auditRepository.save(new AuditEvent(event, details));
// Ez MINDIG commit-olódik, akkor is ha a hívó tranzakció
// rollback-el — az audit log megmarad
}
}
@Service
public class OrderService {
private final AuditService auditService;
private final OrderRepository orderRepository;
@Transactional
public void processOrder(Order order) {
orderRepository.save(order);
auditService.logAuditEvent("ORDER_CREATED", order.getId().toString());
// Ha itt exception jön → order rollback, de az audit megmarad
riskyOperation();
}
}
Timeout és readOnly
@Transactional(timeout = 5) // 5 másodperc timeout
public void longRunningOperation() {
// Ha 5s-nél tovább tart → TransactionTimedOutException
}
@Transactional(readOnly = true)
public List<OrderDto> generateReport(ReportCriteria criteria) {
// readOnly:
// 1. Hibernate nem készít dirty checking snapshot-ot
// 2. Egyes DB-k read-only tranzakciót indítanak (optimalizáció)
// 3. Flush mode: MANUAL (nincs automatikus flush)
return orderRepository.findByCriteria(criteria)
.stream().map(OrderDto::from).toList();
}
Event-alapú tranzakciós pattern
@Service
public class OrderService {
private final ApplicationEventPublisher publisher;
@Transactional
public Order createOrder(OrderRequest request) {
Order order = orderRepository.save(new Order(request));
publisher.publishEvent(new OrderCreatedEvent(order));
return order;
}
}
@Component
public class OrderEventListener {
// Csak sikeres COMMIT után fut le:
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onOrderCreated(OrderCreatedEvent event) {
emailService.sendOrderConfirmation(event.getOrder());
}
// Rollback esetén fut:
@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
public void onOrderFailed(OrderCreatedEvent event) {
alertService.notifyOrderFailure(event.getOrder());
}
}
Programmatic rollback
@Transactional
public void processPayment(Payment payment) {
paymentRepository.save(payment);
if (!externalGateway.validate(payment)) {
// Programmatic rollback kérés:
TransactionAspectSupport.currentTransactionStatus()
.setRollbackOnly();
return;
}
externalGateway.charge(payment);
}
5. Trade-offok
| Előny | Hátrány |
|---|---|
| Deklaratív (@Transactional) — tiszta kód | Proxy-alapú → self-invocation csapda |
| ACID garancia automatikusan | Teljesítmény overhead (commit/rollback) |
| Propagation rugalmasság | REQUIRES_NEW: connection pool kimerülés kockázat |
| readOnly optimalizáció | Rejtett viselkedés (rollback szabályok) |
| @TransactionalEventListener | Izolációs szint + lock → deadlock kockázat |
Mikor deklaratív (@Transactional)
- Szerviz réteg metódusok (standard use case)
- CRUD műveletek, amelyeknél az ACID garancia fontos
- Read-only lekérdezések (readOnly = true)
Mikor programmatic (TransactionTemplate)
- Finom granularitású tranzakció-vezérlés
- Egy metóduson belül többszörös tranzakció
- Dinamikus izolációs szint
6. Gyakori hibák
❌ Self-invocation — proxy megkerülése
@Service
public class OrderService {
// ROSSZ: this.validate() megkerüli a proxy-t
@Transactional
public void process(Order order) {
validate(order); // NEM tranzakcionális!
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void validate(Order order) { ... }
}
Megoldás: Különálló bean-be helyezni, vagy self-injection.
❌ Checked exception nem görgeti vissza
// ROSSZ: IOException nem okoz rollback-et
@Transactional
public void importData(InputStream is) throws IOException {
// IOException (checked) → NEM rollback!
parseAndSave(is);
}
// JÓ: explicit rollbackFor
@Transactional(rollbackFor = Exception.class)
public void importData(InputStream is) throws IOException {
parseAndSave(is);
}
❌ @Transactional private metóduson
// ROSSZ: a CGLIB proxy nem tudja override-olni a private metódust
@Transactional
private void updateOrder(Order order) { ... } // Nincs hatás!
// JÓ: public metódus
@Transactional
public void updateOrder(Order order) { ... }
❌ REQUIRES_NEW túlzott használata
// ROSSZ: minden metódus REQUIRES_NEW → sok connection foglalás
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void step1() { ... }
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void step2() { ... }
// JÓ: REQUIRED (alapértelmezett) a legtöbb esetben elég
@Transactional
public void step1() { ... }
❌ Exception lenyomása catch-ben
// ROSSZ: a catch block megakadályozza a rollback-et
@Transactional
public void process() {
try {
riskyOperation();
} catch (RuntimeException e) {
log.error("Failed", e);
// A tranzakció NEM rollback-el, mert az exception nem propagálódik!
}
}
// JÓ: újra dobni vagy setRollbackOnly()
@Transactional
public void process() {
try {
riskyOperation();
} catch (RuntimeException e) {
log.error("Failed", e);
throw e; // Rollback megtörténik
}
}
7. Mélyebb összefüggések
A proxy mechanizmus teljes képe
- A Spring
BeanPostProcessordetektálja a@Transactionalannotációt - CGLIB proxy-t hoz létre a bean köré (osztályszintű proxy)
- A
TransactionInterceptormegkapja a metódushívást - Ellenőrzi a
@Transactionalattribútumokat (propagation, isolation, rollback) PlatformTransactionManager.getTransaction()— tranzakció indítás/csatlakozás- Target metódus meghívása
- Siker →
commit(), RuntimeException →rollback()
Propagation belső működése
Caller → REQUIRED → Csatlakozik a meglévő TX-hez VAGY újat indít
→ REQUIRES_NEW → Az aktuális TX-et felfüggeszti, új TX
→ NESTED → Savepoint az aktuális TX-ben
→ SUPPORTS → TX-ben fut ha van, egyébként nélküle
→ MANDATORY → Meglévő TX kötelező, egyébként exception
A REQUIRES_NEW két DB connection-t foglal: a felfüggesztett + az új tranzakció. Ha a pool mérete kicsi, deadlock vagy timeout lehet.
Isolation szintek részletesen
Dirty Read: Olvasod egy másik uncommitted tranzakció módosítását. Non-Repeatable Read: Kétszer olvasva ugyanazt a sort, más értéket kapsz (másik TX commit-olt). Phantom Read: Kétszer futtatva ugyanazt a query-t, más sorok jönnek vissza (másik TX INSERT-elt).
READ_UNCOMMITTED → Leggyorsabb, legkevésbé biztonságos
READ_COMMITTED → Legelterjedtebb (PostgreSQL default)
REPEATABLE_READ → MySQL InnoDB default
SERIALIZABLE → Leglassabb, legbiztonságosabb
@Transactional vs TransactionTemplate vs TransactionManager
| Megoldás | Típus | Mikor |
|---|---|---|
@Transactional |
Deklaratív | 90% az esetek — szerviz metódusok |
TransactionTemplate |
Programmatic | Finom granularitás, több TX egy metódusban |
PlatformTransactionManager |
Low-level | Saját keretrendszer / interceptor |
Spring Boot auto-konfigurált tranzakciókezelők
| Starter | TransactionManager |
|---|---|
spring-boot-starter-data-jpa |
JpaTransactionManager |
spring-boot-starter-jdbc |
DataSourceTransactionManager |
spring-boot-starter-data-r2dbc |
R2dbcTransactionManager |
8. Interjúkérdések
Hogyan működik a @Transactional annotáció? AOP proxy-n keresztül: a Spring CGLIB proxy-t hoz létre a bean köré, a TransactionInterceptor indítja/commit-olja/rollback-eli a tranzakciót a target metódus hívása körül.
Mi a self-invocation probléma és hogyan oldod meg? Ha egy @Transactional metódus ugyanazon osztályon belül hív egy másikat (
this.method()), az megkerüli a proxy-t — a tranzakciós annotáció nem érvényesül. Megoldás: külön bean-be mozgatni, vagy self-injection.Mi a REQUIRED és REQUIRES_NEW propagáció közötti különbség? REQUIRED: meglévő tranzakcióhoz csatlakozik vagy újat indít. REQUIRES_NEW: mindig új tranzakciót indít, a meglévőt felfüggeszti. REQUIRES_NEW két DB connection-t foglal.
Mikor NEM görgeti vissza a @Transactional a tranzakciót? Alapértelmezetten checked exception esetén NEM rollback. Csak RuntimeException és Error okoz rollback-et. Explicit:
rollbackFor = Exception.class.Mi a readOnly = true hatása? Hibernate nem készít dirty checking snapshot-ot, flush mode MANUAL-ra áll. Egyes adatbázisok read-only tranzakciót indítanak (PostgreSQL optimalizáció).
Mire használod a @TransactionalEventListener-t? Eseménykezelés a tranzakció fázisaihoz kötve: AFTER_COMMIT (csak sikeres commit után), AFTER_ROLLBACK, BEFORE_COMMIT. Tipikus: email küldés, értesítés.
Miért nem működik a @Transactional private metóduson? A CGLIB proxy nem tudja override-olni a private metódust. A tranzakciós interceptor csak public metódusokon érvényesül.
9. Szószedet
| Fogalom | Jelentés |
|---|---|
| @Transactional | Deklaratív tranzakciókezelő annotáció |
| Propagation | Tranzakció terjedési viselkedés (REQUIRED, REQUIRES_NEW, stb.) |
| Isolation | Párhuzamos tranzakciók elkülönítési szintje |
| Rollback | Tranzakció visszagörgetése hiba esetén |
| ACID | Atomicity, Consistency, Isolation, Durability |
| Self-invocation | Proxy megkerülése belső metódushívással |
| PlatformTransactionManager | Spring tranzakciós API absztrakció |
| TransactionTemplate | Programmatic tranzakciókezelő helper |
| readOnly | Jelzés az ORM-nek és DB-nek, hogy nem lesz írás |
| Savepoint | NESTED propagáció belső mentési pontja |
| @TransactionalEventListener | Tranzakció fázisához kötött eseménykezelő |
| CGLIB Proxy | Osztályszintű proxy, amely a @Transactional-t implementálja |
10. Gyorsreferencia
@TRANSACTIONAL ATTRIBÚTUMOK:
propagation REQUIRED (default), REQUIRES_NEW, NESTED, SUPPORTS
isolation DEFAULT, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE
readOnly true/false (default: false)
timeout másodpercben (default: -1 = nincs)
rollbackFor Exception osztály(ok) → rollback
noRollbackFor Exception osztály(ok) → NEM rollback
ROLLBACK SZABÁLYOK:
RuntimeException → rollback (alapértelmezett)
Checked Exception → NEM rollback (alapértelmezett)
rollbackFor = Exception.class → MINDEN exception rollback
PROPAGATION:
REQUIRED meglévőhöz csatlakozik VAGY újat indít
REQUIRES_NEW mindig új TX, meglévő felfüggesztve
NESTED savepoint a meglévő TX-ben
MANDATORY meglévő TX kötelező, egyébként exception
SELF-INVOCATION:
this.method() megkerüli a proxy-t → ❌
injectedSelf.method() proxy-n megy → ✅
separateBean.method() proxy-n megy → ✅
TESZTELÉS:
@Transactional a teszten → automatikus rollback
@Commit → explicit commit (ritka)
EVENT LISTENER:
@TransactionalEventListener(phase = AFTER_COMMIT)
@TransactionalEventListener(phase = AFTER_ROLLBACK)
@TransactionalEventListener(phase = BEFORE_COMMIT)
🎮 Játékok
10 kérdés