İçeriğe geç
Çılgın Yazılım

Veritabanı ve Performans kod örnekli

Veritabanı Kilitlenmesi (Deadlock) Neden Olur, Nasıl Önlenir?

Deadlock rastgele bir talihsizlik değildir; neredeyse her zaman tutarsız kilit sırasının sonucudur. Nedenini ve çözümünü görelim.

Çılgın Yazılım 4 dk okuma

Veritabanı Kilitlenmesi (Deadlock) Neden Olur, Nasıl Önlenir?
İçindekiler
  1. Klasik senaryo
  2. Çözüm 1: sabit kilit sırası
  3. Çözüm 2: transaction'ı kısa tutun
  4. Çözüm 3: eksik indeksleri kapatın
  5. Deadlock günlüğünü okumak
  6. Yeniden deneme: kaçınılmaz olanı yönetmek
  7. İzolasyon seviyesiyle ilişkisi
  8. Sonuç

Veritabanı kilitlenme (deadlock), iki işlemin birbirinin tuttuğu kilidi beklemesi ve hiçbirinin ilerleyememesidir. Üretimde ortaya çıktığında rastgele ve öngörülemez görünür; oysa neredeyse her zaman tek bir sebebi vardır: satırlara farklı sıralarda dokunmak.

Kilitlenmenin dört koşulu
Dört şarttan yalnızca birini kırmak yeterlidir — en pratik olanı döngüsel beklemedir.

Klasik senaryo

İki eşzamanlı para transferi düşünün. Birinci işlem A hesabını kilitleyip B'yi bekler, ikinci işlem B'yi kilitleyip A'yı bekler. Her ikisi de sonsuza kadar bekleyecektir — veritabanı devreye girip birini kurban seçer ve geri alır.

SQL
-- Oturum 1
START TRANSACTION;
UPDATE hesaplar SET bakiye = bakiye - 100 WHERE id = 1;   -- 1 kilitlendi
UPDATE hesaplar SET bakiye = bakiye + 100 WHERE id = 2;   -- 2 bekleniyor

-- Oturum 2 (aynı anda)
START TRANSACTION;
UPDATE hesaplar SET bakiye = bakiye - 50 WHERE id = 2;    -- 2 kilitlendi
UPDATE hesaplar SET bakiye = bakiye + 50 WHERE id = 1;    -- 1 bekleniyor → DEADLOCK

Çözüm 1: sabit kilit sırası

Kilitlenmenin dört şartından en kolay kırılanı döngüsel beklemedir. Tüm kod yollarında satırlara aynı sırayla dokunursanız döngü oluşamaz. Pratikte bu, birincil anahtara göre sıralamak demektir.

PHP
public function transfer(int $kaynakId, int $hedefId, int $kurus): void
{
    // Kilitleri HER ZAMAN artan kimlik sırasında al — döngüsel bekleme imkânsız hâle gelir
    $sirali = [$kaynakId, $hedefId];
    sort($sirali);

    $db = db_connect();
    $db->transException(true)->transStart();

    foreach ($sirali as $id) {
        $db->query('SELECT bakiye FROM hesaplar WHERE id = ? FOR UPDATE', [$id]);
    }

    $db->query('UPDATE hesaplar SET bakiye = bakiye - ? WHERE id = ?', [$kurus, $kaynakId]);
    $db->query('UPDATE hesaplar SET bakiye = bakiye + ? WHERE id = ?', [$kurus, $hedefId]);

    $db->transComplete();
}

Çözüm 2: transaction'ı kısa tutun

Kilit süresi ne kadar uzunsa çakışma olasılığı o kadar yüksektir. En sık görülen hata, transaction içinde yavaş bir işlem yapmaktır: e-posta göndermek, dış API çağırmak, dosya yazmak. Bunların hiçbiri kilitli bölgede olmamalıdır.

Transaction içinde ağ çağrısı yapmayın. Üçüncü parti servis 8 saniye yanıt vermezse, o süre boyunca satır kilitleri tutulur ve tüm sistem yavaşlar.

Çözüm 3: eksik indeksleri kapatın

Az bilinen ama çok yaygın bir sebep budur. İndekssiz bir WHERE koşuluyla güncelleme yaptığınızda InnoDB, taranan tüm satırlara kilit koyabilir. Yani tek satır güncellediğinizi sanırken binlerce satırı kilitlemiş olursunuz. EXPLAIN çıktısını okumak, bu durumu doğrudan görünür kılar.

Deadlock günlüğünü okumak

Tahmin yürütmek yerine veritabanına sorun. InnoDB son kilitlenmenin tam dökümünü tutar.

SQL
SHOW ENGINE INNODB STATUS\G
-- Çıktıdaki "LATEST DETECTED DEADLOCK" bölümü şunları verir:
--   (1) TRANSACTION: hangi sorgu, hangi kilidi bekliyordu
--   (2) TRANSACTION: karşı taraf hangi kilidi tutuyordu
--   WE ROLL BACK TRANSACTION (n): hangisi kurban seçildi

Bu çıktıdaki iki sorguyu yan yana koyduğunuzda kilit sırası farkı genellikle ilk bakışta görünür.

Kilit süresini kısaltmanın bir başka yolu, aynı isteğin tekrar işlenmesini engellemektir; idempotency anahtarı bu tekrarları en baştan eler.

