Veritabanı Yönetimi

    MySQL Deadlock Nedir, Nasıl Çözülür

    InnoDB deadlock kayıtlarını okuyup kilit sırasını düzelterek kalıcı çözüm üretme rehberi.

    10 dk okuma Güncellendi: 25 Ağustos 2026

    Uygulama günlüğünde Deadlock found when trying to get lock; try restarting transaction satırını ilk gördüğünde genelde iki tepki verilir: ya görmezden gelinir ("nadiren oluyor") ya da paniğe kapılıp veritabanının bozulduğu sanılır. İkisi de yanlıştır. MySQL deadlock, InnoDB'nin bir arızası değil, iki transaction'ın birbirinin kilidini beklediğini fark edip birini iptal etmesi yani bir koruma mekanizmasıdır. Alternatifi, iki oturumun sonsuza kadar birbirini beklemesi olurdu.

    Yine de sık tekrarlayan deadlock'lar gerçek bir tasarım sorununun belirtisidir ve göz ardı edilirse yoğun saatlerde sipariş kaybına kadar gidebilir. Bu yazıda deadlock'un tam olarak nasıl oluştuğunu, SHOW ENGINE INNODB STATUS çıktısının hangi satırlarının okunması gerektiğini, eksik index'lerin deadlock üretmedeki payını, gap lock davranışını ve hem uygulama hem şema tarafında kalıcı çözümleri anlatacağım. Ayrıca sıkça karıştırılan Lock wait timeout exceeded hatasıyla farkını netleştireceğiz.

    Deadlock Nasıl Oluşur: En Küçük Örnek#

    Deadlock için iki transaction'ın aynı iki kaynağı ters sırayla kilitlemesi yeterlidir. Aşağıdaki örnek bunu en yalın haliyle gösterir; iki farklı MySQL oturumunda adım adım çalıştırırsan hatayı kendin üretebilirsin.

    -- Oturum A
    START TRANSACTION;
    UPDATE hesap SET bakiye = bakiye - 100 WHERE id = 1;   -- id=1 kilitlendi
    -- (burada bekle)
    UPDATE hesap SET bakiye = bakiye + 100 WHERE id = 2;   -- id=2 için bekler
    
    -- Oturum B (aynı anda)
    START TRANSACTION;
    UPDATE hesap SET bakiye = bakiye - 50 WHERE id = 2;    -- id=2 kilitlendi
    UPDATE hesap SET bakiye = bakiye + 50 WHERE id = 1;    -- id=1 için bekler -> DEADLOCK
    

    A oturumu 1'i tutup 2'yi bekler, B oturumu 2'yi tutup 1'i bekler. InnoDB bu döngüyü tespit eder ve daha az iş yapmış transaction'ı kurban seçip geri alır (rollback). Kurban seçimi genelde daha az satır değiştirmiş, yani geri alınması daha ucuz olan taraftır. Diğer transaction sorunsuz devam eder.

    Buradan çıkan ilk kural şudur: aynı kaynaklara her zaman aynı sırayla dokun. Yukarıdaki örnekte her iki oturum da id'leri küçükten büyüğe kilitleseydi deadlock imkânsız olurdu. Bu, uygulama kodunda uygulanabilecek en ucuz ve en etkili çözümdür.

    SHOW ENGINE INNODB STATUS Çıktısını Okumak#

    Deadlock oluştuğunda InnoDB son olayın ayrıntısını bellekte tutar. Teşhis her zaman buradan başlar:

    SHOW ENGINE INNODB STATUS\G
    -- Çıktıda "LATEST DETECTED DEADLOCK" başlığını ara
    

    Çıktı ilk bakışta ürkütücüdür ama aslında yalnızca beş bilgi taşır ve gerisi gürültüdür:

    Aranan satırNe söyler
    TRANSACTION ... ACTIVE n secTransaction ne kadar süredir açık; uzunsa tasarım sorunu
    WAITING FOR THIS LOCK TO BE GRANTEDO oturumun beklediği kilit ve hangi index üzerinde
    HOLDS THE LOCK(S)O oturumun elinde tuttuğu kilit
    index ... of table ...Kilit hangi index üzerinde alınmış; PRIMARY değilse ipucu
    WE ROLL BACK TRANSACTION (n)Hangi tarafın kurban seçildiği

    Tipik bir bölüm şuna benzer:

    *** (1) TRANSACTION:
    TRANSACTION 84521, ACTIVE 6 sec starting index read
    mysql tables in use 1, locked 1
    LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s)
    UPDATE hesap SET bakiye = bakiye + 100 WHERE id = 2
    
    *** (1) WAITING FOR THIS LOCK TO BE GRANTED:
    RECORD LOCKS space id 42 page no 4 n bits 80 index PRIMARY of table `sirket`.`hesap`
    trx id 84521 lock_mode X locks rec but not gap waiting
    
    *** WE ROLL BACK TRANSACTION (1)
    

    Burada bakılacak en değerli iki alan index PRIMARY of table ve lock_mode X locks rec but not gap ifadeleridir. İlki kilidin hangi index üzerinden alındığını söyler; ikincisi kilidin türünü. locks gap before rec görüyorsan aralık kilidi devrededir ve bu, satır bazlı bir çakışma değil aralık çakışmasıdır — sebebi neredeyse her zaman eksik ya da seçici olmayan bir index'tir.

    Deadlock'ları tek tek yakalamak yerine hepsini hata günlüğüne yazdırabilirsin. Üretimde bunu geçici olarak açmak teşhisi çok kolaylaştırır:

    [mysqld]
    innodb_print_all_deadlocks = ON   ; her deadlock error.log'a yazılır
    

    Bu ayarı sürekli açık bırakma; çok sık deadlock alan bir sistemde günlük dosyası hızla şişer.

    Eksik Index'ler Deadlock'u Nasıl Üretir#

    En çok gözden kaçan sebep budur. InnoDB satır kilitlerini tablonun kendisine değil, index kayıtlarına koyar. Bir UPDATE sorgusu uygun index'i bulamazsa tam tarama yapar ve taradığı her satırı kilitler. Yani WHERE durum = 2 sorgusu için index yoksa, teoride tek bir satırı güncelleyecek olan bir işlem tablodaki milyonlarca satırı kilitler. İki böyle sorgu aynı anda çalıştığında deadlock kaçınılmaz olur.

    -- Index yoksa bu sorgu tabloyu tarar ve taradığı her satırı kilitler
    UPDATE siparis SET durum = 3 WHERE kargo_kodu = 'TR12345';
    
    -- Çözüm: filtreyi karşılayan index
    ALTER TABLE siparis ADD KEY ix_kargo (kargo_kodu);
    

    Kilit maliyetini görmek için sorgunun planına bakmak yeterlidir; EXPLAIN çıktısında type: ALL görüyorsan o UPDATE tabloyu kilitliyor demektir. Plan okumanın ayrıntısını MySQL EXPLAIN ile sorgu analizi yazısında, hangi kolona index açılacağını ise SQL index tasarımı yazısında anlattım.

    Bir başka kritik nokta ikincil index üzerinden yapılan güncellemelerdir. İkincil index üzerinden bulunan satır için InnoDB hem ikincil index kaydını hem birincil anahtar kaydını kilitler. Bu, kilit alma sırasının sorgudan sorguya değişebileceği anlamına gelir ve fark edilmesi zor deadlock'lar üretir. Çözüm yine aynıdır: mümkün olduğunca birincil anahtar üzerinden güncelleme yap.

    Gap Lock ve İzolasyon Seviyesinin Rolü#

    InnoDB'nin varsayılan izolasyon seviyesi REPEATABLE READ'dir ve bu seviyede "hayalet satır" (phantom read) oluşmasını engellemek için yalnızca var olan satırlar değil, satırlar arasındaki boşluklar da kilitlenir. Buna gap lock denir ve deadlock kaynaklarının önemli bir bölümünü oluşturur.

    -- REPEATABLE READ altında bu sorgu 100-200 arasındaki BOŞLUĞU da kilitler
    SELECT * FROM siparis WHERE id BETWEEN 100 AND 200 FOR UPDATE;
    -- Başka bir oturumun id=150 ile INSERT yapması engellenir
    

    Aynı sorguyu READ COMMITTED seviyesinde çalıştırırsan gap lock alınmaz, yalnızca gerçekten eşleşen satırlar kilitlenir. Bu yüzden yoğun INSERT yapan sistemlerde izolasyon seviyesini READ COMMITTED'a çekmek deadlock sıklığını belirgin biçimde azaltır. Bedeli ise tekrarlanabilir okuma garantisini kaybetmektir; aynı transaction içinde iki kez aynı sorguyu çalıştırırsan farklı sonuç alabilirsin.

    -- Oturum bazında dene
    SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
    
    -- Kalıcı yapmak için my.cnf
    -- [mysqld]
    -- transaction_isolation = READ-COMMITTED
    

    Bu kararı vermeden önce iki seviyenin garantilerini iyi bilmelisin; ayrıntılı karşılaştırmayı transaction izolasyon seviyeleri yazısına ayırdım. Kısaca: raporlama tutarlılığı kritikse REPEATABLE READ kal, yoğun eşzamanlı yazma varsa READ COMMITTED genelde daha iyi bir denge kurar.

    Deadlock ile Lock Wait Timeout Arasındaki Fark#

    Bu ikisi sürekli karıştırılır ama farklı sorunlardır ve farklı çözümler ister.

    DeadlockLock wait timeout
    Hata metniDeadlock found when trying to get lockLock wait timeout exceeded; try restarting transaction
    SebepKarşılıklı bekleme döngüsüTek yönlü, uzun süren bir kilit beklemesi
    Tespit süresiAnında (InnoDB döngüyü görür)innodb_lock_wait_timeout kadar (varsayılan 50 sn)
    KurbanInnoDB seçer, otomatik rollbackBekleyen sorgu iptal olur
    Kalıcı çözümKilit sırasını sabitle, index ekleTransaction'ları kısalt, uzun süreni bul

    Lock wait timeout alıyorsan kimin beklettiğini bulman gerekir. MySQL 8'de bunu tek sorguyla görebilirsin:

    -- Kim kimi bekletiyor (MySQL 8.0+)
    SELECT r.trx_id AS bekleyen, r.trx_mysql_thread_id AS bekleyen_thread,
           r.trx_query AS bekleyen_sorgu,
           b.trx_id AS engelleyen, b.trx_mysql_thread_id AS engelleyen_thread,
           b.trx_query AS engelleyen_sorgu
    FROM performance_schema.data_lock_waits w
    JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_engine_transaction_id
    JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_engine_transaction_id;
    
    -- Uzun süredir açık transaction'lar
    SELECT trx_id, trx_started, TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS saniye,
           trx_rows_locked, trx_query
    FROM information_schema.innodb_trx
    ORDER BY trx_started;
    

    Sonuçta engelleyen bir oturum varsa ve gerçekten takılmışsa KILL thread_id ile sonlandırabilirsin; ama bu bir teşhis değil ilk yardımdır. Aynı oturum yarın yine takılacaksa asıl iş, o transaction'ın neden bu kadar uzun açık kaldığını bulmaktır.

    Kalıcı Çözümler: Uygulama ve Şema Tarafı#

    Deadlock'u tamamen sıfırlamak çoğu sistemde gerçekçi bir hedef değildir; hedef, sıklığını ihmal edilebilir seviyeye indirmek ve oluştuğunda uygulamanın düzgün davranmasını sağlamaktır. Sırasıyla uygulanacak adımlar şunlardır:

    1. Kilit sırasını sabitle. Toplu güncellemelerde satırları her zaman aynı kritere göre sırala: UPDATE ... WHERE id IN (...) yerine id'leri küçükten büyüğe sıralayıp tek tek işle ya da ORDER BY id ile deterministik hale getir.
    2. Transaction'ları kısalt. START TRANSACTION ile COMMIT arasında HTTP isteği, dosya yazma, e-posta gönderme gibi işler yapma. Kilit süresi ne kadar kısaysa çakışma olasılığı o kadar düşer.
    3. Eksik index'leri kapat. Her UPDATE ve DELETE sorgusunun bir index kullandığını EXPLAIN ile doğrula.
    4. İzolasyon seviyesini gözden geçir. Yoğun INSERT varsa READ COMMITTED gap lock'ları ortadan kaldırır.
    5. Yeniden deneme mantığı ekle. Deadlock kurbanı olan transaction'ı otomatik olarak bir-iki kez yeniden dene; bu, kabul edilmiş standart bir uygulamadır.

    Yeniden deneme mantığı olmadan hiçbir çözüm tamamlanmış sayılmaz, çünkü eşzamanlı bir sistemde deadlock her zaman olabilir. Basit bir kalıp şöyledir:

    -- Sözde kod mantığı: hata kodu 1213 (deadlock) yakalanırsa kısa bekleyip tekrar dene
    -- deneme = 0
    -- while deneme < 3:
    --     try: START TRANSACTION; ... ; COMMIT; break
    --     catch 1213: ROLLBACK; sleep(rastgele 50-200 ms); deneme += 1
    

    Beklemeyi sabit değil rastgele yapmak önemlidir; sabit bekleme, aynı iki işlemin aynı anda tekrar çarpışmasına yol açar. Ayrıca yalnızca 1213 (deadlock) ve 1205 (lock wait timeout) hata kodlarını yeniden dene; diğer hataları tekrarlamak veri tutarlılığını bozabilir.

    Sıkça Sorulan Sorular#

    Deadlock veriyi bozar mı#

    Hayır. InnoDB deadlock tespit ettiğinde kurban seçtiği transaction'ı bütünüyle geri alır, yani o transaction hiç çalışmamış gibi olur. Yarım kalmış bir güncelleme diskte kalmaz ve tutarlılık korunur. Tek kayıp, o işlemin yapılmamış olmasıdır; bu yüzden uygulamanın hatayı yakalayıp işlemi yeniden denemesi gerekir. Yeniden deneme yoksa kullanıcı açısından işlem başarısız görünür.

    Deadlock hatası ne sıklıkta normaldir#

    Yoğun eşzamanlı yazma yapan bir sistemde günde birkaç deadlock olağandır ve yeniden deneme mantığıyla görünmez hale gelir. Ancak dakikada birkaç kez yaşanıyorsa bu bir tasarım sorunudur: büyük ihtimalle eksik index, çok uzun süren transaction'lar veya tutarsız kilit sırası vardır. Hata günlüğünde artan bir eğilim görüyorsan önce SHOW ENGINE INNODB STATUS çıktısındaki index adına bakmalısın.

    innodb_lock_wait_timeout değerini artırmalı mıyım#

    Genellikle hayır. Varsayılan 50 saniye çoğu web uygulaması için zaten uzundur; artırmak yalnızca kullanıcının daha uzun beklemesini sağlar, sorunu çözmez. Aksine, kısa bir değer (örneğin 10 saniye) sorunu daha erken görünür kılar ve uygulamanın hızlıca yeniden denemesine imkân verir. Değeri artırmak yalnızca gerçekten uzun süren toplu işlemlerin olduğu, planlı bakım pencerelerinde anlamlıdır.

    Deadlock kayıtlarını nasıl kalıcı olarak toplarım#

    innodb_print_all_deadlocks = ON ayarı her deadlock olayını MySQL hata günlüğüne yazar, böylece SHOW ENGINE INNODB STATUS çıktısının yalnızca son olayı tutması sorunu ortadan kalkar. Teşhis dönemi boyunca bu ayarı açık tut, sorunu çözdükten sonra kapat; çünkü sık deadlock alan bir sistemde günlük dosyası hızla büyür ve diski doldurabilir.

    SELECT sorguları da deadlock'a girer mi#

    Düz bir SELECT sorgusu InnoDB'de tutarlı okuma (MVCC) ile çalışır ve kilit almaz, dolayısıyla deadlock'a katılmaz. Ancak SELECT ... FOR UPDATE veya SELECT ... LOCK IN SHARE MODE yazdığında kilit alırsın ve bu sorgular kesinlikle deadlock'a girebilir. Özellikle FOR UPDATE ile geniş aralıklar taramak, gap lock nedeniyle deadlock üreten en yaygın kalıplardan biridir.

    Deadlock'u tamamen ortadan kaldırabilir miyim#

    Tek kullanıcılı ya da işlemleri sıraya alan bir sistemde evet, ama eşzamanlı yazma yapan gerçek bir uygulamada pratikte hayır. Doğru hedef, sıklığı ihmal edilebilir seviyeye indirmek ve kaçınılmaz olanları uygulama katmanında sessizce yeniden denemektir. Kilit sırasını sabitlemek, transaction'ları kısaltmak ve index'leri tamamlamak sıklığı genellikle yüzde doksan üzerinde azaltır.

    MyISAM kullansam deadlock olmaz mı#

    Teknik olarak MyISAM satır kilidi kullanmadığı için InnoDB tarzı deadlock üretmez, ama bu bir çözüm değildir. MyISAM tablo düzeyinde kilit alır; yani tek bir yazma işlemi tüm tabloyu bloke eder ve eşzamanlılık dramatik biçimde düşer. Ayrıca transaction desteği ve çökme güvenliği yoktur. İki motorun farklarını InnoDB ve MyISAM karşılaştırması yazısında ayrıntılı ele aldım; deadlock'tan kaçmak için MyISAM'a dönmek neredeyse her zaman daha büyük sorunlar yaratır.

    Kapanış#

    Deadlock'la başa çıkmanın özü dört alışkanlıkta toplanıyor: kaynaklara her zaman aynı sırayla dokun, transaction'ları olabildiğince kısa tut, her UPDATE ve DELETE sorgusunun bir index kullandığını EXPLAIN ile doğrula ve uygulamana 1213 hata kodunu yakalayıp rastgele bekleme ile yeniden deneyen bir mantık ekle. Teşhiste ise SHOW ENGINE INNODB STATUS çıktısındaki index adı ve kilit türü, sorunun kaynağını neredeyse her zaman doğrudan gösterir.

    Kilit çekişmesi bazen tasarımdan değil, sunucunun yükü kaldıramamasından kaynaklanır; yavaş disk ve yetersiz bellek transaction sürelerini uzatarak çakışma olasılığını artırır. Veritabanını hızlı NVMe disklere ve garanti edilen kaynaklara taşımak istersen VDS ve bulut sunucu paketlerimize göz atabilir, yüksek eşzamanlılıkta dedicated sunucu tercih edebilirsin. İzleme ve ayarları bize bırakmak isterseniz sunucu yönetimi hizmetimiz kilit analizini de kapsıyor.

    MySQLInnoDBKilitler

    Uygulamaya geçmeye hazır mısınız?

    NVMe SSD, ücretsiz SSL ve %99.9 uptime garantisiyle Clou.TR hosting ve sunucu çözümleriyle projenizi hayata geçirin.