← Kursa Dön
📄 Text · 35 min

Transaction Isolation Levels

Giriş — İki Kişi Aynı Anda Aynı Ürünü Satın Alırsa Ne Olur?

Bir e-ticaret sitesinde stokta 1 adet kalan bir ürünü düşün. Ali ve Zeynep aynı anda "Satın Al" butonuna basıyor. İkisi de stoku kontrol ediyor: "1 adet var, tamam." İkisi de siparişi oluşturuyor. İkisi de stoku 1 düşürüyor: 1 - 1 = 0 ve 0 - 1 = -1. Stok eksi 1 oldu — bu olmamalıydı.

Bu, concurrency (eş zamanlılık) problemidir ve veritabanlarının en kritik konularından biri. Transaction isolation level'lar (işlem yalıtım seviyeleri), birden fazla transaction aynı anda çalışırken birbirlerini nasıl etkilediğini kontrol eder.

🎯 Analoji: Transaction isolation'ı bir ofisteki toplantı odaları gibi düşün. Tam yalıtım (SERIALIZABLE) = her toplantı ayrı odada, birbirini duymuyor. Düşük yalıtım (READ UNCOMMITTED) = açık ofis, herkes birbirinin konuşmasını duyuyor. Orta seviye = cam bölmeli odalar — birbirini görüyor ama doğrudan etkilemiyor.


Transaction Hatırlatma

Daha önce transaction temellerini öğrenmiştik. Kısa bir hatırlatma:

START TRANSACTION;

-- İşlemler
UPDATE accounts SET balance = balance - 1000 WHERE account_id = 1;
UPDATE accounts SET balance = balance + 1000 WHERE account_id = 2;

COMMIT;    -- Tüm değişiklikleri kalıcı yap
-- veya
ROLLBACK;  -- Tüm değişiklikleri geri al

Transaction, ACID özelliklerini garanti eder:

  • Atomicity: Ya hepsi ya hiçbiri

  • Consistency: Tutarlılık korunur

  • Isolation: Transaction'lar birbirini etkilemez

  • Durability: Commit edilen veri kalıcıdır

İşte "Isolation" kısmı, bu dersin konusu: bir transaction'daki değişiklikleri diğer transaction'lar ne zaman ve nasıl görür?


Concurrency Problemleri

Yalıtım seviyelerini anlamak için önce hangi sorunları çözdüklerini bilmek gerekir:

1. Dirty Read (Kirli Okuma)

Bir transaction, henüz commit edilmemiş başka bir transaction'ın değişikliklerini okur. O transaction rollback yaparsa, okunan veri hiçbir zaman var olmamış olur.

Transaction A:                      Transaction B:
UPDATE products SET price = 500     
WHERE product_id = 1;               
(henüz COMMIT yok!)                 
                                    SELECT price FROM products 
                                    WHERE product_id = 1;
                                    -- price = 500 okudu (dirty read!)
ROLLBACK;                           
-- Fiyat aslında eski değerine döndü
-- Ama B, 500 TL'yi "gerçek" sandı

2. Non-Repeatable Read (Tekrarlanamaz Okuma)

Bir transaction aynı satırı iki kez okuduğunda farklı sonuç alır — arada başka bir transaction o satırı değiştirip commit etmiştir.

Transaction A:                      Transaction B:
SELECT price FROM products          
WHERE product_id = 1;               
-- price = 1000                     
                                    UPDATE products SET price = 1500
                                    WHERE product_id = 1;
                                    COMMIT;
SELECT price FROM products          
WHERE product_id = 1;               
-- price = 1500 ← Farklı!          
-- Aynı sorgu, aynı transaction, farklı sonuç

3. Phantom Read (Hayalet Okuma)

Bir transaction aynı sorguyu iki kez çalıştırdığında farklı satır sayısı alır — arada başka bir transaction yeni satır eklemiş veya silmiştir.

Transaction A:                      Transaction B:
SELECT COUNT(*) FROM orders          
WHERE customer_id = 101;             
-- count = 5                        
                                    INSERT INTO orders (customer_id, ...)
                                    VALUES (101, ...);
                                    COMMIT;
SELECT COUNT(*) FROM orders          
WHERE customer_id = 101;             
-- count = 6 ← Hayalet satır!      

Dört İzolasyon Seviyesi

SQL standardı dört izolasyon seviyesi tanımlar. Her biri farklı bir güvenlik/performans dengesi sunar:

