Veritabanı işlemlerinin yalıtım seviyeleri, genellikle yazılım geliştirmeye yeni başlayanların “detay” olarak gördüğü, ancak tecrübelendikçe ne kadar kritik olduğunu anladığım bir konu oldu. Yıllar içinde, bu basit görünen ayarın, bir uygulamanın veri bütünlüğünden performansına, hatta iş süreçlerinin doğruluğuna kadar nasıl derinlemesine etki ettiğini defalarca tecrübe ettim. Bu yazı, bana göre neden bu kadar önemli olduğunu ve kariyerimde hangi noktalarda karşıma çıktığını anlatacak.
Bir üretim ERP’sinde çalışırken, sevkiyat listeleriyle ilgili tutarsızlıklar yaşıyorduk. Raporu gün içinde birkaç kez çektiklerinde farklı toplamlar görünüyor, bu da büyük bir kafa karışıklığına neden oluyordu. İlk başta uygulama katmanında bir bug aradım, ancak root cause aslında veritabanının varsayılan işlem yalıtım seviyesinden ve eş zamanlı çalışan diğer işlemlerden kaynaklanıyordu. Bu tür ince detaylar, genelde kimsenin aklına gelmez ama sistemin kalbinde yatan sorunları çözmek için anahtar olabilir.
Veritabanı İşlem Yalıtım Seviyeleri: Temel Bir Bakış
İşlem yalıtım seviyeleri (Transaction Isolation Levels), veritabanındaki eş zamanlı işlemlerin birbirini ne kadar etkileyeceğini belirler. ANSI SQL standardı dört ana seviye tanımlar: Read Uncommitted, Read Committed, Repeatable Read ve Serializable. Ancak her veritabanı sistemi, bu seviyeleri kendi iç mimarisine göre farklı şekillerde uygular. Örneğin, PostgreSQL Read Uncommitted seviyesini desteklemez ve Read Committed varsayılanıdır.
Benim deneyimimde, bu seviyeler sadece teorik tanımlar değil, doğrudan uygulamanın güvenilirliğini ve veri bütünlüğünü etkileyen pratik kararlardır. Yanlış bir seçim, finansal raporlarda tutarsızlıklar, stok hataları veya müşteri verilerinde bozulmalar gibi ciddi sonuçlara yol açabilir. Bu yüzden, bir sistem tasarlarken veya mevcut bir sistemi debug ederken, bu seviyeleri anlamak ve doğru kullanmak bence olmazsa olmazdır.
Yukarıdaki sevkiyat senaryosunun kökü de buydu: raporlama işlemi READ COMMITTED seviyesinde çalışırken, gün içinde yeni kayıtlar ekleniyor ve bazıları güncelleniyordu. Rapor, her SELECT sorgusunda en son commit edilmiş veriyi gördüğü için, işlem süresi boyunca değişen veriler nedeniyle tutarsız sonuçlar üretiyordu. Bu, “Phantom Read” ve “Non-Repeatable Read” senaryolarına güzel bir örnektir ve bu konuyu daha derinlemesine araştırmam gerektiğinin ilk sinyali olmuştu.
READ COMMITTED ve Beklenmedik Sonuçları
READ COMMITTED, birçok veritabanının (PostgreSQL dahil) varsayılan yalıtım seviyesidir ve genellikle çoğu uygulama için yeterli kabul edilir. Bu seviyede, bir işlem yalnızca diğer işlemler tarafından commit edilmiş verileri görür. Bu, “kirli okumaları” (dirty reads) engeller; yani, henüz commit edilmemiş, potansiyel olarak geri alınabilecek verileri okumazsınız. Ancak, bu seviye “tekrarlanamayan okumalar” (non-repeatable reads) ve “hayalet okumalar” (phantom reads) sorunlarına açıktır.
Benim tecrübelerimde, READ COMMITTED seviyesinin getirdiği bu esneklik, bazen sinsi sorunlara yol açtı. Özellikle birden fazla SELECT sorgusu içeren uzun süreli işlemler veya raporlamalar söz konusu olduğunda, işlemin başında okuduğum bir verinin, işlemin ilerleyen aşamalarında başka bir işlem tarafından güncellenip commit edilmesiyle değiştiğini gördüm. Bu durum, veri analizi veya finansal raporlama gibi alanlarda kritik tutarsızlıklar yaratabiliyor.
Bunun klasik bir örneği, envanter yönetiminde stok kontrol edip rezervasyon yapan bir işlemdir: işlem önce ürünün mevcut stokunu sorgular, sonra müşteri için belirli bir miktarı ayırır ve ardından stok miktarını günceller. Eğer ilk sorgu ile güncelleme arasında başka bir işlem aynı ürünün stoğunu değiştirirse, READ COMMITTED seviyesinde çalışan işlem yanlış stok miktarıyla rezervasyon yapabilir. Bu da müşteriye “ürün mevcut” deyip sonra “stokta yok” deme riskini getirir.
BEGIN;
-- İlk SELECT: stok 100
SELECT stock_quantity FROM products WHERE id = 123;
-- Bu arada başka bir işlem stoğu 90'a düşürüyor ve commit ediyor.
-- İkinci SELECT: stok 90
SELECT stock_quantity FROM products WHERE id = 123;
COMMIT;
Yukarıdaki senaryo, READ COMMITTED altında aynı işlem içinde bile tutarsız okumaların nasıl ortaya çıkabileceğini gösteriyor. Eğer iş mantığı bu tür dalgalanmalara karşı hassassa, daha yüksek bir yalıtım seviyesine geçmek veya uygulama katmanında ek kilitleme mekanizmaları kullanmak gerekebilir. Benim için bu, sadece veritabanı ayarlarının değil, aynı zamanda iş akışının ve uygulamanın nasıl tasarlandığının da bu seviyelerle doğrudan ilişkili olduğunu gösterdi.
REPEATABLE READ: Tutarlılık İçin Bir Adım
REPEATABLE READ yalıtım seviyesi, READ COMMITTED’ın çözemediği tekrarlanamayan okuma sorununu gidermek için tasarlanmıştır. Bu seviyede, bir işlem başladığında okuduğu tüm veriler, o işlem bitene kadar aynı kalır. Yani, aynı işlemi içinde aynı SELECT sorgusunu kaç kez çalıştırırsanız çalıştırın, her zaman aynı sonuç kümesini alırsınız. Bu, özellikle raporlama ve analitik işlemlerde veri tutarlılığını sağlamak için çok önemlidir.
Ancak, REPEATABLE READ dahi “hayalet okumalar” (phantom reads) sorununa karşı her zaman tam koruma sağlamaz. Phantom reads, bir işlem boyunca bir sorgunun döndürdüğü satır kümesinin değişmesidir; yani, işlemin başında belirli bir koşulu sağlayan 5 satır varken, işlemin ortasında başka bir işlem o koşulu sağlayan yeni bir satır eklerse, aynı sorgu bu yeni satırı görebilir. PostgreSQL’in REPEATABLE READ uygulaması, SELECT sorguları için gerçekten phantom read’leri engeller (MVCC snapshot sayesinde), ancak diğer bazı veritabanı sistemlerinde durum farklı olabilir. Bu yüzden, standart tanımlamaları ve spesifik veritabanı davranışlarını iyi ayırt etmek gerekiyor.
Finansal hesaplama içeren karmaşık raporlarda bu seviye kritik hâle gelir. Bu raporların doğru olabilmesi için, rapor süresince okunan tüm finansal kayıtların sabit kalması gerekir. READ COMMITTED ile böyle bir rapor çalıştırıldığında, ara ara başka işlemler yeni gelir/gider kayıtları ekledikçe hesaplamalar sürekli değişebilir. REPEATABLE READ’e geçmek bu sorunu çözer.
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
-- İlk SELECT: Tüm gelir kalemlerini al
SELECT SUM(amount) FROM transactions WHERE type = 'income' AND date BETWEEN '2026-01-01' AND '2026-01-31';
-- Bu arada başka bir işlem aynı tarihler için yeni bir gelir kaydı ekliyor ve commit ediyor.
-- İkinci SELECT: Aynı sorguyu tekrar çalıştır, sonuç ilkine eşit olmalı
SELECT SUM(amount) FROM transactions WHERE type = 'income' AND date BETWEEN '2026-01-01' AND '2026-01-31';
COMMIT;
Bu örnekte, REPEATABLE READ sayesinde ikinci SELECT sorgusu, ilk sorgunun gördüğü verinin snapshot’ını kullanarak aynı sonucu döndürecektir. Bu, raporlama veya belirli bir anlık durumu analiz etme ihtiyacı olan uygulamalar için büyük bir güvenilirlik sağlar. Ancak, bu seviyenin artan kaynak kullanımı ve potansiyel kilitlenme riskleri olduğunu da unutmamak gerekir. Her ne kadar PostgreSQL’in MVCC (Multi-Version Concurrency Control) yapısı bu riskleri minimize etse de, transaction retries için hazırlıklı olmak her zaman iyidir.
SERIALIZABLE: Güvenlik ve Performans Dengesi
SERIALIZABLE, ANSI SQL standardındaki en yüksek yalıtım seviyesidir. Bu seviyede, eş zamanlı çalışan tüm işlemler, sanki birbiri ardına sırayla (seri olarak) çalışmış gibi görünür. Bu, bir işlemin diğer bir işlem tarafından oluşturulan herhangi bir tutarsızlığı görmesini tamamen engeller; yani, dirty reads, non-repeatable reads ve phantom reads gibi tüm anormallikler ortadan kalkar. Veri bütünlüğünün mutlak öncelikli olduğu kritik uygulamalar için ideal bir seçimdir.
Ancak, bu güvenlik seviyesinin bir bedeli vardır: performans. SERIALIZABLE işlemleri, çakışan işlemler olduğunda serialization_failure hatasıyla karşılaşabilir ve geri alınmak zorunda kalabilir. Bu, uygulamanın bu tür hataları yakalayıp işlemi tekrar denemesi (retry logic) gerektiği anlamına gelir. Bu ekstra yük, özellikle yüksek eş zamanlılığa ve yoğun yazma işlemlerine sahip sistemlerde ciddi bir darboğaz yaratabilir.
Bir bankanın iç platformunda, karmaşık bir mutabakat süreci tasarlarken SERIALIZABLE kullanmak zorunda kaldım. Bu süreç, farklı hesaplar arasında para transferlerini ve bakiye güncellemelerini içeriyordu ve en ufak bir tutarsızlık kabul edilemezdi. İlk başta, serialization_failure hatalarını yeterince iyi ele almadığım için sık sık işlem kesintileri yaşıyorduk. Debug süreci, bu hataları yakalayıp işlemleri otomatik olarak yeniden denememiz gerektiğini öğretti.
import psycopg2
from psycopg2 import errors
def run_serializable_transaction(conn_string, query_func):
for _ in range(5): # 5 deneme hakkı ver
conn = None
try:
conn = psycopg2.connect(conn_string)
conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE)
cur = conn.cursor()
query_func(cur)
conn.commit()
return True
except errors.SerializationFailure:
print("SerializationFailure detected, retrying...")
if conn:
conn.rollback() # İşlemi geri al
except Exception as e:
print(f"An unexpected error occurred: {e}")
if conn:
conn.rollback()
return False
finally:
if conn:
conn.close()
print("Max retries reached, transaction failed.")
return False
# Örnek kullanım
def my_complex_query(cursor):
cursor.execute("UPDATE accounts SET balance = balance - 100 WHERE id = 1;")
cursor.execute("UPDATE accounts SET balance = balance + 100 WHERE id = 2;")
# run_serializable_transaction("dbname=mydb user=myuser", my_complex_query)
Bu örnek, SERIALIZABLE seviyesinde bir işlem yaparken SerializationFailure hatalarını nasıl yakalayıp yeniden deneyeceğimizi gösteriyor. Bu basit retry mekanizması, uygulamanın dayanıklılığını ve güvenilirliğini önemli ölçüde artırdı. SERIALIZABLE kullanırken, uygulama katmanının bu tür hatalara karşı hazırlıklı olması gerektiğini unutmamak, benim için büyük bir ders oldu. Performans kayıplarını minimize etmek için, bu seviyeyi gerçekten ihtiyaç duyulan kritik işlemlerle sınırlı tutmak en akıllıca yaklaşımdır.
Gerçek Dünya Uygulamalarında Trade-off’lar ve Yanlış Anlamalar
Veritabanı yalıtım seviyelerini seçmek, her zaman bir trade-off meselesidir: veri tutarlılığı, eş zamanlılık ve performans arasında bir denge kurmak zorundasınız. “Her zaman en yüksek seviyeyi kullanın, çünkü en güvenlisi odur” gibi bir düşünce, pratikte çoğu zaman sürdürülemez performans sorunlarına yol açar. Diğer yandan, “varsayılan seviye her zaman yeterlidir” düşüncesi de, sinsi veri bozulmalarına zemin hazırlayabilir.
Yoğun veri girişi yapılan bir sistemde her şeyi SERIALIZABLE ile çalıştırmakta ısrar etmek tipik bir yanlıştır: sonuç olarak veritabanı kilitlenmeleri ve serialization_failure hataları nedeniyle kayda değer oranda işlem başarısızlığı ortaya çıkar. Oysa detaylı bir analiz ve profil çıkarma çoğu zaman, işlemlerin büyük kısmının READ COMMITTED veya belirli bölümlerin REPEATABLE READ ile yeterince güvenli olabileceğini gösterir. Doğru seviyeler seçildiğinde işlem başarısızlık oranı ihmal edilebilir bir düzeye iner.
Bu durum, yalıtım seviyesi seçiminin sadece teknik bir karar olmadığını, aynı zamanda iş süreçlerinin doğasını ve uygulamanın beklentilerini de yansıtması gerektiğini gösterdi. İş mantığı, hangi verilerin ne kadar süreyle tutarlı kalması gerektiğini dikte eder. Örneğin, bir kullanıcının sepetindeki ürün miktarının anlık olarak değişmesi kabul edilebilirken, finansal bir raporun iki kez çalıştırıldığında farklı sonuçlar vermesi kabul edilemez.
Veritabanı yalıtım seviyeleri konusunda doğru kararı vermek, aynı zamanda veritabanı performans optimizasyonunun da ayrılmaz bir parçasıdır. Gereksiz yere yüksek bir yalıtım seviyesi kullanmak, kilitleme çekişmelerini artırarak sistemin genel throughput’unu düşürebilir. Bu nedenle, her zaman en az kısıtlayıcı seviyeyi seçmeli, ancak bu seviyenin iş gereksinimlerini karşıladığından emin olmalıyız. Daha önce veritabanı performans optimizasyonunda karşılaştığım zorluklar yazımda bu dengeleri daha detaylı ele almıştım.
Kariyerimde Yalıtım Seviyelerinin Yeri: Neden Önemli Bir Bilgi?
Bu konuyu kariyer kategorisinde yazmamın özel bir nedeni var: veritabanı işlem yalıtım seviyelerini derinlemesine anlamak, bence bir yazılımcının veya sistem mimarının yetkinlik seviyesini doğrudan gösteren önemli bir ayıraçtır. Yüzeysel bilgiyle birçok uygulama geliştirebilirsiniz, ancak veri bütünlüğü sorunları ortaya çıktığında, bu detayları bilmek sizi diğerlerinden ayırır.
Benim kariyerimde, bu bilgi birçok kez fark yaratmamı sağladı. Finansal raporlarda periyodik olarak ortaya çıkan ve uzun süredir çözülemeyen küçük tutarsızlıklarda genellikle raporlama aracı, iş mantığı veya manuel veri giriş hataları suçlanır. Oysa çoğu zaman kök sebep, karmaşık bir çok adımlı işlemin READ COMMITTED seviyesinde çalışırken, eş zamanlı başka bir işlemin veriyi değiştirip raporun ortasında farklı bir snapshot almasına izin vermesidir. Bu tür sorunlar çoğunlukla basit bir SET TRANSACTION ISOLATION LEVEL komutuyla çözülür.
Bu tür bir anlayış, sadece bug çözmekle kalmaz, aynı zamanda daha sağlam ve ölçeklenebilir sistemler tasarlamanıza da yardımcı olur. Sistem mimarisi kararları alırken, bir monolith’ten microservice’lere geçiş yaparken veya event-sourcing gibi mimari desenleri uygularken, işlemlerin nasıl yalıtılacağını bilmek kritik öneme sahiptir. kurumsal yazılım mimarisi kararlarında deneyimlerim yazımda bu tür mimari seçimlerin altındaki motivasyonları daha detaylı anlatmıştım.
Bu bilgi, gelecekteki olası sorunları öngörmenizi ve daha en başta doğru tasarımı yapmanızı sağlar. Bir sistemin sadece “çalışıyor” olması yeterli değildir, aynı zamanda “doğru çalışıyor” olması gerekir. Veritabanı işlem yalıtım seviyeleri, bu “doğru çalışma”nın temel taşlarından biridir.
Sonuç: Yalıtım Seviyeleri Bir Lüks Değil, Bir Zorunluluktur
Veritabanı işlem yalıtım seviyeleri, çoğu zaman göz ardı edilen, ancak veri bütünlüğünün ve sistem güvenilirliğinin temelini oluşturan kritik bir konudur. Benim 20 yıla yakın tecrübemde, bu konuyu anlamanın sadece teknik bir detay olmaktan öte, sağlam ve güvenilir yazılım sistemleri inşa etmenin anahtarı olduğunu defalarca gördüm. READ COMMITTED’ın varsayılan olduğu bir dünyada yaşıyor olsak da, iş gereksinimlerinizin ne zaman REPEATABLE READ veya SERIALIZABLE gerektirdiğini bilmek, sizi olası veri felaketlerinden korur.
Bu konuya zaman ayırmak, sadece iyi bir veritabanı yöneticisi veya yazılımcı olmak için değil, aynı zamanda kompleks sistemlerdeki sinsi hataları tespit edip çözebilen, dolayısıyla kariyerinde fark yaratabilen bir profesyonel olmak için de elzemdir. Benim net pozisyonum: Bu seviyelerin ne anlama geldiğini, hangi senaryolarda hangi sorunlara yol açtığını ve hangi seviyenin hangi trade-off’ları getirdiğini anlamak, her teknoloji insanının bilgi dağarcığında bulunması gereken temel bir yetkinliktir.