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 alTransaction, 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ürMySQL 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ışırNe 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
| Özellik | READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE |
|---|---|---|---|---|
| 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 seviyesi | Yok | Satır kilidi | Satır + snapshot | Aralık kilidi |
| Varsayılan | — | Oracle, PostgreSQL | MySQL 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 bekliyorMySQL deadlock'ı otomatik algılar ve bir transaction'ı rollback eder:
ERROR 1213 (40001): Deadlock found when trying to get lock;
try restarting transactionDeadlock Ö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 deneLocking — 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ırSELECT ... 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 SELECT | Diğer SELECT FOR SHARE | Diğer FOR UPDATE | Diğ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
İ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.
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.
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.
Deadlock'u bug sanmak — Deadlock normal bir durumdur. Uygulamada retry mekanizması olmalı — deadlock hatası alınca transaction'ı yeniden dene.
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.
AI Asistan
Sorularını yanıtlamaya hazır