Haladó Olvasási idő: ~10 perc

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

  1. A Spring BeanPostProcessor detektálja a @Transactional annotációt
  2. CGLIB proxy-t hoz létre a bean köré (osztályszintű proxy)
  3. A TransactionInterceptor megkapja a metódushívást
  4. Ellenőrzi a @Transactional attribútumokat (propagation, isolation, rollback)
  5. PlatformTransactionManager.getTransaction() — tranzakció indítás/csatlakozás
  6. Target metódus meghívása
  7. 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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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ó).

  6. 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.

  7. 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