InnoDB Deadlock Tespiti ve Çözümü için SQL Sorguları
Veritabanı yönetiminin en sinir bozucu anlarından biri, prodüksiyonda bir deadlock (kilitlenme) patlamasıyla karşılaşmaktır. Uygulamalar askıda kalır, loglar hata mesajlarıyla dolar, kullanıcılar şikayet etmeye başlar ve sen de hangi sorgunun hangisini beklediğini çözmeye çalışırsın. InnoDB’nin bu konuda oldukça iyi araçları var, ama bunları nasıl kullanacağını bilmezsen çok değerli zaman kaybedersin. Bu yazıda gerçek dünya senaryolarıyla deadlock tespiti, analizi ve çözümünü adım adım ele alacağız.
Deadlock Nedir ve Neden Olur?
InnoDB satır seviyesinde kilitleme (row-level locking) kullanır. Bu genellikle harika bir şeydir çünkü eşzamanlılığı artırır. Ancak iki ya da daha fazla transaction birbirinin tuttuğu kilitleri beklemeye başladığında ortaya deadlock çıkar.
Klasik senaryo şöyle işler:
- Transaction A tablo1’deki satır 1’i kilitler, ardından tablo2’deki satır 2’yi kilitlemek ister
- Transaction B tablo2’deki satır 2’yi kilitler, ardından tablo1’deki satır 1’i kilitlemek ister
- İkisi de birbirini sonsuza kadar bekler
InnoDB bu durumu tespit ettiğinde transaction’lardan birini kurban seçip geri alır (rollback) ve “ERROR 1213: Deadlock found when trying to get lock” hatasını döndürür. Aslında bu otomatik çözüm bir nimettir, en azından sistem tamamen kilitlenmez.
InnoDB Status ile İlk Tanı
Deadlock yaşandığında yapacağın ilk şey InnoDB’nin dahili durum raporuna bakmaktır. Bu rapor son yaşanan deadlock hakkında çok detaylı bilgi verir.
mysql -u root -p -e "SHOW ENGINE INNODB STATUSG"
Bu komut tonlarca çıktı üretir. Deadlock ile ilgili kısım LATEST DETECTED DEADLOCK başlığı altındadır. Grep ile direkt oraya atlayabilirsin:
mysql -u root -p -e "SHOW ENGINE INNODB STATUSG" 2>/dev/null |
awk '/LATEST DETECTED DEADLOCK/,/TRANSACTIONS/'
Çıktı şuna benzer bir şey olacaktır:
------------------------
LATEST DETECTED DEADLOCK
------------------------
2024-01-15 14:23:45 0x7f8a1c003700
*** (1) TRANSACTION:
TRANSACTION 421893, ACTIVE 0 sec starting index read
MySQL thread id 1542, OS thread handle 140234567890, query id 98432 localhost app_user
UPDATE orders SET status='processing' WHERE id=1001
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 234 page no 5 n bits 72 index PRIMARY of table `shopdb`.`orders`
*** (2) TRANSACTION:
TRANSACTION 421894, ACTIVE 0 sec starting index read
MySQL thread id 1543, OS thread handle 140234567891, query id 98433 localhost app_user
UPDATE order_items SET quantity=5 WHERE order_id=1001
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 234 page no 5 n bits 72 index PRIMARY of table `shopdb`.`orders`
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 235 page no 3 n bits 64 index PRIMARY of table `shopdb`.`order_items`
*** WE ROLL BACK TRANSACTION (1)
Bu çıktıyı okumak başlangıçta korkutucu görünebilir ama aslında çok net bir bilgi sunar: hangi transaction hangi kilidi tutuyor, hangisini bekliyor ve hangisi rollback edildi.
Performance Schema ile Detaylı Kilit Analizi
InnoDB Status anlık bir fotoğraf sunar ve sadece son deadlock’u gösterir. Daha sistematik bir analiz için Performance Schema kullanmak gerekir. Önce etkin olduğunu doğrula:
SELECT * FROM performance_schema.setup_instruments
WHERE name LIKE '%lock%' AND enabled = 'YES';
SELECT * FROM performance_schema.setup_consumers
WHERE name LIKE '%lock%';
Aktif değilse etkinleştir:
UPDATE performance_schema.setup_instruments
SET ENABLED = 'YES', TIMED = 'YES'
WHERE name LIKE 'wait/lock/metadata/sql/mdl';
UPDATE performance_schema.setup_consumers
SET ENABLED = 'YES'
WHERE name = 'events_waits_current';
Şu anda bekleyen kilitleri görmek için şu sorguyu kullan. Bu özellikle “deadlock olmadı ama sorgular çok yavaş” durumlarında hayat kurtarır:
SELECT
r.trx_id AS waiting_trx_id,
r.trx_mysql_thread_id AS waiting_thread,
r.trx_query AS waiting_query,
b.trx_id AS blocking_trx_id,
b.trx_mysql_thread_id AS blocking_thread,
b.trx_query AS blocking_query,
b.trx_started AS blocking_trx_started,
TIMESTAMPDIFF(SECOND, b.trx_started, NOW()) AS blocking_duration_seconds
FROM
information_schema.innodb_lock_waits w
INNER JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
INNER JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id;
Bu sorgu sana şu anda kimin kimi beklediğini, ne kadar süredir beklediğini ve sorguların ne olduğunu gösterir. Prodüksiyonda yavaşlık yaşandığında bu sorguyu çalıştırmak refleks haline gelmelidir.
MariaDB 10.5+ ve MySQL 8.0 için Güncel Sorgular
information_schema.innodb_lock_waits ve innodb_locks tabloları MySQL 8.0’da kaldırıldı. Yerini performance_schema.data_lock_waits ve performance_schema.data_locks aldı. MariaDB 10.5+ için de benzer durum söz konusu. Her iki sürümle çalışan güncel sorgu şöyle olmalı:
-- MySQL 8.0+ ve MariaDB 10.5+ için
SELECT
dlw.REQUESTING_ENGINE_TRANSACTION_ID AS waiting_trx,
dlw.BLOCKING_ENGINE_TRANSACTION_ID AS blocking_trx,
dl_req.THREAD_ID AS waiting_thread_id,
dl_req.OBJECT_SCHEMA AS schema_name,
dl_req.OBJECT_NAME AS table_name,
dl_req.LOCK_TYPE AS waiting_lock_type,
dl_req.LOCK_MODE AS waiting_lock_mode,
dl_req.LOCK_STATUS AS lock_status,
dl_blk.THREAD_ID AS blocking_thread_id,
dl_blk.LOCK_TYPE AS blocking_lock_type,
dl_blk.LOCK_MODE AS blocking_lock_mode
FROM
performance_schema.data_lock_waits dlw
INNER JOIN performance_schema.data_locks dl_req
ON dlw.REQUESTING_ENGINE_LOCK_ID = dl_req.ENGINE_LOCK_ID
INNER JOIN performance_schema.data_locks dl_blk
ON dlw.BLOCKING_ENGINE_LOCK_ID = dl_blk.ENGINE_LOCK_ID;
Deadlock Loglamasını Kalıcı Hale Getirmek
Varsayılan olarak InnoDB sadece son deadlock’u hafızada tutar. Prodüksiyonda bu yeterli değildir. innodb_print_all_deadlocks parametresini aktif ederek tüm deadlock’ların error log’a yazılmasını sağla:
# my.cnf veya my.ini dosyasına ekle
[mysqld]
innodb_print_all_deadlocks = 1
innodb_lock_wait_timeout = 50
innodb_deadlock_detect = ON
Ardından servisi yeniden başlat:
# Restart olmadan da çalışma zamanında aktif edebilirsin
mysql -u root -p -e "SET GLOBAL innodb_print_all_deadlocks = 1;"
Deadlock loglarını error log’dan filtreleyerek izlemek için:
# Error log konumunu öğren
mysql -u root -p -e "SHOW VARIABLES LIKE 'log_error';"
# Log'u canlı izle ve sadece deadlock satırlarını göster
tail -f /var/log/mysql/error.log | grep -i "deadlock|TRANSACTION|WAITING FOR"
Gerçek Dünya Senaryosu: E-ticaret Sipariş Sistemi
Diyelim ki bir e-ticaret platformu yönetiyorsun. Sipariş onaylandığında şu iki işlem eşzamanlı tetikleniyor:
İşlem 1 (Ödeme Servisi):
START TRANSACTION;
UPDATE orders SET payment_status = 'paid' WHERE id = 5001;
UPDATE inventory SET reserved = reserved - 1 WHERE product_id = 200;
COMMIT;
İşlem 2 (Stok Servisi):
START TRANSACTION;
UPDATE inventory SET reserved = reserved - 1 WHERE product_id = 200;
UPDATE orders SET fulfillment_status = 'ready' WHERE id = 5001;
COMMIT;
Görüyor musun? İki transaction da aynı tabloları ama ters sırada güncelliyor. Bu klasik bir deadlock reçetesidir.
Tespit için şu sorguyu çalıştır:
-- Transaction geçmişini ve kilit bilgilerini birleştir
SELECT
t.trx_id,
t.trx_state,
t.trx_started,
t.trx_wait_started,
t.trx_mysql_thread_id,
t.trx_query,
t.trx_rows_locked,
t.trx_rows_modified,
t.trx_lock_structs,
TIMESTAMPDIFF(SECOND, t.trx_started, NOW()) AS trx_age_seconds
FROM
information_schema.innodb_trx t
WHERE
t.trx_state = 'LOCK WAIT'
ORDER BY
trx_age_seconds DESC;
Çözüm: Her iki transaction’ın da tabloları aynı sırada güncellemesini sağla. Uygulama kodunda bu sırayı standartlaştır. orders tablosu her zaman önce, inventory her zaman sonra güncellensin.
Hangi Sorgular En Çok Kilide Neden Oluyor?
Kronik deadlock sorunlarını çözmek için hangi sorgu veya uygulama kullanıcısının en çok kilite yol açtığını bulmak gerekir:
-- Uzun süredir açık transaction'ları bul
SELECT
trx.trx_id,
trx.trx_state,
trx.trx_started,
TIMESTAMPDIFF(SECOND, trx.trx_started, NOW()) AS open_seconds,
trx.trx_mysql_thread_id AS thread_id,
trx.trx_query AS current_query,
trx.trx_tables_in_use,
trx.trx_tables_locked,
trx.trx_lock_structs,
proc.user AS db_user,
proc.host AS client_host,
proc.db AS database_name,
proc.command AS thread_command,
proc.time AS thread_time,
proc.info AS full_query
FROM
information_schema.innodb_trx trx
LEFT JOIN information_schema.processlist proc
ON trx.trx_mysql_thread_id = proc.id
WHERE
TIMESTAMPDIFF(SECOND, trx.trx_started, NOW()) > 30
ORDER BY
open_seconds DESC;
30 saniyeden uzun açık kalan transaction’lar kırmızı alarm işaretidir. Bunlar genellikle uygulama hatalarından (commit edilmeyen transaction’lar) ya da uzun süren batch işlemlerden kaynaklanır.
Sorunlu Thread’i Sonlandırmak
Kritik bir deadlock veya kilit bekleme durumunda, bloke eden thread’i öldürmek gerekebilir:
# Önce processlist'i kontrol et
mysql -u root -p -e "SHOW FULL PROCESSLISTG" | grep -A 5 "Lock|lock"
# Belirli bir thread'i sonlandır
mysql -u root -p -e "KILL 1542;"
# Sadece sorguyu sonlandır, bağlantıyı kesme
mysql -u root -p -e "KILL QUERY 1542;"
Thread’i kill etmeden önce ne yaptığını iyice anla. Yarı kalmış bir transaction veri tutarsızlığına yol açabilir. InnoDB rollback yapacaktır ama bunu bilerek ve kontrollü yapman gerekir.
İndeks Eksikliği ve Deadlock İlişkisi
Deadlock’ların göz ardı edilen en yaygın nedenlerinden biri eksik indekslerdir. InnoDB, WHERE koşulundaki sütunda indeks yoksa tablo taraması (full table scan) yapar ve bu süreçte çok daha fazla satırı kilitler.
-- İndeks kullanımını kontrol et
EXPLAIN SELECT * FROM orders WHERE customer_email = '[email protected]';
-- Eğer type = 'ALL' görüyorsan tam tablo taraması yapıyor demektir
-- Bu durumda çok fazla satır kilitlenir
-- İndeks ekle
ALTER TABLE orders ADD INDEX idx_customer_email (customer_email);
-- Sonucu doğrula
EXPLAIN SELECT * FROM orders WHERE customer_email = '[email protected]';
-- type = 'ref' veya 'const' görmelisin
Bir diğer yaygın tuzak ise gap lock (aralık kilidi) sorunudur. InnoDB, REPEATABLE READ izolasyon seviyesinde aralıkları da kilitler. Bunu anlamak için:
-- Mevcut izolasyon seviyesini kontrol et
SELECT @@global.transaction_isolation, @@session.transaction_isolation;
-- READ COMMITTED'a geçmek gap lock sorunlarını azaltır
-- ama phantom read sorununa yol açabilir, dikkatli değerlendir
SET SESSION transaction_isolation = 'READ-COMMITTED';
-- Ya da my.cnf'e ekle
-- transaction_isolation = READ-COMMITTED
Deadlock Önleme Stratejileri
Tespit önemlidir ama asıl hedef deadlock’ları önlemektir. İşte kanıtlanmış yöntemler:
- Tutarlı sıralama: Birden fazla tabloyu güncelleyen her işlemde tabloları her zaman aynı sırada kilitle. Alfabetik ya da ID bazlı sabit bir sıra belirle.
- Transaction süresini kısa tut: Transaction içinde ağ çağrısı, dosya okuma gibi yavaş işlemler yapma. Veriyi önce çek, işle, sonra kısa bir transaction ile yaz.
- SELECT FOR UPDATE kullanımına dikkat et: Gerçekten güncelleme yapacaksan kullan. Sadece okuma amaçlıysa
LOCK IN SHARE MODEya da düz SELECT tercih et.
- Toplu güncellemelerde sıralama: Birden fazla satırı güncellerken satırları ID’ye göre sıralı güncelle. Böylece farklı transaction’lar aynı sırayla ilerlediğinden çakışma azalır.
- innodb_lock_wait_timeout değerini ayarla: Varsayılan 50 saniyedir. Çoğu uygulama için bu çok uzun. 10-15 saniyeye indirmek daha hızlı hata tespiti sağlar.
-- Mevcut değeri kontrol et
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';
-- Çalışma zamanında değiştir
SET GLOBAL innodb_lock_wait_timeout = 15;
-- Kalıcı yapmak için my.cnf'e ekle
-- innodb_lock_wait_timeout = 15
Otomatik İzleme Scripti
Prodüksiyonda deadlock’ları sürekli izlemek için basit bir bash scripti işini görür:
#!/bin/bash
# deadlock_monitor.sh
# Cron ile her dakika çalıştır: * * * * * /opt/scripts/deadlock_monitor.sh
DB_USER="monitor_user"
DB_PASS="gizli_sifre"
LOG_FILE="/var/log/mysql/deadlock_monitor.log"
ALERT_EMAIL="[email protected]"
THRESHOLD_SECONDS=60
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
# Uzun süren lock wait'leri kontrol et
long_waits=$(mysql -u "$DB_USER" -p"$DB_PASS" -N -e "
SELECT COUNT(*)
FROM information_schema.innodb_trx
WHERE trx_state = 'LOCK WAIT'
AND TIMESTAMPDIFF(SECOND, trx_wait_started, NOW()) > $THRESHOLD_SECONDS;
" 2>/dev/null)
if [ "$long_waits" -gt 0 ]; then
echo "[$timestamp] UYARI: $long_waits adet uzun sureli lock wait tespit edildi" >> "$LOG_FILE"
# Detayları al
details=$(mysql -u "$DB_USER" -p"$DB_PASS" -e "
SELECT trx_id, trx_mysql_thread_id,
TIMESTAMPDIFF(SECOND, trx_wait_started, NOW()) AS wait_sec,
trx_query
FROM information_schema.innodb_trx
WHERE trx_state = 'LOCK WAIT';" 2>/dev/null)
echo "$details" >> "$LOG_FILE"
# Mail gonder
echo -e "Sunucu: $(hostname)nZaman: $timestampnn$details" |
mail -s "MySQL Deadlock Alarmi" "$ALERT_EMAIL"
fi
# Son deadlock bilgisini logla
mysql -u "$DB_USER" -p"$DB_PASS" -e "SHOW ENGINE INNODB STATUSG" 2>/dev/null |
awk '/LATEST DETECTED DEADLOCK/,/--------/' >> "$LOG_FILE"
Bu scripti çalıştırmak için gerekli izinleri ver:
chmod +x /opt/scripts/deadlock_monitor.sh
# Monitor kullanıcısı için minimum izinler
mysql -u root -p -e "
CREATE USER 'monitor_user'@'localhost' IDENTIFIED BY 'gizli_sifre';
GRANT PROCESS, REPLICATION CLIENT ON *.* TO 'monitor_user'@'localhost';
GRANT SELECT ON information_schema.* TO 'monitor_user'@'localhost';
FLUSH PRIVILEGES;"
Sys Schema ile Hazır Görünümler
MySQL 5.7+ ve MariaDB 10.6+ ile gelen sys schema, deadlock analizi için harika kısayollar sunar:
-- InnoDB kilit beklentilerini özet olarak göster
SELECT * FROM sys.innodb_lock_waits;
-- En çok kilit üreten tablolar
SELECT
object_schema,
object_name,
count_read,
count_write,
count_fetch,
count_insert,
count_update,
count_delete
FROM
sys.schema_table_statistics
ORDER BY
count_write DESC
LIMIT 20;
-- Uzun süredir açık olan session'lar
SELECT * FROM sys.session
WHERE conn_id != CONNECTION_ID()
AND time > 30
ORDER BY time DESC;
Sonuç
Deadlock yönetimi tek seferlik bir müdahale değil, sürekli bir izleme ve iyileştirme döngüsüdür. Özetlemek gerekirse:
- Tespit için
SHOW ENGINE INNODB STATUSveinformation_schema/performance_schemasorgularını kullan - Kronik sorunlar için
innodb_print_all_deadlocksparametresini aktif et ve logları düzenli incele - Önleme için tutarlı kilit sırası, kısa transaction’lar ve doğru indeksler sağla
- İzleme için otomatik scriptler veya Percona Monitoring and Management (PMM) gibi araçlar kullan
- Acil müdahale için
KILLkomutunu nasıl ve ne zaman kullanacağını bil
İndeks eksikliği, tutarsız kilit sıralaması ve uzun açık kalan transaction’lar deadlock’ların yüzde sekseninden fazlasının nedenidir. Bu üç konuya odaklanırsan çoğu sorunu kökten çözebilirsin. Gerisi zaten kendiliğinden netleşir.
