Transaction Propagation ve Rollback Yayılımı
30 saniyede özet
Bir metot patladı, sen hatayı yakalayıp devam ettin; ama sipariş yine de kaydedilmedi. Çünkü içteki metot ayrı bir transaction açmamış, seninkine katılmış ve onu 'geri alınacak' diye işaretlemişti.
İki kişi aynı alışveriş sepetini taşıyor. Biri sepete kırık bir yumurta koyunca kasiyer bütün sepeti geri çeviriyor.
Adım adım oku
- REQUIRED ile stok ayıran içteki metot, siparişin açtığı transaction'a, yani aynı sepete katılır.
- İçteki metot patlar ve paylaşılan sepete geri alınacak damgası vurulur.
- Dıştaki metot hatayı yakalayıp devam etse de commit anında damga görülür ve sepetin tamamı geri alınır.
- REQUIRES_NEW ile içteki metot kendi sepetini açar. Yalnızca o geri alınır, sipariş kaydedilir.
-
Bayt: Stok ayırma patlarsa hatayı yakalayıp siparişi yine de kaydedeceğim. Mantıklı, değil mi?
-
Sen: Kulağa mantıklı geliyor.
-
Bayt: Ama sipariş hiç kaydolmadı, üstüne bir de UnexpectedRollbackException geldi!
-
Bayt: İki metot aynı sepeti paylaşıyorsa, sepete vurulan damga ikisini de bağlar.
Kodda sipariş kaydediliyor; stok ayırma patlarsa hata yakalanıyor ve sipariş yine de kaydedilmek isteniyor. Ama bu kod tam tersini yapar:
@Transactionalpublic void placeOrder(Order order) { orderRepository.save(order);
try { inventoryService.reserve(order); // @Transactional, patlıyor } catch (RuntimeException e) { log.warn("Stok rezerve edilemedi, siparişi yine de kaydet", e); }}Exception yakalandı, akış devam etti, metot sorunsuz döndü. Ama placeOrder çağıran kod UnexpectedRollbackException alır ve sipariş de kaydedilmez.
Sipariş neden kaydolmadı?
placeOrder() içinde çağrılan reserve() bir hata fırlattı. Hatayı try/catch ile yakalayıp devam ettin. placeOrder() bitince sipariş kaydolur mu? Cevabı göster
Hayır. Paylaşılan transaction geri alınmak üzere işaretlendi; commit anında UnexpectedRollbackException gelir.
reserve() metodu da @Transactional ve propagationAçık bir transaction varken yeni bir `@Transactional` metodun ne yapacağını belirleyen ayar. Varsayılan `REQUIRED` var olana katılır, yeni açmaz.Sözlükte gör → ayarı varsayılan olan REQUIRED. REQUIRED yeni bir transaction açmaz: ortada zaten bir tane varsa, ona katılır.
Yani kodda iki @Transactional var ama veritabanında tek bir transaction açık, tıpkı iki kişinin aynı alışveriş sepetini doldurması gibi. reserve() patladığında Spring bu ortak sepete “geri alınacak” (rollback-only) etiketi yapıştırır ve bu etiket bir daha sökülmez.
Dışarıda hatayı yakalasan bile, kaydetme anında Spring “bu sepet rollback-onlyTransaction'a konan "bu artık commit edilemez" bayrağı. İç bir çağrıda istisna olduğunda konur; sen istisnayı yutsan bile dıştaki commit patlar.Sözlükte gör →-only işaretli, kaydedemem” der ve UnexpectedRollbackException fırlatır.
Kafam karıştı, daha basit anlat
İki kişi aynı alışveriş sepetini dolduruyor. Biri “bu sepet iptal” derse, ötekinin koyduğu ürünler de sepetle birlikte gider.
`REQUIRED` (varsayılan) propagation ne yapar?
Kendin gör
Varsayılan ayar tam olarak yukarıdaki kodu modelliyor. Adımla ve TX#1’in ne zaman rollback-only olduğuna bak.
@Transactional propagation ve rollback yayılımı
Tohum 1Çağrı zinciri
OrderService.placeOrder()REQUIREDInventoryService.reserve()REQUIRED
Fiziksel transaction’lar (0)
Henüz transaction açılmadı.
Kalıcı olan işler
— hiçbiri —
Şu an ne oldu?
Transaction açıldı
Dış metot @Transactional olduğu için Spring bir proxy üzerinden çağrıyı sarmalayıp transaction başlattı.
Aklında kalsın: Proxy detayı önemlidir: aynı sınıf içinden this.method() çağrısı proxy’den geçmez, bu yüzden @Transactional hiç çalışmaz.
Görevler0/3
UnexpectedRollbackException ile karşılaşaçık
İpucu
Varsayılan ayarlarla sonuna kadar ilerle. İstisnayı yakaladın, ama transaction yine de rollback-only işaretlendi.
İç metot patlasın, dış işin yine de commit olsun (REQUIRES_NEW)açık
İpucu
İç metodu REQUIRES_NEW yap ve istisnayı dışarıda yakala. İç iş kendi transaction’ında geri alınır, dışarısı etkilenmez.
Aynı sonucu tek fiziksel transaction ile al (NESTED)açık
İpucu
NESTED savepoint kullanır: iç iş savepoint’e geri sarılır, bağlantı ve transaction aynı kalır.
Olay günlüğü (0)
Henüz olay yok. Oynat veya adımla.
Üç ayarı da karşılaştır:
REQUIRED+ yakala (varsayılan) — tek fiziksel transaction,rollback-only, sonundaUnexpectedRollbackException. Hiçbir şey kaydedilmiyor.REQUIRES_NEW+ yakala — iki fiziksel transaction.TX#2geri alınıyor,TX#1temiz kalıyor ve commit oluyor. Sipariş kaydediliyor.NESTED+ yakala — tek transaction ama savepoint’li. İç metodun işi geri sarılıyor, dış metodunki korunuyor.
Bir de yakalamayı kapat: exception dışarı çıkıyor, her şey geri alınıyor. Bu zaten beklenen davranış ve en az sürpriz olanı.
Simülatörün altındaki Görevler listesi üç ayarı da tek tek denetiyor. Aynı sonuca iki ayrı yoldan ulaşmak, REQUIRES_NEW ile NESTED arasındaki farkı en net gösteren şey.
Kafam karıştı, daha basit anlat
İçerideki metot hata verince ortak sepete “iptal edilecek” etiketi yapışır. Dıştaki metot hatayı yakalasa bile etiket yerinde durur.
`@Transactional` bir metot normal biterse ve `RuntimeException` atarsa ne olur?
Üç ayar, üç farklı sonuç
| Fiziksel TX | Bağlantı | İç hata dışarıyı bozar mı | |
|---|---|---|---|
REQUIRED | 1 (katılır) | 1 | Evet — rollback-only |
REQUIRES_NEW | 2 | 2 | Hayır |
NESTED | 1 (savepoint) | 1 | Hayır |
Bu tablodaki bağlantı sütunu en çok atlanan yerdir.
NESTED bu sorunu yaşamaz çünkü aynı bağlantıyı kullanır. Karşılığında savepoint desteği ister: JDBC’de sorunsuz, JPA/Hibernate ile her zaman güvenilir değildir ve bazı veritabanlarında desteklenmez.
Diğer propagation değerleri· istersen atla
Günlük kodda ilk üçü yeter; geri kalanlar şunlar:
SUPPORTS— transaction varsa katılır, yoksa transaction’sız çalışır.MANDATORY— transaction yoksa exception. “Beni asla tek başıma çağırma” demek.NEVER— transaction varsa exception.NOT_SUPPORTED— varsa askıya alır, transaction’sız çalışır.
Kafam karıştı, daha basit anlat
REQUIRES_NEW ayrı bir sepet açar, ama ilk sepet de kasada beklemeye devam eder. Çok kişi aynı anda bunu yaparsa kasalar dolar.
Her propagation seviyesini, açık bir transaction varken ne yaptığına göre ayır.
Tuzaklar
1. Sınıfın kendi metodunu çağırması. @Transactional, sınıfın önüne konan bir aracı (proxySpring'in bean'in yerine koyduğu sarmalayıcı nesne. `@Transactional` gibi anotasyonlar burada çalışır — çağrı proxy'den geçmezse hiçbir şey olmaz.Sözlükte gör →) sayesinde çalışır. Aynı sınıfın içinden çağırdığın metot bu aracıdan geçmez:
@Servicepublic class OrderService {
public void placeOrder(Order order) { this.saveInTransaction(order); // proxy atlandı — @Transactional YOK SAYILDI }
@Transactional public void saveInTransaction(Order order) { ... }}
// Çözüm: çağrıyı başka bir bean'e taşı, veya self-injection yapAynı sebeple private, final ve static metotlarda @Transactional çalışmaz — proxy onları sarmalayamaz.
2. Checked exception rollback yapmaz. Spring varsayılan olarak yalnızca RuntimeException ve Error için geri alır:
@Transactional // checked exception'da rollback YOK — işlem commit olurpublic void transfer(Account from, Account to) throws InsufficientFundsException { debit(from); credit(to); // burada checked exception atarsa debit kalıcı olur}
// Çözüm:@Transactional(rollbackFor = Exception.class)`disaMetot()` çağrıldığında veritabanına kaç kayıt yazılır?
`@Transactional` bir metot içinde yakalanan bir `RuntimeException` sonrası akış devam ediyor. Commit olur mu?
Ne zaman hangisi
- Varsayılan
REQUIRED— çağrı zincirinin tamamı tek bir atomik iş olmalıysa. Çoğu durum budur. REQUIRES_NEW— işlem başarısız olsa bile kalıcı olması gereken şeyler için: denetim kaydı (audit log), başarısızlık sayacı, bildirim kuyruğuna ekleme.NESTED— bir alt işlemin başarısızlığını tolere etmek istiyorsan ve ikinci bir bağlantı ödemek istemiyorsan.
Aşağıdaki örnek bir bankanın ödeme servisinden. Üç propagation kararı bir arada: denetim izi geri alınmamalı, ücretsiz işlem hakkı kontrolü havaleyi öldürmemeli, toplu maaş dosyasında hatalı bir satır diğer maaşları geri almamalı.
Derinleş · Bankada propagation: havale, denetim izi ve toplu maaş ödemesi 6 dosya · ~147 satır · ilk okumada atlayabilirsin
Kendini sına
REQUIRED, var olan bir transaction varsa ona katılır.
Dış metot @Transactional. İç metot da @Transactional (REQUIRED) ve RuntimeException fırlatıyor. Dış metot bu exception'ı try/catch ile yakalayıp devam ediyor. Sonuç ne olur?
Aklında kalacak üç şey
- 1 REQUIRED yeni bir transaction açmaz, var olana katılır. Kodda iki @Transactional görmek, veritabanında iki transaction olduğu anlamına gelmez.
- 2 İç metot patlayınca ortak transaction 'yalnızca geri alınabilir' diye işaretlenir; hatayı dışarıda yakalamak bu işareti silmez. Çözüm REQUIRES_NEW ya da NESTED'dir.
- 3 İki tuzak daha: this.metot() çağrısında @Transactional hiç çalışmaz, ve Spring varsayılan olarak yalnızca RuntimeException'da geri alır.
5 kart sonraki derste seni bekliyor
Bu dersin üstüne kurulanlar
Bunlar bu dersi temel alıyor; hazır olduğunda devam edebilirsin.