Uygulama günlüğünde art arda düşen iki hata var: Lock wait timeout exceeded; try restarting transaction ve Deadlock found when trying to get lock. Sipariş kaydedilmiyor, sepet güncellenmiyor, kullanıcı üç kez deniyor. Sunucunun CPU'su boşta, disk rahat, ama sistem duruyor gibi. MySQL tablo kilitlenme sorunları tam olarak böyle görünür: kaynak sıkıntısı değil, sıra bekleme sorunudur ve teşhisi bilmiyorsan tamamen görünmezdir.
Bu rehberde önce MySQL'de kaç çeşit kilit olduğunu ve hangisinin ne zaman devreye girdiğini ayıracağım; sonra kilitlenmeyi anlık olarak nasıl göreceğini, 1205 ve 1213 hatalarını nasıl okuyacağını ve ALTER TABLE komutunun neden bazen sonsuza kadar beklediğini anlatacağım. Son bölüm kalıcı çözümlere ayrıldı — çünkü kilitlenmeyi anlık olarak çözmek (bir bağlantıyı öldürmek) kolaydır, tekrar etmesini önlemek asıl iştir.
MySQL'de Kaç Çeşit Kilit Var#
Kilitleri karıştırmak yanlış çözümlere götürür, o yüzden önce ayıralım. MySQL'de birbirinden bağımsız çalışan üç ana kilit ailesi vardır.
| Kilit türü | Kapsam | Kim koyar | Tipik belirti |
|---|---|---|---|
| Satır kilidi (InnoDB) | Tek satır ya da aralık | UPDATE, DELETE, SELECT FOR UPDATE | Lock wait timeout, deadlock |
| Metadata kilidi (MDL) | Tablo tanımı | Her sorgu okur, DDL yazar | ALTER TABLE takılır |
| Tablo kilidi | Tüm tablo | MyISAM, LOCK TABLES | Tüm yazmalar durur |
Satır kilitleri InnoDB'nin normal çalışma biçimidir ve genelde sorun değildir; sorun, bir işlemin kilidi gereğinden uzun tutmasıyla başlar. Metadata kilitleri çok daha sinsidir: açık kalmış küçük bir SELECT işlemi bile bir ALTER TABLE komutunu ve arkasından gelen tüm sorguları kilitleyebilir. Tablo kilitleri ise çoğunlukla MyISAM tablolarından gelir; MyISAM satır seviyesinde kilit bilmez, bir yazma tüm tabloyu kapatır.
InnoDB satır kilitlerinin bir de az bilinen türü var: boşluk kilitleri (gap lock). Varsayılan REPEATABLE READ yalıtım seviyesinde, bir aralık sorgusu yalnızca var olan satırları değil, aradaki boşlukları da kilitler. Bu, hayalet satır okumasını önler ama beklenmedik kilitlenmelere yol açabilir: henüz var olmayan bir satıra ekleme yapmaya çalışan başka bir işlem beklemeye düşer. Kilit çakışmalarının kaynağı bulunamıyorsa akla gelmesi gereken ilk şeylerden biridir.
Kilitlenmeyi Anlık Olarak Teşhis Etmek#
Sistem o an kilitliyken ilk bakacağın yer süreç listesidir. State sütunundaki metin, hangi kilit ailesiyle uğraştığını doğrudan söyler.
SELECT id, user, host, db, command, time, state, LEFT(info, 120) AS sorgu
FROM information_schema.processlist
WHERE command != 'Sleep' AND time > 2
ORDER BY time DESC;
Karşına çıkan durum metinlerinin anlamları şunlardır:
| State metni | Anlamı |
|---|---|
| Waiting for table metadata lock | DDL, açık bir işlem yüzünden bekliyor |
| Waiting for table level lock | MyISAM tablo kilidi bekleniyor |
| updating / statistics | Satır kilidi bekleniyor olabilir |
| Waiting for handler commit | Commit sırasında bekleme |
Satır kilitlerinde kimin kimi beklettiğini görmek için MySQL 8'de performance_schema tabloları kullanılır. Aşağıdaki sorgu, bekleyen işlemi ve onu bekleten işlemi yan yana verir:
SELECT
r.trx_id AS bekleyen_islem,
r.trx_mysql_thread_id AS bekleyen_baglanti,
LEFT(r.trx_query, 80) AS bekleyen_sorgu,
b.trx_id AS engelleyen_islem,
b.trx_mysql_thread_id AS engelleyen_baglanti,
LEFT(b.trx_query, 80) AS engelleyen_sorgu,
TIMESTAMPDIFF(SECOND, r.trx_wait_started, NOW()) AS bekleme_sn
FROM performance_schema.data_lock_waits w
JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_engine_transaction_id
JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_engine_transaction_id;
sys şeması aynı bilgiyi hazır bir görünümle sunar ve elde tutması daha kolaydır:
SELECT * FROM sys.innodb_lock_waits\G
Uzun süredir açık kalmış işlemleri bulmak da çok işe yarar; kilitlenmelerin büyük çoğunluğunun kaynağı, bitirilmemiş bir transaction'dır:
SELECT trx_id, trx_state, trx_started,
TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS saniye,
trx_mysql_thread_id AS baglanti, LEFT(trx_query, 100) AS sorgu
FROM information_schema.innodb_trx
ORDER BY trx_started;
trx_state değeri RUNNING ve trx_query boş olan bir kayıt görüyorsan, o bağlantı bir işlem açmış ama commit etmemiş demektir — klasik "uygulama BEGIN dedi, sonra bir HTTP isteği bekledi" senaryosu. Acil çözüm o bağlantıyı sonlandırmaktır:
KILL 184922; -- bağlantıyı tamamen kapat
KILL QUERY 184922; -- yalnızca çalışan sorguyu durdur, bağlantı kalsın
KILL bir çözüm değil, kanamayı durdurmadır; asıl iş, o bağlantının neden öyle kaldığını bulmaktır.
Lock Wait Timeout (1205) Hatası#
Bu hata, bir işlemin ihtiyaç duyduğu satır kilidini belirlenen süre içinde alamadığı anlamına gelir:
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
Varsayılan bekleme süresi innodb_lock_wait_timeout değişkeniyle belirlenir ve genellikle 50 saniyedir. Yani bir işlem 50 saniye boyunca kilidi bekledi ve vazgeçti. Buradaki asıl bilgi hatanın kendisinde değil, kimin kilidi 50 saniye boyunca tuttuğundadır.
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';
-- Geçici olarak düşürüp hızlı hata almak isteyebilirsin
SET GLOBAL innodb_lock_wait_timeout = 20;
Süreyi artırmak neredeyse hiçbir zaman doğru çözüm değildir; yalnızca sorunu geciktirir ve bu arada bağlantı havuzu dolar. Süreyi düşürmek ise bazen faydalıdır: uygulama daha hızlı hata alır, yeniden dener ve kullanıcı 50 saniye beklemiş olmaz.
Kalıcı çözüm üç başlıkta toplanır. Birincisi, işlemleri kısaltmak. Bir transaction içinde harici bir servise istek atmak, dosya yazmak ya da kullanıcı girdisi beklemek en yaygın hatadır; kilit tüm bu süre boyunca tutulur. Kural şudur: BEGIN ile COMMIT arasında yalnızca veritabanı işlemleri olsun.
İkincisi, doğru indeks. İndekssiz bir UPDATE, aradığı satırı bulmak için taradığı tüm satırları kilitler. Aynı sorgu uygun indeksle yalnızca birkaç satırı kilitler. Yani indeks eksikliği doğrudan bir kilitlenme sebebidir:
-- İndeks yoksa bu sorgu tabloyu tarar ve taradığı satırları kilitler
UPDATE siparis SET durum = 'iptal' WHERE musteri_id = 8421;
-- Çözüm
CREATE INDEX idx_siparis_musteri ON siparis (musteri_id);
Üçüncüsü, otomatik commit'i kapalı bırakmamak. Bazı istemci kütüphaneleri autocommit=0 ile çalışır ve geliştirici commit etmeyi unutursa işlem açık kalır. Bunu kontrol et:
SELECT @@autocommit;
Hangi sorguların kilit beklemede uzun süre geçirdiğini geriye dönük görmek için yavaş sorgu günlüğü çok işe yarar; Lock_time alanı Query_time değerine yakınsa sorun sorgunun kendisi değil, beklemedir. Kurulum için slow query log yapılandırma yazısına bakabilirsin.
Deadlock (1213) Hatası ve Kaydın Okunması#
Deadlock, iki işlemin birbirinin tuttuğu kilidi beklemesi durumudur ve kimse ilerleyemez. InnoDB bunu otomatik olarak algılar, işlemlerden birini geri alır ve o işleme hata döndürür:
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
Önemli bir nokta: deadlock bir arıza değildir. InnoDB tasarımı gereği bunu algılar ve çözer; sistem kilitli kalmaz. Sorun, uygulamanın bu hatayı işlememesidir. Doğru davranış, işlemi baştan denemektir — hata metni de bunu söylüyor. Uygulamanda bu hata kodunu yakalayıp iki ya da üç kez yeniden deneme yapan bir sarmalayıcı yazmak, kilitlenme kaynaklı hataların çoğunu görünmez hâle getirir.
Yine de sık tekrarlayan bir deadlock varsa kökeni bulunmalıdır. Son deadlock kaydı şu komutla görülür:
SHOW ENGINE INNODB STATUS\G
Çıktının LATEST DETECTED DEADLOCK bölümü iki işlemi, tuttukları ve bekledikleri kilitleri ve hangisinin kurban seçildiğini gösterir:
------------------------
LATEST DETECTED DEADLOCK
------------------------
2026-08-25 12:04:51 0x7f2a1c0f5700
*** (1) TRANSACTION:
TRANSACTION 84412, 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 stok SET adet = adet - 1 WHERE urun_id = 55
*** (2) TRANSACTION:
TRANSACTION 84413, ACTIVE 5 sec starting index read
UPDATE stok SET adet = adet - 1 WHERE urun_id = 12
*** WE ROLL BACK TRANSACTION (2)
Bu kayıt klasik bir örneği anlatıyor: iki işlem aynı iki satıra ters sırayla dokunuyor. Birinci işlem önce 55, sonra 12 numaralı ürünü güncelliyor; ikincisi önce 12, sonra 55'i. Çözüm, uygulamada satırlara daima aynı sırayla erişmektir — örneğin birincil anahtara göre sıralayarak. Bu tek disiplin, çoğu deadlock'u tamamen ortadan kaldırır.
Tüm deadlock'ları günlüğe yazdırmak istersen (yalnızca sonuncusu değil):
SET GLOBAL innodb_print_all_deadlocks = ON;
-- Kayıtlar MySQL hata günlüğüne düşer: /var/log/mysql/error.log
Yalıtım seviyesini READ COMMITTED yapmak da boşluk kilitlerini büyük ölçüde ortadan kaldırdığı için deadlock sıklığını düşürür. Ancak bu, uygulamanın tutarlılık varsayımlarını değiştirir; körü körüne uygulanacak bir ayar değildir.
Metadata Kilitleri: ALTER TABLE Neden Takılır#
En çok kafa karıştıran senaryo budur. Küçük bir tabloya sütun eklemek istiyorsun, ALTER TABLE çalıştırıyorsun ve komut hiç bitmiyor. Üstelik o tabloya gelen tüm yeni sorgular da kuyruğa giriyor ve site duruyor.
Sebep şudur: her sorgu, çalıştığı tablonun tanımı üzerinde paylaşımlı bir metadata kilidi tutar ve bu kilit işlem bitene kadar bırakılmaz. ALTER TABLE ise özel bir metadata kilidi ister. Ortada saatlerdir commit edilmemiş, hatta yalnızca tek bir SELECT çalıştırmış bir işlem varsa, ALTER beklemeye geçer. Ve MySQL kilit isteklerini sıraya aldığı için, ALTER'ın arkasına gelen her yeni sorgu da beklemeye başlar. Yani tek bir unutulmuş transaction, tüm tabloyu erişilmez yapar.
Suçluyu bulmak için önce süreç listesine bak:
SELECT id, user, time, state, LEFT(info, 100) AS sorgu
FROM information_schema.processlist
WHERE state LIKE '%metadata lock%';
Bu sana bekleyenleri gösterir, engelleyeni değil. Engelleyeni bulmak için açık işlemlere bakmalısın:
SELECT p.id AS baglanti, p.user, p.host, p.time AS bosta_saniye,
t.trx_started, LEFT(COALESCE(t.trx_query, '(bosta islem)'), 80) AS son_sorgu
FROM information_schema.innodb_trx t
JOIN information_schema.processlist p ON p.id = t.trx_mysql_thread_id
ORDER BY t.trx_started;
En eski trx_started değerine sahip ve şu an sorgu çalıştırmayan bağlantı, aradığın kişidir. KILL ile sonlandırdığında ALTER hemen ilerler.
Bu sorunu kalıcı olarak önlemenin iki yolu var. Birincisi, şema değişikliklerini pt-online-schema-change ile yapmaktır; araç tabloyu arka planda kopyalayarak dönüştürür ve yalnızca en sonda çok kısa bir kilit alır. Kullanımı Percona Toolkit ile veritabanı bakımı yazısında anlatılıyor. İkincisi ise DDL komutlarına zaman aşımı koymaktır; böylece takılan bir ALTER arkasında kuyruk biriktirmez:
SET SESSION lock_wait_timeout = 10;
ALTER TABLE magaza.siparis ADD COLUMN not_alani VARCHAR(255) NULL;
Kalıcı Çözümler ve Sık Yapılan Hatalar#
Birinci hata, innodb_lock_wait_timeout değerini artırarak sorunu "çözmektir". Bu, kullanıcıyı daha uzun bekletmekten başka bir işe yaramaz ve bu arada bağlantı havuzu dolar; sonuçta MySQL too many connections hatası ile de tanışırsın. Süreyi artırmak yerine kilidi tutan işlemi kısalt.
İkinci hata, deadlock'u uygulamada işlememektir. 1213 hatası beklenen bir durumdur ve doğru yanıt işlemi yeniden denemektir. Bunu yapmayan bir uygulama, kullanıcıya gereksiz yere hata gösterir. Özellikle Galera gibi çoklu yazma kurgularında bu hata daha sık görülür; Galera Cluster kurulumu yazısında bunun sebebini açıkladım.
Üçüncü hata, MyISAM tablolarını unutmaktır. MyISAM satır kilidi bilmez; tek bir UPDATE tüm tabloyu kapatır ve okuma yapan herkes bekler. Hâlâ MyISAM tablon varsa InnoDB'ye dönüştür:
SELECT table_schema, table_name, engine
FROM information_schema.tables
WHERE engine = 'MyISAM'
AND table_schema NOT IN ('mysql','information_schema','performance_schema','sys');
ALTER TABLE magaza.eski_tablo ENGINE = InnoDB;
Dördüncü hata, uzun raporlama sorgularını yazma yapan sunucuda çalıştırmaktır. Saatlerce süren bir rapor sorgusu açık bir işlem tutar ve metadata kilidi zincirini tetikler. Bu tür sorguları bir okuma replikasına taşımak hem yükü hem de kilit riskini ortadan kaldırır.
Beşinci hata, işlem içinde gereğinden fazla satır kilitlemektir. Toplu güncellemeleri tek seferde yapmak yerine parçalara bölmek, kilit süresini kısaltır ve diğer işlemlere nefes aldırır:
-- Tek seferde 500 bin satır yerine, 2000'lik partiler hâlinde
UPDATE siparis SET arsiv = 1
WHERE olusturma < '2025-01-01' AND arsiv = 0
LIMIT 2000;
Bu döngüyü her turda kısa bir bekleme ile çalıştırmak, üretimde çok daha güvenlidir. Aynı işi otomatik yapan hazır bir araç arıyorsan pt-archiver tam olarak bunun içindir.
Sıkça Sorulan Sorular#
Lock wait timeout hatası neden alınır#
Bir işlem, güncellemek istediği satırın kilidini başka bir işlem tuttuğu için bekler ve innodb_lock_wait_timeout süresi (genellikle 50 saniye) dolduğunda bu hatayı alır. Yani hatayı alan işlem suçlu değildir; asıl sorun kilidi uzun süre tutan diğer işlemdir. Çözüm için information_schema.innodb_trx tablosundan uzun süredir açık kalan işlemi bulup neden commit edilmediğini araştırmak gerekir.
Deadlock hatası veritabanının bozulduğu anlamına mı gelir#
Hayır. Deadlock, iki işlemin birbirini beklediği bir durumdur ve InnoDB bunu otomatik olarak algılayıp işlemlerden birini geri alarak çözer; veride hiçbir bozulma olmaz. Geri alınan işlem hiç yapılmamış gibi olur. Doğru davranış, uygulamada bu hatayı yakalayıp işlemi yeniden denemektir. Sık tekrarlıyorsa kod tarafında satırlara erişim sırasını standartlaştırmak gerekir.
Kilitlenmeyi kim yapıyor nasıl bulurum#
MySQL 8'de en pratik yol sys.innodb_lock_waits görünümüdür; bekleyen ve engelleyen işlemleri, bağlantı numaralarıyla birlikte yan yana listeler. Alternatif olarak information_schema.innodb_trx tablosundan en eski trx_started değerine sahip işlemi bulabilirsin. Metadata kilidi sorunlarında ise information_schema.processlist tablosunda Waiting for table metadata lock durumundaki sorguları arayıp, engelleyen açık işlemi ayrıca tespit etmen gerekir.
ALTER TABLE takıldığında ne yapmalıyım#
Komutu iptal etmeden önce sebebini bul; genellikle o tabloya dokunmuş ve hâlâ commit edilmemiş bir işlem vardır. information_schema.innodb_trx tablosunda en eski işlemi bulup KILL ile sonlandırdığında ALTER hemen ilerler. Bu durumun tekrarlamaması için şema değişikliklerini pt-online-schema-change ile yapmayı ve DDL komutlarından önce SET SESSION lock_wait_timeout ile kısa bir zaman aşımı belirlemeyi alışkanlık hâline getir.
innodb_lock_wait_timeout değerini artırmalı mıyım#
Genellikle hayır. Süreyi artırmak sorunu çözmez, yalnızca kullanıcının daha uzun beklemesine ve bu arada bağlantıların birikmesine yol açar. Çoğu durumda süreyi düşürmek daha faydalıdır: uygulama hızlıca hata alır, yeniden dener ve kaynak boşa harcanmaz. Asıl çözüm, kilidi tutan işlemleri kısaltmak ve eksik indeksleri tamamlayarak kilitlenen satır sayısını azaltmaktır.
İndeks eklemek kilitlenmeyi nasıl azaltır#
InnoDB, bir güncelleme yaparken aradığı satırı bulmak için taradığı tüm satırları kilitler. Uygun indeks yoksa tabloyu baştan sona tarar ve bu sırada gereğinden çok daha fazla satır kilitlenir; başka işlemler de bu satırlara erişemez. Uygun bir indeks eklendiğinde MySQL doğrudan ilgili satırlara gider ve yalnızca onları kilitler. Yani indeks eksikliği hem yavaşlık hem de kilitlenme sebebidir; ikisini aynı anda çözersin.
Kapanış#
Kilitlenme sorunları, kaynak yetersizliği gibi görünmedikleri için en geç fark edilen performans sorunlarıdır; ama teşhis yöntemini bir kez öğrendiğinde çözümleri genellikle basittir. Aklında kalması gereken dört alışkanlık şu: transaction'ları kısa tut ve içine harici çağrı koyma, uygulamada 1213 deadlock hatasını yakalayıp işlemi yeniden dene, güncelleme yaptığın sütunlarda indeks olduğundan emin ol ve şema değişikliklerini kilitsiz araçlarla yap. Bir de uzun raporlama sorgularını yazma yapan sunucudan uzak tut; bu tek karar, metadata kilidi zincirlerinin çoğunu baştan engeller.
Okuma yükünü ayrı bir sunucuya taşımak istiyorsan VDS ve bulut sunucu paketlerimizle bir replika kurabilirsin. Kilit analizini, indeks çalışmalarını ve şema değişikliklerini uzman gözüyle yürütmek istersen sunucu yönetimi hizmetimiz bu işi üstlenir; yoğun trafik altında uygulama katmanını korumak için WAF ve DDoS koruma çözümlerimize de bakabilirsin.