Postgres transaction'larını çok adımlı iş akışları için öğrenin: güncellemeleri güvenle nasıl gruplayacağınız, kısmi yazmaları nasıl önleyeceğiniz, retry'leri nasıl ele alacağınız ve veriyi nasıl tutarlı tutacağınız.

Çoğu gerçek özellik tek bir veritabanı güncellemesi değildir. Bunlar kısa bir zincirdir: bir satır ekle, bir bakiyeyi güncelle, bir durumu işaretle, bir denetim kaydı yaz, belki bir işi kuyruğa al. Kısmi yazma, bu adımlardan sadece bazıları veritabanına ulaşınca ortaya çıkar.
Bu durum zinciri bir şey kestiğinde görülür: bir sunucu hatası, uygulama ile Postgres arasındaki zaman aşımı, adım 2'den sonra bir çökme veya adım 1'i tekrar çalıştıran bir retry. Her ifade kendi başına doğrudur. İş akışı yarıda durduğunda işler bozulur.
Genellikle bunu hızlıca fark edebilirsiniz:
Somut bir örnek: bir plan yükseltmesi müşterinin planını günceller, bir ödeme kaydı ekler ve kullanılabilir kredileri artırır. Uygulama ödeme kaydını sakladıktan sonra kredileri eklemeden çökerse destek tarafı bir tabloda "ödendi" diğerinde "kredi yok" görür. İstemci retry yaparsa ödemeyi iki kez kaydedebilirsiniz.
Amaç basit: iş akışını tek bir anahtar gibi ele alın. Ya her adım başarılı olur, ya hiçbiri olmaz; böylece yarım kalmış iş saklamazsınız.
Transaction, veritabanının şu demesinin yoludur: bu adımları tek bir iş birimi gibi ele al. Ya tüm değişiklikler olur, ya hiçbiri. Bu, birden fazla güncelleme gerektiren iş akışlarının tamamında önemlidir: bir satır oluşturma, bakiye güncelleme, denetim kaydı yazma gibi.
Parayı bir hesaptan diğerine taşımayı düşünün. Hesaptan düşmeli ve diğerine eklemelisiniz. Uygulama ilk adımın ardından çökerse, sistem sadece düşmeyi "hatırlamamalıdır".
Commit yaptığınızda Postgres'e şöyle dersiniz: bu transaction'da yaptığım her şeyi tut. Tüm değişiklikler kalıcı olur ve diğer oturumlardan görünür.
Rollback yaptığınızda Postgres'e şöyle dersiniz: bu transaction'da yaptığım her şeyi unut. Postgres değişiklikleri transaction hiç olmamış gibi geri alır.
Bir transaction içinde, Postgres commit etmeden önce yarım kalmış sonuçları diğer oturumlara göstermeyeceğini garanti eder. Bir şey başarısız olup rollback yapılırsa veritabanı o transaction'ın yazımlarını temizler.
Transaction kötü iş akışı tasarımını düzeltmez. Yanlış miktarı çıkartırsanız, yanlış kullanıcı kimliğini kullanırsanız veya gerekli bir kontrolü atladıysanız Postgres yanlış sonucu olduğu gibi commit eder. Transaction'lar ayrıca doğru kısıtlar, kilitler veya izolasyon seviyesi eşlik etmedikçe her iş düzeyi çatışmayı (örneğin stok fazlası satışı) otomatik olarak engellemez.
Gerçek dünya eylemini tamamlamak için birden fazla tabloyu (veya birden fazla satırı) güncellediğiniz her durumda transaction adayı vardır. Nokta aynı: ya her şey yapılmalı, ya hiçbiri.
Sipariş akışı klasik örnektir. Bir sipariş satırı oluşturabilir, stoğu rezerve edebilir, ödeme alabilir, sonra siparişi "ödendi" olarak işaretleyebilirsiniz. Ödeme başarılı ama durum güncellemesi başarısız olursa paranız alınır ama sipariş hâlâ ödenmemiş görünür. Sipariş satırı oluşturulmuş ama stok rezerve edilmemişse satışını yaptığınız ürünler aslında yok olabilir.
Kullanıcı kaydı da sessizce aynı şekilde bozulur. Kullanıcıyı oluşturmak, profil kaydı eklemek, rolleri atamak ve bir karşılama e-postasının gönderilmesi gerektiğini kaydetmek tek bir mantıksal eylemdir. Gruplanmazsa, oturum açabilen ama izinleri olmayan bir kullanıcı veya kullanıcı olmayan bir profile sahip bir kayıt ortaya çıkabilir.
Back-office işlemleri genellikle sıkı "denetim izi + durum değişikliği" davranışı gerektirir. Bir isteği onaylamak, denetim kaydı yazmak ve bakiyeyi güncellemek birlikte başarılı olmalıdır. Bakiye değişir ama denetim kaydı eksikse kim neyi, neden değiştirdiğini gösteren kanıtı kaybedersiniz.
Arka plan işleri de fayda sağlar; özellikle bir iş öğesini işlerken birden fazla adım varsa: öğeyi sahiplen, işçi çatışmasını önle, iş güncellemesini uygula, raporlama ve retry için sonucu kaydet, sonra öğeyi tamamlandı veya başarısız (sebep ile) olarak işaretle. Bu adımlar ayrılırsa retry'ler ve eşzamanlılık karmaşa yaratır.
Çok adımlı özellikler, onları bağımsız güncellemeler yığını gibi ele aldığınızda kırılır. Bir veritabanı istemcisi açmadan önce iş akışını tek bir kısa hikaye olarak yazın: kullanıcı için "tamam" sayılacak kesin bitiş çizgisi nedir?
Önce adımları yalın dilde listeleyin, sonra tek bir başarı koşulu tanımlayın. Örneğin: "Sipariş oluşturuldu, stok rezerve edildi ve kullanıcı bir sipariş onay numarası görüyor." Bu koşulun altında kalan her şey başarısızlıktır, tablolardan bazıları güncellenmiş olsa bile.
Sonra veritabanı işi ile dış iş arasına sert bir çizgi çizin. Veritabanı adımları transaction ile koruyabileceğiniz adımlardır. Kart ödemeleri, e-posta gönderimleri veya üçüncü taraf API çağrıları gibi dış çağrılar yavaş ve tahmin edilemez şekilde başarısız olabilir ve genellikle geri alınamaz.
Basit bir planlama yaklaşımı: adımları (1) tamamen birlikte olması gereken, (2) commit'ten sonra olabilecek olarak ayırın.
Transaction içinde birlikte tutmanız gereken adımları sadece şunlarla sınırlayın:
Yan etkileri dışarı taşıyın. Örneğin, önce siparişi commit edin, sonra outbox kaydına göre onay e-postasını gönderin.
Her adım için, bir sonraki adım başarısız olursa ne olması gerektiğini yazın. "Rollback" ya veritabanı rollback'i anlamına gelebilir, ya da telafi edici bir işlem anlamına.
Örnek: ödeme başarılı ama stok rezerve edilemediyse, önceden geri ödeme yapmaya mı karar vereceksiniz, yoksa siparişi "ödeme alındı, stok bekleniyor" olarak mı işaretleyip bunu asenkron olarak mı halledeceksiniz?
Transaction, Postgres'e şu demek için kullanılan bir yoldur: bu adımları bir birim olarak ele al. Ya hepsi olur, ya hiçbiri. Bu, kısmi yazmaları önlemenin en basit yoludur.
Başlangıçtan bitişe tek bir veritabanı bağlantısı (tek oturum) kullanın. Adımları farklı bağlantılara yayarsanız Postgres tümüyle ya hep ya hiç sonucunu garanti edemez.
Sıra basittir: BEGIN, gerekli okuma ve yazmaları çalıştırın, her şey başarılıysa COMMIT, aksi halde ROLLBACK yapın ve net bir hata döndürün.
İşte SQL ile minimal bir örnek:
BEGIN;
-- reads that inform your decision
SELECT balance FROM accounts WHERE id = 42 FOR UPDATE;
-- writes that must stay together
UPDATE accounts SET balance = balance - 50 WHERE id = 42;
INSERT INTO ledger(account_id, amount, note) VALUES (42, -50, 'Purchase');
COMMIT;
-- on error (in code), run:
-- ROLLBACK;
Transaction'lar çalışırken kilit tutar. Ne kadar uzun açık tutarsanız, diğer işleri o kadar engellersiniz ve zaman aşımı veya deadlock yaşama olasılığınız artar. Transaction içinde gerekli olanı yapın ve yavaş işleri (e-posta gönderimi, ödeme sağlayıcı çağrıları, PDF üretimi) dışarı taşıyın.
Bir şey başarısız olduğunda, hassas verileri sızdırmadan sorunu yeniden üretebilecek yeterli bağlamı loglayın: iş akışı adı, order_id veya user_id, ana parametreler (tutar, para birimi) ve Postgres hata kodu. Tam payload'ları, kart verilerini veya kişisel detayları loglamaktan kaçının.
Eşzamanlılık, aynı anda iki şeyin gerçekleşmesidir. Son konser bileti için iki müşteri aynı anda ödeme yapmaya çalışsın. Her iki ekranda da "1 kaldı" görünür, ikisi de "Öde"ye basar ve şimdi uygulamanız kime vereceğine karar vermeli.
Koruma olmadan, iki istek aynı eski değeri okuyup her ikisi de güncelleme yazabilir. Böylece negatif stok, çoğaltılmış rezervasyonlar veya siparişi olmayan bir ödeme ortaya çıkar.
Satır kilitleri en basit korumadır. Değiştireceğiniz satırı kilitlersiniz, kontrolleri yapar, sonra güncellersiniz. Aynı satıra dokunan diğer transaction'lar sizin commit veya rollback yapmanıza kadar bekler, bu da çift güncellemeyi engeller.
Yaygın bir desen: bir transaction başlatın, stok satırını FOR UPDATE ile seçin, stok olup olmadığını doğrulayın, azaltın, sonra siparişi insert edin. Bu kritik adımları tamamlarken "kapıyı tutar."
İzolasyon seviyeleri, eşzamanlı transaction'ların ne kadar çakışmasına izin verdiğinizi kontrol eder. Genel olarak güvenlik ile hız arasında bir takas vardır:
Kilitleri kısa tutun. Bir transaction dış API çağrısı yaparken veya kullanıcı aksiyonunu beklerken açık oturursa uzun beklemeler ve zaman aşımı olur. Açık bir başarısızlık yolu tercih edin: bir lock timeout ayarlayın, hatayı yakalayın ve istekleri asla takılmaya bırakmayıp "lütfen tekrar deneyin" döndürün.
Veritabanı dışında iş yapmanız gerekiyorsa (örneğin kart çekme), iş akışını bölün: hızlıca rezerve edin, commit edin, sonra yavaş kısmı yapın ve kısa başka bir transaction ile sonlandırın.
Retry'ler Postgres tabanlı uygulamalarda normaldir. Bir istek doğru kodla bile başarısız olabilir: deadlock'lar, statement timeout'lar, kısa ağ kopmaları veya daha yüksek izolasyon seviyelerinde serialization error. Aynı handler'ı tekrar çalıştırırsanız ikinci bir sipariş oluşturma, iki kez ücretlendirme veya duplicate "event" satırları riski vardır.
Çözüm idempotency: aynı girdiyle işlemi iki kez çalıştırmak güvenli olmalı. Veritabanı bu isteğin "aynı" istek olduğunu tanıyabilmeli ve tutarlı cevap vermeli.
Pratik bir desen: her çok adımlı iş akışına bir idempotency anahtarı (çoğunlukla istemci tarafından oluşturulan request_id) ekleyin ve bunu ana kayda depolayın, sonra bu anahtar için unique constraint koyun.
Örneğin: checkout'ta kullanıcı Pay'e tıkladığında request_id oluşturun, sonra siparişi o request_id ile insert edin. Retry olursa ikinci deneme unique kısıtı vurur ve yeni bir tane oluşturmak yerine mevcut siparişi döndürürsünüz.
Genellikle önemli olanlar:
Retry döngüsünü transaction dışına taşıyın. Her deneme yeni bir transaction ile başlamalı ve iş birimini baştan tekrar çalıştırmalı. Başarısız bir transaction içinde retry yapmak işe yaramaz çünkü Postgres o transaction'ı aborted olarak işaretler.
Küçük bir örnek: uygulamanız sipariş oluşturup stoğu rezerve etmeye çalışır ama COMMIT'ten hemen sonra zaman aşımı olur. İstemci retry yapar. Idempotency anahtarı ile ikinci istek zaten oluşturulmuş siparişi döndürür ve ikinci bir rezervasyon yapmaz.
Transaction'lar çok adımlı iş akışını bir arada tutar, ama veriyi otomatik olarak doğru yapmaz. Hata olursa bile "yanlış" durumları veritabanında zorlamak veya imkânsız kılmak, uygulama kodunda bir hata olsa bile kısmi yazma etkilerini azaltmanın güçlü bir yoludur.
Temel güvenlik bariyerleri ile başlayın. Foreign key'ler referansların gerçek olmasını sağlar (sipariş satırı eksik bir siparişe işaret edemez). NOT NULL yarım satırları durdurur. CHECK kısıtları mantığa aykırı değerleri yakalar (örneğin quantity > 0, total_cents >= 0). Bu kurallar her yazmada çalışır, hangi servis veya script veritabanına dokunsa.
Daha uzun iş akışları için durum değişikliklerini açıkça modelleyin. Birçok boolean flag yerine tek bir status sütunu (pending, paid, shipped, canceled) kullanın ve yalnızca geçerli geçişlere izin verin. Bunu kısıtlar veya trigger'larla enforce edebilir, veritabanının illegal geçişleri reddetmesini sağlayabilirsiniz (ör. shipped -> pending gibi).
Benzersizliği başka bir doğruluk biçimi olarak kullanın. Duplicate'lar iş akışınızı bozacaksa unique constraint ekleyin: order_number, invoice_number veya retry'ler için kullanılan idempotency_key. Eğer uygulamanız aynı isteği tekrar gönderirse Postgres ikinci insert'i engeller ve siz "zaten işlendi" döndürerek güvenle devam edersiniz.
İzlenebilirlik gerektiğinde bunu açıkça depolayın. Kim neyi ne zaman değiştirdiğini kaydeden bir audit (veya history) tablosu, "esrarengiz güncellemeleri" olay sırasında sorgulanabilir olgulara çevirir.
Çoğu kısmi yazma "kötü SQL" yüzünden değil. Onları kolaylaştıran iş akışı kararları yüzünden ortaya çıkar.
accounts sonra orders güncellerken, başka bir istek tam tersi sırayı kullanıyorsa yük altında deadlock ihtimalini artırırsınız.Somut örnek: checkout sırasında stoğu rezerve eder, siparişi oluşturur, sonra kartı çalarsınız. Kartı transaction içinde çalarsanız, ağ beklerken stok kilidini tutuyor olabilirsiniz. Eğer çekim başarılı ama transaction daha sonra rollback olursa müşteriyi karttan çektiniz ama sipariş yok.
Daha güvenli bir desen: transaction içinde sadece veritabanı durumuna odaklanın (stok rezerve et, sipariş oluştur, ödeme girişini pending olarak kaydet), commit edin, sonra dış API'yi çağırın ve sonucu yeni kısa bir transaction ile geri yazın. Birçok ekip bunu pending durumu ve arka plan job ile uygular.
Bir iş akışı birden fazla adım içerdiğinde (insert, update, charge, send), amaç basittir: ya her şey kaydedilsin, ya hiçbiri.
Gerekli tüm veritabanı yazmalarını tek bir transaction içinde tutun. Bir adım başarısız olursa rollback yapın ve veriyi tam olarak öncekine döndürün.
Başarı koşulunu açıkça belirtin. Örneğin: "Sipariş oluşturuldu, stok rezerve edildi ve ödeme durumu kaydedildi." Buın dışındaki her şey hata yoludur ve transaction'ı abort etmelidir.
BEGIN ... COMMIT bloğu içinde olmalı.ROLLBACK ile sonuçlanmalı ve çağırana net bir başarısızlık döndürülmeli.Aynı isteğin tekrar edilebileceğini varsayın. Veritabanı sadece-bir-kez kurallarını uygulamanıza yardımcı olmalı.
Transaction içinde minimum işi yapın, ağ çağrıları sırasında kilit tutmaktan kaçının.
Nerede kırıldığını göremezseniz tahmin etmeye devam edersiniz.
Bir checkout birkaç adımı birlikte hareket ettirmelidir: siparişi oluştur, stoğu rezerve et, ödeme denemesini kaydet, sonra sipariş durumunu işaretle.
Kullanıcı 1 adet ürün için Satın Al'a tıkladığını düşünün.
Tek bir transaction içinde yalnızca veritabanı değişikliklerini yapın:
orders tablosuna pending_payment durumuyla bir satır ekleyin.inventory.available'ı azaltın veya bir reservations satırı oluşturun).idempotency_key (unique) ile bir payment_intents satırı ekleyin.outbox satırı ekleyin.Herhangi bir ifade başarısız olursa (stok yok, kısıt hatası, çökme), Postgres tüm transaction'ı rollback eder. Sipariş ama rezervasyon yok; veya rezervasyon ama sipariş yok gibi durumlar oluşmaz.
Ödeme sağlayıcı veritabanınızın dışında olduğu için onu ayrı bir adım olarak ele alın.
Sağlayıcı çağrısı commit'ten önce başarısız olursa transaction'ı iptal edin ve hiçbir şey yazılmasın. Sağlayıcı çağrısı commit'ten sonra başarısız olursa yeni bir transaction ile ödeme denemesini başarısız olarak işaretleyin, rezervasyonu serbest bırakın ve siparişi iptal edilmiş olarak ayarlayın.
İstemcinin her checkout denemesi için bir idempotency_key göndermesini sağlayın. Bunu payment_intents(idempotency_key) üzerinde unique indeks ile zorlayın (veya isterseniz orders üzerinde). Retry durumunda kod mevcut satırları bulup devam eder; yeni sipariş oluşturmaz.
E-postaları transaction içinde göndermeyin. Aynı transaction içinde bir outbox kaydı yazın, commit'ten sonra arka plan worker e-postayı göndersin. Böylece rollback olan bir sipariş için asla e-posta gönderilmez.
Bir haftada birden fazla tabloya dokunan bir iş akışı seçin: kayıt + karşılama e-postası kuyruğa alma, checkout + stok, fatura + defter kaydı, veya proje oluşturma + varsayılan ayarlar.
Önce adımları yazın, sonra her zaman doğru olması gereken kuralları (invariants) tanımlayın. Örnek: "Bir sipariş ya tamamen ödeli ve rezerve edilmiş olur, ya ödenmemiş ve rezerve edilmemiş. Asla yarım rezerve olmaz." Bu kuralları tek bir ya hep ya hiç birimine çevirin.
Basit bir plan:
Sonra kasıtlı olarak çirkin durumları test edin. Adım 2'den sonra çökme simüle edin, commit'ten hemen önce zaman aşımı simüle edin, UI'dan çift gönderim yapın. Hedef sıkıcı sonuçlar: yetim satırlar yok, iki kat ücretlendirme yok, sonsuza kadar bekleyen pending yok.
Hızlı prototipte, handler'lar ve şema üretmeden önce iş akışını planlama-öncelikli bir araçla taslak çıkarmak yardımcı olabilir. Örneğin, Koder.ai (koder.ai) Planning Mode'a sahip ve snapshot ve rollback destekliyor; transaction sınırları ve kısıtlar üzerinde iterasyon yaparken kullanışlı olabilir.
Bu hafta bir iş akışı için bunu yapın. İkinci olan çok daha hızlı olacaktır.