1. READ UNCOMMITTED — En Düşük Yalıtım

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
-- veya session bazlı:
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
  • Dirty read: ✅ Olabilir

  • Non-repeatable read: ✅ Olabilir

  • Phantom read: ✅ Olabilir

  • Performans: En hızlı (kilit yok)

-- Transaction A: READ UNCOMMITTED
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;

SELECT price FROM products WHERE product_id = 1;
-- Başka bir transaction'ın henüz COMMIT etmediği değişikliği görebilir!
-- Eğer o transaction ROLLBACK yaparsa, yanlış veri okumuş olursun

COMMIT;

Ne zaman kullanılır? Hemen hemen hiçbir zaman. Belki yaklaşık sayımlar, anlık istatistikler gibi kesinlik gerektirmeyen okuma sorgularında. Ama genel tavsiye: kullanma.

2. READ COMMITTED — Oracle'ın Varsayılanı

SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
  • Dirty read: ❌ Önlenir

  • Non-repeatable read: ✅ Olabilir

  • Phantom read: ✅ Olabilir

  • Performans: İyi

Her SELECT, o an commit edilmiş en güncel veriyi okur. Commit edilmemiş değişiklikleri görmez.

-- Transaction A: READ COMMITTED
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;

SELECT price FROM products WHERE product_id = 1;
-- price = 1000 (commit edilmiş veri)

-- Transaction B: UPDATE ... SET price = 1500; COMMIT;

SELECT price FROM products WHERE product_id = 1;
-- price = 1500 ← Değişti! (B commit etti, A yeni değeri görüyor)
-- Non-repeatable read gerçekleşti

COMMIT;

Ne zaman kullanılır? Çoğu OLTP uygulamasında yeterli. "Her okuduğumda en güncel veriyi istiyorum ama commit edilmemiş veriyi görmek istemiyorum" senaryosunda.

