Transaction ve Isolation Seviyeleri: Veri Tutarlılığının Temel Taşları
Veritabanı işlemlerinin güvenilirliğini sağlayan en önemli kavramlardan biri transaction (işlem) ve onun isolation (izolasyon) özelliğidir. Bir banka havalesinden e-ticaret siparişine kadar kritik işlemlerin doğru çalışması, bu iki kavramın doğru anlaşılmasına bağlıdır.
Bu yazıda, transaction kavramını derinlemesine inceleyecek, isolation seviyelerini detaylandıracak ve her seviyenin yol açtığı sorunları gerçek kod örnekleriyle açıklayacağız.
1. Transaction (İşlem) Nedir?
Transaction, mantıksal bir bütün oluşturan bir veya daha fazla veritabanı işleminin (INSERT, UPDATE, DELETE) atomik olarak yürütülmesidir.
Transaction'ın 4 Temel Özelliği (ACID)
| Özellik | Açıklama | Örnek |
|---|---|---|
| Atomicity (Atomiklik) | Ya hep ya hiç | Para çekme ve yatırma işlemlerinin ikisi de başarılı olmalı veya hiçbiri olmamalı |
| Consistency (Tutarlılık) | Veritabanı kuralları her zaman geçerli | Bakiye hiçbir zaman 0'ın altına düşemez |
| Isolation (İzolasyon) | Transaction'lar birbirini etkilemez | İki kişi aynı anda son bileti alamaz |
| Durability (Kalıcılık) | Commit edilen veriler kalıcıdır | Sistem çökse bile sipariş kaybolmaz |
Transaction Komutları
-- Transaction başlat
BEGIN;
-- veya
START TRANSACTION;
-- İşlemler
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- Başarılıysa kalıcı yap
COMMIT;
-- Hata varsa geri al
ROLLBACK;
-- Transaction içinde nokta belirle
SAVEPOINT sp1;
-- İşlemler...
-- Sadece sp1'den sonrasını geri al
ROLLBACK TO SAVEPOINT sp1;
2. Isolation Sorunları (Concurrency Problemleri)
Transaction'lar aynı anda çalıştığında ortaya çıkan 4 temel sorun vardır:
2.1 Dirty Read (Kirli Okuma)
Bir transaction, henüz commit edilmemiş (ve dolayısıyla rollback olabilecek) verileri okur.
Senaryo:
Transaction 1: Transaction 2:
BEGIN;
BEGIN;
UPDATE accounts SET balance=0
WHERE id=1;
SELECT balance FROM accounts WHERE id=1;
-- 0 görür (henüz commit edilmedi!)
ROLLBACK; -- Değişiklik geri alındı
-- Transaction 2 hala 0'ı okumaya devam ediyor!
-- Bu veri aslında hiç var olmadı!
2.2 Non-repeatable Read (Tekrarlanamaz Okuma)
Aynı transaction içinde aynı veri iki kere okunduğunda farklı değerler görülmesi.
Senaryo:
Transaction 1: Transaction 2:
BEGIN;
SELECT balance FROM accounts
WHERE id=1; -- 100 TL görür
BEGIN;
UPDATE accounts SET balance=200
WHERE id=1;
COMMIT; -- Değişiklik kalıcı
SELECT balance FROM accounts
WHERE id=1; -- 200 TL görür (farklı değer!)
COMMIT;
-- Aynı transaction'da aynı satır iki farklı değer verdi!
2.3 Phantom Read (Hayalet Okuma)
Aynı transaction içinde aynı sorgu çalıştırıldığında, farklı sayıda satır görülmesi.
Senaryo:
Transaction 1: Transaction 2:
BEGIN;
SELECT * FROM accounts
WHERE balance > 1000;
-- 5 satır döndü
BEGIN;
INSERT INTO accounts (id, balance)
VALUES (10, 2000);
COMMIT;
SELECT * FROM accounts
WHERE balance > 1000;
-- 6 satır döndü (yeni satır "hayalet" gibi göründü!)
COMMIT;
2.4 Lost Update (Kayıp Güncelleme)
İki transaction aynı veriyi günceller ve birinin güncellemesi diğerinin üzerine yazar.
Senaryo:
Transaction 1: Transaction 2:
BEGIN; BEGIN;
SELECT balance FROM accounts
WHERE id=1; -- 100 TL görür
SELECT balance FROM accounts
WHERE id=1; -- 100 TL görür
UPDATE accounts SET balance=150 -- 100 + 50
WHERE id=1;
UPDATE accounts SET balance=200 -- 100 + 100
WHERE id=1;
COMMIT; -- 150 kaydedildi COMMIT; -- 200 kaydedildi (150'nin üzerine yazdı!)
-- Transaction 1'in güncellemesi kayboldu!
3. Isolation Seviyeleri (SQL Standardı)
SQL standardı 4 isolation seviyesi tanımlar. Aşağıdan yukarıya doğru tutarlılık artar, performans düşer.
| Isolation Seviyesi | Dirty Read | Non-repeatable Read | Phantom Read | Lost Update |
|---|---|---|---|---|
| READ UNCOMMITTED | ✅ (Oluşabilir) | ✅ (Oluşabilir) | ✅ (Oluşabilir) | ✅ (Oluşabilir) |
| READ COMMITTED | ❌ (Engellenir) | ✅ (Oluşabilir) | ✅ (Oluşabilir) | ✅ (Oluşabilir) |
| REPEATABLE READ | ❌ (Engellenir) | ❌ (Engellenir) | ✅ (Oluşabilir) | ❌ (Engellenir) |
| SERIALIZABLE | ❌ (Engellenir) | ❌ (Engellenir) | ❌ (Engellenir) | ❌ (Engellenir) |
4. Isolation Seviyelerinin Detaylı İncelenmesi
4.1 READ UNCOMMITTED (En Düşük Seviye)
Hiçbir izolasyon yok. Transaction'lar birbirinin commit edilmemiş verilerini görebilir.
-- PostgreSQL'de READ UNCOMMITTED aslında READ COMMITTED gibi çalışır
-- MySQL'de gerçek READ UNCOMMITTED vardır
-- Session 1
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN;
UPDATE accounts SET balance = 0 WHERE id = 1;
-- Henüz COMMIT yok!
-- Session 2 (eşzamanlı)
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN;
SELECT balance FROM accounts WHERE id = 1;
-- MySQL'de: 0 görür (dirty read!)
-- PostgreSQL'de: 100 görür (READ COMMITTED gibi davranır)
-- Session 1
ROLLBACK; -- Değişiklik iptal
-- Session 2 hala 0 görüyor (ama veri hiç var olmadı!)
Kullanım Alanı: Neredeyse hiçbir yerde kullanılmaz. Sadece yaklaşık raporlamalarda.
4.2 READ COMMITTED (Varsayılan - Çoğu DB)
Sadece commit edilmiş veriler okunabilir. Dirty read engellenir ama non-repeatable read olabilir.
-- PostgreSQL, Oracle, SQL Server varsayılanı
-- Session 1
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- 100 TL
-- Session 2
BEGIN;
UPDATE accounts SET balance = 200 WHERE id = 1;
COMMIT; -- Güncelleme kalıcı
-- Session 1 (devam)
SELECT balance FROM accounts WHERE id = 1; -- 200 TL (non-repeatable read!)
COMMIT;
PostgreSQL'de READ COMMITTED Davranışı:
-- PostgreSQL'de her sorgu, sorgu başladığı anda commit edilmiş verileri görür
BEGIN;
-- Sorgu 1: T=1 anındaki commit edilmiş veriler
SELECT * FROM accounts WHERE balance > 1000;
-- Başka transaction commit yaparsa...
-- Sorgu 2: T=2 anındaki commit edilmiş veriler (farklı olabilir!)
SELECT * FROM accounts WHERE balance > 1000;
COMMIT;
MySQL'de READ COMMITTED Davranışı:
-- MySQL'de de benzer, ama locking davranışı farklı
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
SELECT * FROM accounts WHERE balance > 1000 FOR UPDATE;
-- Sadece eşleşen satırlar kilitlenir (gap lock yok)
COMMIT;
4.3 REPEATABLE READ
Transaction boyunca aynı veri her okunduğunda aynı değeri verir. Non-repeatable read engellenir.
-- MySQL varsayılanı, PostgreSQL'de de mevcut
-- Session 1
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- 100 TL
-- Session 2
BEGIN;
UPDATE accounts SET balance = 200 WHERE id = 1;
COMMIT; -- Güncelleme yapıldı
-- Session 1 (devam)
SELECT balance FROM accounts WHERE id = 1; -- HALA 100 TL (aynı değer!)
-- PostgreSQL: Transaction başlangıcındaki snapshot kullanılır
-- MySQL: İlk okumadaki snapshot kullanılır
COMMIT;
-- Commit'ten sonra: 200 TL görür
PostgreSQL vs MySQL REPEATABLE READ Farkı
PostgreSQL (Snapshot Isolation):
-- PostgreSQL: Transaction başlangıcında snapshot alınır
BEGIN; -- T=1 anında snapshot
-- T=2'de başka transaction commit yapsa da...
SELECT * FROM accounts; -- T=1 anındaki veriler
SELECT * FROM accounts; -- Hala T=1 anındaki veriler
COMMIT;
MySQL (İlk Okuma Snapshot'ı):
-- MySQL: İlk okumada snapshot alınır
BEGIN; -- T=1
-- T=2'de başka transaction commit yaptı
SELECT * FROM accounts; -- T=2 anındaki veriler (ilk okuma)
-- T=3'te başka transaction commit yaptı
SELECT * FROM accounts; -- Hala T=2 anındaki veriler (ilk snapshot)
COMMIT;
Phantom Read MySQL'de:
-- MySQL REPEATABLE READ'de phantom read olmaz! (Gap lock sayesinde)
-- Session 1
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT * FROM accounts WHERE balance > 1000; -- 5 satır
-- Session 2
BEGIN;
INSERT INTO accounts VALUES (10, 2000); -- MySQL'de bu BLOKELENİR!
-- (Gap lock nedeniyle)
COMMIT; -- Session 1 commit edene kadar bekler
-- Session 1
SELECT * FROM accounts WHERE balance > 1000; -- Hala 5 satır
COMMIT; -- Session 2 artık ekleyebilir
4.4 SERIALIZABLE (En Yüksek Seviye)
Transaction'lar sanki sırayla çalışıyormuş gibi davranır. Tüm concurrency sorunları engellenir.
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN;
-- Bu sorgu, accounts tablosunda balance > 1000 olan tüm satırları ve
-- bu aralığa yeni eklenebilecek tüm satırları kilitler
SELECT * FROM accounts WHERE balance > 1000;
-- Başka bir transaction, balance > 1000 olan yeni bir kayıt EKLEYEMEZ!
-- Hata alır: "could not serialize access due to concurrent update"
COMMIT;
SERIALIZABLE Hata Senaryosu:
-- Session 1
BEGIN ISOLATION LEVEL SERIALIZABLE;
SELECT * FROM accounts WHERE balance > 1000; -- 5 satır
-- Session 2
BEGIN ISOLATION LEVEL SERIALIZABLE;
INSERT INTO accounts VALUES (10, 2000);
COMMIT; -- PostgreSQL'de: COMMIT başarılı olur ama...
-- Session 1
SELECT * FROM accounts WHERE balance > 1000; -- Hala 5 satır
COMMIT; -- PostgreSQL'de HATA: "could not serialize access"
-- Çünkü Session 1'in snapshot'ı ile Session 2'nin değişikliği çakışıyor
5. Pratik Örnek: E-Ticaret Stok Yönetimi
Gerçek bir e-ticaret senaryosunda farklı isolation seviyelerini inceleyelim:
5.1 Problem: Son Ürünü İki Kişi Almaya Çalışıyor
-- Tablo: products (id, name, stock, version)
-- 1. Kullanıcı (Transaction 1)
BEGIN;
SELECT stock FROM products WHERE id = 100; -- stock = 1
-- 2. Kullanıcı (Transaction 2) - EŞZAMANLI
BEGIN;
SELECT stock FROM products WHERE id = 100; -- stock = 1
-- Transaction 1
UPDATE products SET stock = 0 WHERE id = 100;
-- Transaction 2
UPDATE products SET stock = 0 WHERE id = 100;
-- Bu noktada ne olacak?
5.2 Çözüm 1: Optimistic Locking (İyimser Kilit)
-- Ürün tablosuna version sütunu ekleyelim
ALTER TABLE products ADD COLUMN version INTEGER DEFAULT 0;
-- Transaction 1
BEGIN;
SELECT stock, version FROM products WHERE id = 100;
-- stock=1, version=5
-- Transaction 2 (eşzamanlı)
BEGIN;
SELECT stock, version FROM products WHERE id = 100;
-- stock=1, version=5
-- Transaction 1 - Güncelleme
UPDATE products
SET stock = 0, version = 6
WHERE id = 100 AND version = 5;
-- 1 satır etkilendi (başarılı)
-- Transaction 2 - Güncelleme
UPDATE products
SET stock = 0, version = 6
WHERE id = 100 AND version = 5;
-- 0 satır etkilendi! (version değiştiği için)
-- Transaction 2 başarısız, kullanıcıya "stok bitti" mesajı
5.3 Çözüm 2: Pessimistic Locking (Kötümser Kilit)
-- Transaction 1
BEGIN;
SELECT * FROM products WHERE id = 100 FOR UPDATE;
-- Satır kilitlendi, başkası okuyamaz/güncelleyemez
-- Transaction 2 (bekler)
BEGIN;
SELECT * FROM products WHERE id = 100 FOR UPDATE;
-- Transaction 1 COMMIT edene kadar BLOKE OLUR
-- Transaction 1
UPDATE products SET stock = 0 WHERE id = 100;
COMMIT; -- Kilit bırakıldı
-- Transaction 2 (şimdi çalışır)
-- Satırı okur: stock=0, güncelleme yapamaz
COMMIT;
5.4 Çözüm 3: SERIALIZABLE ile Otomatik
-- En güvenli ama en yavaş çözüm
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN;
SELECT stock FROM products WHERE id = 100;
-- Başka transaction aynı ürünü güncellemeye çalışırsa:
-- Ya bekler ya da hata alır
UPDATE products SET stock = stock - 1 WHERE id = 100;
COMMIT;
-- Eğer serialization hatası olursa, transaction tekrar denenmeli
6. Farklı Veritabanlarında Isolation Seviyeleri
| Veritabanı | Varsayılan | SERIALIZABLE Davranışı | Notlar |
|---|---|---|---|
| PostgreSQL | READ COMMITTED | True Serializable (SSI) | Snapshot isolation kullanır |
| MySQL | REPEATABLE READ | True Serializable | Gap lock ile phantom read engellenir |
| Oracle | READ COMMITTED | READ ONLY aslında | Serializable yerine read-only |
| SQL Server | READ COMMITTED | True Serializable | Lock-based |
PostgreSQL Özel Durumu: Serializable Snapshot Isolation (SSI)
-- PostgreSQL'de SERIALIZABLE gerçekten serializable davranır
-- Ama diğer DB'lerden farklı çalışır
-- Session 1
BEGIN ISOLATION LEVEL SERIALIZABLE;
SELECT * FROM accounts WHERE balance > 1000;
-- Session 2
BEGIN ISOLATION LEVEL SERIALIZABLE;
INSERT INTO accounts VALUES (100, 2000);
COMMIT; -- Başarılı
-- Session 1
SELECT * FROM accounts WHERE balance > 1000;
-- Yeni kayıt görünmez (snapshot devam eder)
COMMIT;
-- HATA! "could not serialize access"
-- PostgreSQL bunu tespit eder ve transaction'ı başarısız yapar
7. Pratik Kod Örnekleri
7.1 Python + PostgreSQL ile Isolation Seviyeleri
import psycopg2
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
import threading
import time
def transaction_example(isolation_level, thread_id):
conn = psycopg2.connect(
host="localhost",
database="testdb",
user="postgres",
password="password"
)
# Isolation seviyesini ayarla
if isolation_level == "READ COMMITTED":
conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_READ_COMMITTED)
elif isolation_level == "REPEATABLE READ":
conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_REPEATABLE_READ)
elif isolation_level == "SERIALIZABLE":
conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE)
cursor = conn.cursor()
try:
# Transaction başlat
cursor.execute("BEGIN;")
print(f"Thread {thread_id}: Stok okunuyor...")
cursor.execute("SELECT stock FROM products WHERE id = 1 FOR UPDATE;")
stock = cursor.fetchone()[0]
print(f"Thread {thread_id}: Stok = {stock}")
# Biraz bekle (diğer thread'in çalışması için)
time.sleep(2)
if stock > 0:
print(f"Thread {thread_id}: Stok güncelleniyor...")
cursor.execute("UPDATE products SET stock = stock - 1 WHERE id = 1;")
cursor.execute("COMMIT;")
print(f"Thread {thread_id}: Transaction COMMIT edildi")
except Exception as e:
print(f"Thread {thread_id}: HATA! {e}")
cursor.execute("ROLLBACK;")
finally:
cursor.close()
conn.close()
# Test: İki thread aynı anda çalışsın
def run_test(isolation_level):
print(f"\n--- Testing {isolation_level} ---")
t1 = threading.Thread(target=transaction_example, args=(isolation_level, 1))
t2 = threading.Thread(target=transaction_example, args=(isolation_level, 2))
t1.start()
time.sleep(0.5) # Biraz bekle ki t1 lock'u alsın
t2.start()
t1.join()
t2.join()
# Testleri çalıştır
# run_test("READ COMMITTED")
# run_test("REPEATABLE READ")
# run_test("SERIALIZABLE")
7.2 Spring Boot @Transactional ile Isolation
@Service
public class OrderService {
@Autowired
private ProductRepository productRepository;
@Transactional(isolation = Isolation.SERIALIZABLE)
public Order createOrder(Long productId, int quantity) {
// SERIALIZABLE isolation ile çalışır
Product product = productRepository.findById(productId)
.orElseThrow(() -> new ProductNotFoundException());
if (product.getStock() < quantity) {
throw new InsufficientStockException();
}
product.setStock(product.getStock() - quantity);
productRepository.save(product);
Order order = new Order();
order.setProduct(product);
order.setQuantity(quantity);
order.setOrderDate(new Date());
return orderRepository.save(order);
}
@Transactional(isolation = Isolation.READ_COMMITTED)
public List<Product> getAvailableProducts() {
// READ_COMMITTED ile sorgu anındaki commit edilmiş veriler
return productRepository.findByStockGreaterThan(0);
}
@Transactional(isolation = Isolation.REPEATABLE_READ)
public BigDecimal calculateTotalPrice(Long orderId) {
// REPEATABLE READ ile tüm hesaplama boyunca aynı fiyatlar
Order order = orderRepository.findById(orderId).get();
BigDecimal total = BigDecimal.ZERO;
for (OrderItem item : order.getItems()) {
// Fiyat değişse bile aynı değeri okur
total = total.add(item.getPrice().multiply(
new BigDecimal(item.getQuantity())));
}
return total;
}
}
8. Isolation Seviyesi Seçim Kriterleri
Hangi Seviye Ne Zaman Kullanılmalı?
| Durum | Önerilen Isolation | Sebep |
|---|---|---|
| Raporlama, Dashboard | READ COMMITTED | Performans önemli, küçük tutarsızlıklar tolere edilebilir |
| Bakiye sorgulama | REPEATABLE READ | Tutarlılık önemli, phantom read riski yok |
| Para transferi | SERIALIZABLE | En yüksek güvenlik gerekli |
| Stok güncelleme | REPEATABLE READ + Optimistic Lock | SERIALIZABLE'dan daha iyi performans |
| Toplu veri işleme | READ COMMITTED | Uzun süren işlemlerde kilitleme az |
Performans Karşılaştırması
-- Aynı işlemi farklı isolation seviyelerinde test et
\timing on
-- READ COMMITTED
BEGIN ISOLATION LEVEL READ COMMITTED;
UPDATE products SET stock = stock - 1 WHERE id = 1;
COMMIT;
-- Time: 2.3 ms
-- REPEATABLE READ
BEGIN ISOLATION LEVEL REPEATABLE READ;
UPDATE products SET stock = stock - 1 WHERE id = 1;
COMMIT;
-- Time: 2.5 ms (biraz daha yavaş)
-- SERIALIZABLE
BEGIN ISOLATION LEVEL SERIALIZABLE;
UPDATE products SET stock = stock - 1 WHERE id = 1;
COMMIT;
-- Time: 3.8 ms (en yavaş)
9. En İyi Pratikler
9.1 Transaction Süresini Kısa Tutun
-- KÖTÜ - Uzun transaction
BEGIN;
SELECT * FROM accounts;
-- ... 10 saniye bekler (kullanıcı düşünüyor)
-- ... başka işlemler
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;
-- İYİ - Kısa transaction
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;
9.2 Deadlock Riskini Azaltın
-- KÖTÜ - Deadlock riski yüksek
-- Transaction 1
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
-- Transaction 2 (eşzamanlı)
BEGIN;
UPDATE accounts SET balance = balance - 50 WHERE id = 2;
UPDATE accounts SET balance = balance + 50 WHERE id = 1;
COMMIT;
-- İYİ - Aynı sırada güncelle
-- Her zaman id'ye göre sıralı güncelle
-- Transaction 1 ve 2 aynı sırayı kullanır
9.3 Optimistic Locking ile Retry Mekanizması
def purchase_with_retry(product_id, quantity, max_retries=3):
for attempt in range(max_retries):
try:
with transaction.atomic():
product = Product.objects.select_for_update().get(id=product_id)
if product.stock >= quantity:
product.stock -= quantity
product.version += 1
product.save()
Order.objects.create(
product=product,
quantity=quantity
)
return True
else:
return False
except OperationalError as e:
if "could not serialize access" in str(e):
if attempt == max_retries - 1:
raise # Son denemede hata fırlat
continue # Tekrar dene
else:
raise # Başka hata
Özet: Transaction ve Isolation Kontrol Listesi
- [ ] Doğru isolation seviyesini seçin: Tutarlılık vs performans dengesi
- [ ] Transaction'ları kısa tutun: Kilitleme süresini azaltın
- [ ] Deadlock riskini minimize edin: Kaynakları aynı sırada kullanın
- [ ] Optimistic locking kullanın: Çakışma azsa performans artışı
- [ ] Retry mekanizması ekleyin: SERIALIZABLE hatalarında
- [ ] Monitoring yapın: Uzun süren transaction'ları tespit edin
- [ ] Test edin: Yüksek concurrency altında test senaryoları yazın
Unutmayın: Doğru isolation seviyesi, uygulamanızın ihtiyaçlarına bağlıdır. Her zaman en yüksek seviyeyi kullanmak en iyisi değildir. "Doğru" isolation, yeterli tutarlılığı sağlayan en düşük seviyedir.