Aynı anda çalışan iki işlem birbirinin yarım kalmış verisini görebilir mi? Bir raporun ortasında başkası satır eklerse rapor tutarsız çıkar mı? Transaction izolasyon seviyeleri tam olarak bu soruların cevabını belirler. Seçtiğin seviye, veritabanının sana ne kadar tutarlılık garantisi vereceğini ve karşılığında ne kadar eşzamanlılık kaybedeceğini tanımlar. Yanlış seçim ya raporlarını sessizce bozar ya da sunucunu kilitlerle boğar.
Bu yazıda dört standart izolasyon seviyesini, her birinin engellediği ve izin verdiği anomalileri, InnoDB'nin bunları MVCC ile nasıl uyguladığını ve gerçek iş yüklerinde hangisini seçmen gerektiğini anlatacağım. Örnekleri iki oturumlu senaryolar olarak vereceğim, böylece kendi sunucunda birebir deneyip davranışı gözlemleyebilirsin. Sonda ise MySQL'in varsayılan seviyesinin neden REPEATABLE READ olduğunu ve ne zaman değiştirmen gerektiğini tartışacağız.
Önce Anomaliler: Neyden Korunuyoruz#
İzolasyon seviyelerini anlamak için önce engellemeye çalıştıkları dört durumu tanımalısın. Bunların hepsi, aynı veriye aynı anda dokunan iki transaction yüzünden ortaya çıkar.
Dirty read (kirli okuma): Bir transaction, başka bir transaction'ın henüz commit etmediği veriyi okur. Diğer taraf rollback yaparsa, okuduğun değer hiç var olmamış bir veridir.
Non-repeatable read (tekrarlanamayan okuma): Aynı transaction içinde aynı satırı iki kez okursun ve arada başkası commit ettiği için farklı değer görürsün.
Phantom read (hayalet okuma): Aynı transaction içinde aynı koşullu sorguyu iki kez çalıştırırsın ve ikincisinde arada eklenmiş yeni satırlar belirir.
Lost update (kayıp güncelleme): İki transaction aynı satırı okuyup kendi hesabına göre yazar; ikincisi birincinin değişikliğini ezer.
Bu dördü ile seviyeler arasındaki ilişki standartlaştırılmıştır:
| İzolasyon seviyesi | Dirty read | Non-repeatable read | Phantom read |
|---|---|---|---|
| READ UNCOMMITTED | Mümkün | Mümkün | Mümkün |
| READ COMMITTED | Engellenir | Mümkün | Mümkün |
| REPEATABLE READ | Engellenir | Engellenir | Standartta mümkün, InnoDB'de engellenir |
| SERIALIZABLE | Engellenir | Engellenir | Engellenir |
Tablodaki en ilginç hücre InnoDB satırıdır: standart, REPEATABLE READ seviyesinde phantom read'e izin verir; InnoDB ise gap lock mekanizması sayesinde bunu da engeller. Bu, InnoDB'yi standartın gerektirdiğinden daha katı yapar ve MySQL'in varsayılan davranışını anlamanın anahtarıdır.
READ UNCOMMITTED: Neredeyse Hiç Kullanılmaz#
En düşük seviyedir ve commit edilmemiş veriyi okumana izin verir. Pratikte kullanım alanı çok dardır; çünkü okuduğun değerin gerçekten var olacağının hiçbir garantisi yoktur.
-- Oturum A
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;
SELECT bakiye FROM hesap WHERE id = 1; -- 500 döner
-- Oturum B
START TRANSACTION;
UPDATE hesap SET bakiye = 900 WHERE id = 1; -- henüz commit YOK
-- Oturum A tekrar sorar
SELECT bakiye FROM hesap WHERE id = 1; -- 900 görür (kirli okuma)
-- Oturum B vazgeçer
ROLLBACK; -- A, hiç var olmamış bir değeri okumuş oldu
Bu seviyenin tek meşru kullanımı, kesinliğin önemsiz olduğu kaba tahmin sorgularıdır: "şu an yaklaşık kaç kayıt var" gibi. InnoDB'de zaten okuma kilidi almadığı için performans kazancı da modern sürümlerde ihmal edilebilir düzeydedir; MVCC sayesinde READ COMMITTED de okumayı bloke etmez. Yani riski alıp elde ettiğin kazanç neredeyse sıfırdır.
READ COMMITTED: Her Sorgu Kendi Anlık Görüntüsünü Alır#
Bu seviyede yalnızca commit edilmiş veri görünür. Kritik davranış şudur: transaction içindeki her sorgu kendi anlık görüntüsünü (snapshot) oluşturur. Yani aynı transaction içinde iki kez sorarsan, arada başkası commit ettiyse farklı sonuç alırsın.
-- Oturum A
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT SUM(tutar) FROM siparis WHERE durum = 1; -- 12500
-- Oturum B: yeni sipariş ekler ve commit eder
INSERT INTO siparis (musteri_id, durum, tutar) VALUES (7, 1, 300);
COMMIT;
-- Oturum A aynı sorguyu tekrarlar
SELECT SUM(tutar) FROM siparis WHERE durum = 1; -- 12800 (değişti)
Bu davranış rapor üretirken sorun yaratır ama yoğun eşzamanlı yazma yapan sistemlerde büyük bir avantaj sağlar: gap lock alınmadığı için kilit çekişmesi ve deadlock sıklığı belirgin biçimde düşer. PostgreSQL, Oracle ve SQL Server'ın varsayılanı da bu seviyedir; MySQL'den gelen alışkanlıkla REPEATABLE READ bekleyen geliştiriciler bu farkı sık sık gözden kaçırır.
READ COMMITTED ayrıca satır kilitlerinde şu farkı getirir: bir UPDATE sorgusu koşula uymayan satırların kilidini hemen bırakır. REPEATABLE READ seviyesinde ise taranan satırların kilidi transaction sonuna kadar tutulur. Bu, eksik index'li bir UPDATE sorgusunun yarattığı hasarı azaltır. Kilit davranışının deadlock ile ilişkisini MySQL deadlock çözümü yazısında ayrıntılı ele aldım.
REPEATABLE READ: MySQL'in Varsayılanı#
InnoDB'nin varsayılan seviyesidir ve anlık görüntü transaction'ın ilk okumasında bir kez oluşturulur; transaction boyunca sabit kalır. Yani yukarıdaki senaryoyu bu seviyede tekrarlarsan, Oturum A her iki sorguda da 12500 görür — dışarıda ne olursa olsun.
-- Oturum A
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
SELECT SUM(tutar) FROM siparis WHERE durum = 1; -- 12500 (snapshot alındı)
-- B ekleyip commit etse bile:
SELECT SUM(tutar) FROM siparis WHERE durum = 1; -- yine 12500
COMMIT;
Bu tutarlılık, çok adımlı raporlar ve mutabakat işlemleri için paha biçilmezdir: rapor boyunca dünya donmuş gibi davranır. Ama bir tuzağı vardır ve bu tuzak çok kişiyi yakalar: okuma anlık görüntüden, yazma ise güncel veriden yapılır.
START TRANSACTION;
SELECT stok FROM urun WHERE id = 5; -- snapshot: 10 görüyorsun
-- Bu arada başkası stoğu 3'e düşürüp commit etti
UPDATE urun SET stok = stok - 1 WHERE id = 5; -- GÜNCEL veriye uygulanır: 3 -> 2
SELECT stok FROM urun WHERE id = 5; -- 2 görürsün, 9 değil
COMMIT;
Yani gördüğün değere göre karar verip yazma yaparsan mantık hatası üretirsin. Doğru yöntem, karar vereceğin satırı SELECT ... FOR UPDATE ile okumaktır; bu sorgu snapshot'ı değil güncel veriyi okur ve satırı kilitler:
START TRANSACTION;
SELECT stok FROM urun WHERE id = 5 FOR UPDATE; -- güncel değeri okur ve kilitler
-- stok yeterliyse:
UPDATE urun SET stok = stok - 1 WHERE id = 5;
COMMIT;
REPEATABLE READ seviyesinde InnoDB, phantom read'i gap lock ile engeller. Bu, standartın ötesine geçen bir garantidir ama bedeli deadlock olasılığının artmasıdır; özellikle aralık taraması yapan FOR UPDATE sorgularında.
SERIALIZABLE: En Katı ve En Pahalı#
En yüksek seviyedir ve transaction'ları sanki arka arkaya (seri) çalışıyormuş gibi davranmaya zorlar. InnoDB bunu, düz SELECT sorgularını bile örtük olarak LOCK IN SHARE MODE gibi davranmaya zorlayarak sağlar. Yani okuma bile kilit alır.
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
START TRANSACTION;
SELECT * FROM hesap WHERE id = 1; -- artık paylaşımlı kilit alır
-- Başka bir oturum bu satırı UPDATE edemez, bekler
COMMIT;
Sonuç, tam tutarlılık ama ciddi ölçüde düşen eşzamanlılıktır. Web uygulamalarında neredeyse hiç kullanılmaz; kullanım alanı, mutlak doğruluğun performanstan önemli olduğu kısa ve nadir işlemlerdir — muhasebe kapanışı, stok mutabakatı gibi. Bu seviyede deadlock ve lock wait timeout hataları belirgin biçimde artar, bu yüzden uygulamanın yeniden deneme mantığı olmadan kullanılması önerilmez.
Hangi Seviyeyi Seçmelisin#
Karar, iş yükünün doğasına göre verilir. Aşağıdaki tablo sahada işe yarayan pratik bir kılavuzdur:
| İş yükü | Önerilen seviye | Gerekçe |
|---|---|---|
Yoğun INSERT alan e-ticaret / log sistemi | READ COMMITTED | Gap lock olmadığı için deadlock azalır |
| Klasik CRUD web uygulaması | REPEATABLE READ (varsayılan) | Tutarlılık iyi, çekişme kabul edilebilir |
| Çok adımlı rapor / mutabakat | REPEATABLE READ | Rapor boyunca sabit anlık görüntü |
| Finansal kapanış, kritik stok işlemi | SERIALIZABLE (kısa transaction ile) | Mutlak doğruluk gerekli |
| Kaba istatistik, yaklaşık sayım | READ COMMITTED | READ UNCOMMITTED'ın riskine gerek yok |
Seviyeyi üç kapsamda ayarlayabilirsin ve doğru kapsamı seçmek önemlidir:
-- 1) Yalnızca bir sonraki transaction için
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 2) Bu oturumun tamamı için
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 3) Tüm sunucu için (yeni bağlantıları etkiler)
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- Şu anki seviyeyi öğren
SELECT @@transaction_isolation, @@global.transaction_isolation;
Kalıcı yapmak için yapılandırma dosyasına yazmalısın; aksi hâlde sunucu yeniden başladığında varsayılana döner:
[mysqld]
transaction_isolation = READ-COMMITTED
Bu değeri değiştirmeden önce replikasyon biçimini kontrol et: READ COMMITTED ile ifade tabanlı (STATEMENT) binlog birlikte kullanılamaz, satır tabanlı (ROW) biçime geçmen gerekir. Yapılandırma dosyasında bu tür bağımlılıkları nasıl yöneteceğini my.cnf optimizasyon rehberi yazısında anlattım.
Sık Yapılan Hatalar#
En yaygın hata, izolasyon seviyesinin lost update'i engellediğini sanmaktır. Hiçbir seviye, REPEATABLE READ dahil, "oku-hesapla-yaz" kalıbını otomatik olarak güvenli hâle getirmez. Stok düşürme, bakiye güncelleme veya sayaç artırma gibi işlemlerde ya satırı FOR UPDATE ile kilitlemeli ya da hesabı veritabanında yaptırmalısın:
-- Güvenli: hesap veritabanında yapılır, okuma-yazma arası boşluk yok
UPDATE urun SET stok = stok - 1 WHERE id = 5 AND stok >= 1;
-- Etkilenen satır 0 ise stok yetmemiştir
İkinci hata, transaction'ı açık bırakıp uzun süre iş yapmaktır. REPEATABLE READ seviyesinde açık bir transaction, anlık görüntüsünü koruyabilmek için InnoDB'yi eski satır sürümlerini (undo log) tutmaya zorlar. Saatlerce açık kalan bir transaction, undo tablespace'in şişmesine ve tüm sunucunun yavaşlamasına yol açar. Uzun süredir açık transaction'ları düzenli kontrol et:
SELECT trx_id, trx_started,
TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS saniye, trx_query
FROM information_schema.innodb_trx
WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 60;
Üçüncü hata, otomatik commit davranışını unutmaktır. MySQL varsayılan olarak autocommit = 1 ile çalışır; yani START TRANSACTION yazmadığın her sorgu kendi başına bir transaction'dır ve izolasyon seviyesi tek sorgu kapsamında anlamsız kalır. ORM kullanıyorsan çerçevenin transaction'ı gerçekten açtığını doğrula. Son olarak, depolama motorunun bu garantilerin tamamını sağladığını varsayma: MyISAM tablolarda transaction diye bir kavram yoktur ve izolasyon seviyesi hiçbir etki yaratmaz. Motor farklarını InnoDB ve MyISAM karşılaştırması yazısında bulabilirsin.
Sıkça Sorulan Sorular#
MySQL'in varsayılan izolasyon seviyesi nedir#
InnoDB kullanan MySQL ve MariaDB kurulumlarında varsayılan REPEATABLE READ'dir. Bu, PostgreSQL, Oracle ve SQL Server'ın varsayılanı olan READ COMMITTED'dan farklıdır ve platformlar arası geçişte sürprize yol açabilir. Mevcut değeri SELECT @@transaction_isolation; ile kontrol edebilir, kalıcı olarak değiştirmek istersen my.cnf içine transaction_isolation satırını eklemelisin.
READ COMMITTED'a geçmek performansı artırır mı#
Yoğun eşzamanlı yazma yapan sistemlerde genellikle evet, çünkü gap lock alınmaz ve kilit çekişmesi ile deadlock sıklığı düşer. Ayrıca koşula uymayan satırların kilidi hemen bırakılır. Buna karşılık aynı transaction içinde tekrar eden okumaların tutarlılığını kaybedersin; çok adımlı raporlar yanlış sonuç üretebilir. Geçişten önce raporlama kodunun bu değişiklikten etkilenip etkilenmediğini gözden geçirmelisin.
Phantom read InnoDB'de gerçekten olmuyor mu#
Düz okumalarda (SELECT) olmaz, çünkü REPEATABLE READ seviyesinde okuma sabit bir anlık görüntüden yapılır. Kilitli okumalarda (SELECT ... FOR UPDATE) ise InnoDB gap lock kullanarak yeni satır eklenmesini fiziksel olarak engeller. Yani InnoDB, SQL standardının bu seviyede izin verdiği phantom read'i de kapatır. Bunun bedeli, aralık taraması yapan kilitli sorguların deadlock üretme olasılığının artmasıdır.
İzolasyon seviyesini transaction ortasında değiştirebilir miyim#
Hayır. SET TRANSACTION ISOLATION LEVEL komutu, açık bir transaction varken çalıştırılamaz; MySQL hata verir. Seviyeyi transaction başlamadan önce belirlemen gerekir. Uygulama tarafında en temiz yöntem, bağlantı havuzundan alınan her bağlantı için oturum seviyesini bir kez ayarlamak ya da sunucu genelinde varsayılanı doğru değere getirmektir.
SELECT FOR UPDATE ne zaman kullanmalıyım#
Okuduğun değere göre karar verip aynı transaction içinde yazma yapacaksan kullanmalısın. Klasik örnek stok kontrolüdür: stoğu okuyup yeterliyse düşürmek. FOR UPDATE olmadan, REPEATABLE READ seviyesinde okuduğun değer eski bir anlık görüntüden gelir ve karar yanlış olur. Alternatif olarak koşulu doğrudan UPDATE sorgusunun içine koyup etkilenen satır sayısına bakmak da güvenli ve genelde daha performanslı bir çözümdür.
SERIALIZABLE kullanmak veritabanını kilitler mi#
Tam olarak kilitlemez ama eşzamanlılığı ciddi biçimde düşürür, çünkü düz okumalar bile paylaşımlı kilit almaya başlar. Web trafiği altındaki bir uygulamada bu seviye hızla lock wait timeout hatalarına yol açar. Kullanacaksan yalnızca çok kısa süren, nadir çalışan ve mutlak doğruluk gerektiren işlemlerde kullan; ayrıca uygulamanın hatayı yakalayıp yeniden deneyen bir mantığı mutlaka olmalı.
İzolasyon seviyesi replikasyonu etkiler mi#
Evet. READ COMMITTED seviyesi, ifade tabanlı (STATEMENT) ikili günlük biçimiyle birlikte kullanılamaz; çünkü aynı ifade kopya sunucuda farklı sonuç üretebilir. Bu kombinasyonda MySQL hata verir. binlog_format = ROW kullanıyorsan sorun yoktur ve modern kurulumlarda satır tabanlı biçim zaten varsayılandır. Replikasyon kurmadan önce bu iki ayarın uyumlu olduğunu doğrulaman gerekir.
Kapanış#
İzolasyon seviyelerini pratik birkaç kurala indirgeyebilirsin: varsayılan REPEATABLE READ çoğu uygulama için doğru dengedir, yoğun eşzamanlı yazma varsa READ COMMITTED deadlock'ları azaltır, hiçbir seviye "oku-hesapla-yaz" kalıbını tek başına güvenli yapmaz ve transaction'ı ne kadar kısa tutarsan o kadar az sorun yaşarsın. Karar verirken hangi anomaliyi engellemek istediğini net biçimde adlandır; seviye seçimi ondan sonra kendiliğinden gelir.
Tutarlılık garantilerinin bedeli her zaman kaynak tüketimidir; kilit bekleyen bağlantılar bellek ve CPU tutar. Veritabanını izole ve garanti edilen kaynaklarla çalıştırmak istersen VDS ve bulut sunucu paketlerimiz uygun bir zemin sunar; kritik veri tutan sistemlerde yedekleme hizmetimizle noktasal geri dönüş imkânı elde edebilirsin. Ayar ve izleme yükünü devretmek isterseniz sunucu yönetimi hizmetimiz bu tarafı da üstlenir.