3. REPEATABLE READ — MySQL'in Varsayılanı

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
  • Dirty read: ❌ Önlenir

  • Non-repeatable read: ❌ Önlenir

  • Phantom read: ✅ Olabilir (ama MySQL InnoDB'de de önlenir!)

  • Performans: Orta

Transaction başladığında bir snapshot alınır. Transaction boyunca aynı sorgu her zaman aynı sonucu verir — başka transaction'lar commit etse bile.

-- Transaction A: REPEATABLE READ (MySQL varsayılan)
START TRANSACTION;

SELECT price FROM products WHERE product_id = 1;
-- price = 1000

-- Transaction B: UPDATE ... SET price = 1500; COMMIT;

SELECT price FROM products WHERE product_id = 1;
-- price = 1000 ← Aynı! B commit etse bile A eski değeri görür
-- Snapshot isolation sayesinde

COMMIT;
-- COMMIT'ten sonra artık 1500 görülür

MySQL InnoDB Özel Davranışı: InnoDB'de REPEATABLE READ, gap lock mekanizması sayesinde phantom read'i de önler. Bu, SQL standardının ötesinde bir garantidir — PostgreSQL'de REPEATABLE READ phantom read'e karşı koruma sağlamaz.

💡 İpucu: MySQL'in varsayılan izolasyon seviyesi REPEATABLE READ'dir ve çoğu uygulama için idealdir. Değiştirmek genellikle gerekmez.

4. SERIALIZABLE — En Yüksek Yalıtım

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
  • Dirty read: ❌ Önlenir

  • Non-repeatable read: ❌ Önlenir

  • Phantom read: ❌ Önlenir

  • Performans: En yavaş (en çok kilit)

Transaction'lar seri (birbiri ardına) çalışmış gibi davranır. Eş zamanlılık tamamen kontrol altında ama performans maliyeti çok yüksek.

-- Transaction A: SERIALIZABLE
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
START TRANSACTION;

SELECT COUNT(*) FROM orders WHERE customer_id = 101;
-- count = 5

-- Transaction B: INSERT INTO orders (customer_id, ...) VALUES (101, ...);
-- → Transaction B BEKLER! A tamamlanana kadar INSERT yapamaz

SELECT COUNT(*) FROM orders WHERE customer_id = 101;
-- count = 5 ← Aynı, çünkü B bekliyor

COMMIT;
-- Artık B'nin INSERT'i çalışır

Ne zaman kullanılır? Finansal işlemler, stok yönetimi gibi mutlak tutarlılık gereken durumlarda. Ama deadlock riski yüksektir.


Karşılaştırma Tablosu

ÖzellikREAD UNCOMMITTEDREAD COMMITTEDREPEATABLE READSERIALIZABLE
Dirty Read✅ Olabilir❌ Önlenir❌ Önlenir❌ Önlenir
Non-repeatable Read✅ Olabilir✅ Olabilir❌ Önlenir❌ Önlenir
Phantom Read✅ Olabilir✅ Olabilir❌ (InnoDB)❌ Önlenir
Performans⚡ En hızlı⚡ Hızlı⚡ İyi🐌 Yavaş
Kilit seviyesiYokSatır kilidiSatır + snapshotAralık kilidi
VarsayılanOracle, PostgreSQLMySQL InnoDB

Deadlock — Karşılıklı Kilitlenme

İki transaction birbirinin kilitlediği kaynağı beklerse deadlock oluşur:

Transaction A:                      Transaction B:
UPDATE products SET stock = stock-1  
WHERE product_id = 1;               
-- products id=1 kilitlendi          
                                    UPDATE products SET stock = stock-1
                                    WHERE product_id = 2;
                                    -- products id=2 kilitlendi
                                    
UPDATE products SET stock = stock-1  
WHERE product_id = 2;               
-- BEKLİYOR! B, id=2'yi kilitledi   
                                    UPDATE products SET stock = stock-1
                                    WHERE product_id = 1;
                                    -- BEKLİYOR! A, id=1'i kilitledi
                                    
-- DEADLOCK! İkisi de birbirini bekliyor

MySQL deadlock'ı otomatik algılar ve bir transaction'ı rollback eder:

ERROR 1213 (40001): Deadlock found when trying to get lock; 
try restarting transaction

Deadlock Önleme Stratejileri

-- 1. Kaynakları her zaman aynı sırada kilitle
-- ❌ Deadlock riski: A → 1,2 sırasında, B → 2,1 sırasında
-- ✅ Her ikisi de 1,2 sırasında kilitlesin

-- 2. Transaction'ları kısa tut
START TRANSACTION;
-- Sadece gerekli işlemleri yap
-- Uzun hesaplamalar transaction dışında
COMMIT;

-- 3. Timeout ayarla
SET innodb_lock_wait_timeout = 5;  -- 5 saniye bekle, sonra hata ver

-- 4. Retry mekanizması (uygulama kodunda)
-- Deadlock hatası alırsan, transaction'ı yeniden dene

Locking — Kilit Mekanizmaları

SELECT ... FOR UPDATE

Okuduğun satırları kilitler — başka transaction'lar değiştiremez:

START TRANSACTION;

-- Ürünü oku VE kilitle
SELECT product_id, stock_quantity, price
FROM products 
WHERE product_id = 1
FOR UPDATE;  -- Bu satır kilitlendi!

-- Stok kontrolü yap
-- IF stock_quantity >= requested_quantity THEN ...

-- Güncelle
UPDATE products SET stock_quantity = stock_quantity - 1
WHERE product_id = 1;

COMMIT;  -- Kilit serbest bırakılır

SELECT ... FOR SHARE (MySQL 8.0+)

Okuma kilidi — birden fazla transaction okuyabilir ama kimse yazamaz:

START TRANSACTION;

SELECT * FROM products WHERE product_id = 1
FOR SHARE;  -- Paylaşımlı kilit: herkes okuyabilir, kimse yazamaz

-- Veriyi oku, hesapla, kontrol et...

COMMIT;
Kilit TürüDiğer SELECTDiğer SELECT FOR SHAREDiğer FOR UPDATEDiğer UPDATE/DELETE
Kilitsiz SELECT
FOR SHARE❌ Bekler❌ Bekler
FOR UPDATE❌ Bekler❌ Bekler❌ Bekler

Gerçek Dünya Örneği — Güvenli Stok Yönetimi

DELIMITER //

CREATE PROCEDURE safe_purchase(
    IN p_customer_id INT,
    IN p_product_id INT,
    IN p_quantity INT,
    OUT p_result VARCHAR(200)
)
BEGIN
    DECLARE v_stock INT;
    DECLARE v_price DECIMAL(10,2);
    DECLARE v_order_id INT;
    
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        SET p_result = 'HATA: İşlem geri alındı';
    END;
    
    START TRANSACTION;
    
    -- FOR UPDATE ile stok kilidini al
    SELECT stock_quantity, price 
    INTO v_stock, v_price
    FROM products 
    WHERE product_id = p_product_id
    FOR UPDATE;
    
    -- Stok kontrolü
    IF v_stock < p_quantity THEN
        ROLLBACK;
        SET p_result = CONCAT('Yetersiz stok. Mevcut: ', v_stock);
    ELSE
        -- Sipariş oluştur
        INSERT INTO orders (customer_id, order_date, total_amount, status)
        VALUES (p_customer_id, NOW(), v_price * p_quantity, 'confirmed');
        SET v_order_id = LAST_INSERT_ID();
        
        INSERT INTO order_items (order_id, product_id, quantity, unit_price)
        VALUES (v_order_id, p_product_id, p_quantity, v_price);
        
        -- Stoku düş
        UPDATE products 
        SET stock_quantity = stock_quantity - p_quantity
        WHERE product_id = p_product_id;
        
        COMMIT;
        SET p_result = CONCAT('Başarılı. Sipariş #', v_order_id);
    END IF;
END //

DELIMITER ;

FOR UPDATE ile stok satırı kilitlenir — Ali ve Zeynep aynı anda satın almaya çalışsa bile, biri bekler. İlk gelen alır, ikinci gelen "yetersiz stok" mesajı alır.


Mevcut İzolasyon Seviyesini Kontrol Etme

-- Mevcut session'ın izolasyon seviyesi
SELECT @@transaction_isolation;
-- REPEATABLE-READ (MySQL varsayılan)

-- Global izolasyon seviyesi
SELECT @@global.transaction_isolation;

-- Değiştirme
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

PostgreSQL Farklılıkları

-- PostgreSQL varsayılan: READ COMMITTED (MySQL: REPEATABLE READ)

-- PostgreSQL'de REPEATABLE READ, serialization failure verebilir
-- MySQL'de snapshot isolation ile daha az conflict olur

-- PostgreSQL: Advisory locks (uygulama seviyesi kilit)
SELECT pg_advisory_lock(12345);  -- Özel kilit numarası
-- İşlemler...
SELECT pg_advisory_unlock(12345);
-- MySQL'de doğrudan karşılığı yok ama GET_LOCK() var:
SELECT GET_LOCK('my_lock', 10);  -- 10 saniye timeout
-- İşlemler...
SELECT RELEASE_LOCK('my_lock');

Özet

  • READ UNCOMMITTED: Commit edilmemiş veriyi okur — neredeyse hiç kullanma

  • READ COMMITTED: Sadece commit edilmiş veriyi okur — Oracle/PostgreSQL varsayılanı

  • REPEATABLE READ: Transaction boyunca aynı sonuç — MySQL InnoDB varsayılanı, phantom read'i de önler

  • SERIALIZABLE: Tam yalıtım, seri çalışma garantisi — en yavaş ama en güvenli

  • FOR UPDATE: Satır kilidi — eş zamanlı stok yönetimi için kritik

  • Deadlock: İki transaction birbirini beklerse oluşur — MySQL otomatik algılar ve bir tarafı rollback eder

  • Çoğu uygulama için MySQL'in varsayılanı (REPEATABLE READ) yeterlidir

Sıkça Yapılan Hatalar

  1. İzolasyon seviyesini bilmeden çalışmak — Varsayılanı bil, ne zaman değiştirmen gerektiğini anla. Çoğu geliştirici bu konuyu hiç düşünmez ve production'da sorun çıkınca şaşırır.

  2. FOR UPDATE kullanmadan stok yönetmek — SELECT + UPDATE ayrı ayrı yapılırsa, iki transaction aynı stoku okuyup ikisi de düşürebilir. FOR UPDATE ile kilitle.

  3. Transaction'ları çok uzun tutmak — Uzun transaction = uzun kilit = diğer işlemlerin beklemesi = yavaş sistem. Transaction'ı kısa tut, içinde uzun hesaplama yapma.

  4. Deadlock'u bug sanmak — Deadlock normal bir durumdur. Uygulamada retry mekanizması olmalı — deadlock hatası alınca transaction'ı yeniden dene.

  5. SERIALIZABLE'ı her yerde kullanmak — "En güvenli, hep onu kullanayım" düşüncesi yanlış. SERIALIZABLE çok yavaştır ve deadlock olasılığını artırır. Sadece gerçekten gerektiğinde kullan.