Yeniden deneme: kaçınılmaz olanı yönetmek

Sabit sıralamaya rağmen yoğun sistemlerde kilitlenme tamamen sıfırlanmayabilir. Bu yüzden uygulama katmanında sınırlı bir yeniden deneme mantığı bulunmalıdır — kilitlenme geri alınmış bir işlemdir, yani güvenle tekrarlanabilir.

PHP
function kilitlenmeyeDayanikli(callable $is, int $maxDeneme = 3)
{
    for ($deneme = 1; ; $deneme++) {
        try {
            return $is();
        } catch (\Throwable $e) {
            // 1213: deadlock, 1205: lock wait timeout
            $kilit = str_contains($e->getMessage(), '1213') || str_contains($e->getMessage(), '1205');

            if (! $kilit || $deneme >= $maxDeneme) {
                throw $e;
            }

            usleep(random_int(20_000, 120_000) * $deneme);  // rastgele geri çekilme
        }
    }
}

Geri çekilme süresine rastgelelik eklenmesi kritiktir: sabit bekleme, çakışan iki işlemin aynı anda tekrar denemesine ve aynı çarpışmayı yaşamasına yol açar.

Kod tarafında kilit sırasını tutarlı tutmanın en kolay yolu, biçim ve yapı kurallarını araca bırakmaktır; EditorConfig ve biçimlendirici bu zemini kurar.

İzolasyon seviyesiyle ilişkisi

Daha yüksek izolasyon seviyeleri daha çok kilit anlamına gelir; SERIALIZABLE altında kilitlenme olasılığı belirgin biçimde artar. Uygulamanızın gerçekten hangi seviyeye ihtiyacı olduğunu izolasyon seviyeleri yazımızda karşılaştırmıştık. Kilit türlerinin resmî tanımı için MySQL InnoDB kilitleme belgeleri başvurulacak kaynaktır. Göç sırasında oluşan uzun kilitler için sıfır kesintili göç yaklaşımı da doğrudan ilgilidir.

Sonuç

Kilitlenme bir talih meselesi değil, tasarım sonucudur. Dört önlem çoğu vakayı kapatır: satırlara her zaman aynı sırayla dokunun, transaction'ları kısa tutup içine ağ çağrısı koymayın, güncelleme koşullarınızın indeksli olduğundan emin olun ve kaçınılmaz durumlar için rastgele geri çekilmeli yeniden deneme ekleyin. Üretimde bir kilitlenme gördüğünüzde ilk işiniz InnoDB durum dökümünü almak olsun; iki sorguyu yan yana koyduğunuzda sıralama farkı genellikle kendini gösterir ve düzeltme birkaç satırlık bir değişikliğe iner.

Beğeniniz benzer içerikleri öne çıkarmamıza yardımcı olur.

Sık sorulan sorular

Kilitlenme veri kaybına yol açar mı?

Hayır. Veritabanı kurban seçtiği işlemi tamamen geri alır, yani yarım kalmış bir değişiklik kalmaz. Asıl risk, uygulamanın bu hatayı yakalamayıp kullanıcıya belirsiz bir hata göstermesi ya da işlemi sessizce kaybetmesidir.

Kilitlenme ile lock wait timeout aynı şey mi?

Değil. Kilitlenmede döngüsel bekleme vardır ve veritabanı bunu anında tespit edip birini geri alır. Lock wait timeout ise tek yönlü uzun bir beklemedir; kimse döngüde değildir, sadece kilidi tutan işlem çok uzun sürmektedir. Çözümleri de farklıdır.

Kilit sırasını kodun her yerinde nasıl garanti ederim?

Kilitleme mantığını tek bir yardımcı fonksiyonda toplamak en güvenilir yoldur. Her yerde elle sıralamaya güvenmek, yeni yazılan bir kod yolunda kolayca bozulur. Kod incelemesinde "birden fazla satır kilitleniyor mu?" sorusunu standart kontrol maddesi yapmak da işe yarar.

Yeniden deneme sayısı kaç olmalı?

Üç genellikle yeterlidir. Daha fazlası, altta yatan tasarım sorununu gizler ve yoğunluk anında sistemi daha da yorar. Yeniden deneme sayısını ölçün: sürekli üçüncü denemeye kadar gidiliyorsa sorun kilit sırasındadır, yeniden denemede değil.

Yazan

Çılgın Yazılım

Ben Evren Çılgın; yazılım geliştirme, web teknolojileri ve sistem tasarımı alanlarında uzmanlaşmış bir geliştiriciyim. Uzun yıllardır hem frontend hem de backend tarafında üretken, ölçeklenebilir ve kullanıcı dostu çözümler üretiyorum.

Hakkımızda Bize ulaşın

Bu konuda bir projeniz mi var?

İhtiyacınızı birkaç cümleyle anlatın; uygun yaklaşımı birlikte belirleyelim.

Projenizi Anlatın

İlgili yazılar

Yorumlar

Henüz yorum yok. Sorunuzu ya da deneyiminizi ilk siz yazın.

Yorum yazın

Yorumunuz onaylandıktan sonra yayınlanır. Ekibimiz gerekirse konuyla ilgili bir yanıt da paylaşır.

Yayınlanmaz; yalnızca gerektiğinde size ulaşmak için.

En az 10 karakter.

Tüm yazılar