POSTGRESQL LOCKING VE DEADLOCK

PostgreSQL deadlock iki transaction çapraz kilitlenme akış diyagramı

Saat 03:14, üretim ortamında alarm: iki transaction birbirini bekliyor, log'da "ERROR: deadlock detected" satırı. Biri sipariş güncelliyor, diğeri stok düşüyor — ikisi de aynı iki satıra farklı sırayla dokunmuş. PostgreSQL birini öldürdü, kullanıcı 500 aldı, sipariş yarım kaldı. Bu, kötü kod değil; lock sırası planlanmamış bir kod. Aynı senaryo bir daha tekrarlanmasın diye lock türlerini, deadlock'un nasıl oluştuğunu ve advisory lock'ların ne zaman hayat kurtardığını ayrıntılı ele alıyoruz.

Deadlock Tam Olarak Ne Anda Doğar?

Deadlock, iki ya da daha çok transaction'ın birbirinin tuttuğu lock'u beklediği döngüsel bekleme durumudur. PostgreSQL bunu otomatik tespit eder: deadlock_timeout (varsayılan 1 saniye) süresince bekleyen bir lock varsa, bekleme grafı taranır ve döngü bulunursa kurbanlardan biri iptal edilir.

Klasik senaryo şu şekildedir:

  • Transaction A önce orders satırını, sonra stock satırını günceller.
  • Transaction B aynı satırlara tam ters sırayla dokunur — önce stock, sonra orders.
  • İkisi de ilk satırı kilitlemeyi başarır; ikincisini beklerken birbirlerine kilitlenirler.
  • 1 saniye sonra PostgreSQL döngüyü görür, birini ERROR: deadlock detected ile düşürür.

Önemli bir nokta: deadlock bir bug değil, tasarım eksikliğinin semptomudur. Retry mantığı koymak hatayı gizler, kök sebep lock sırasının deterministik olmamasıdır.

PostgreSQL Lock Türleri ve Çakışma Matrisi

Sorunu çözmek için önce neyin neyle çakıştığını bilmek gerekir. Row-level ve table-level olmak üzere iki ana lock ailesi vardır.

Row-level lock'larda en sık karşılaşılanlar:

  1. FOR UPDATE: Satırı güncelleme ya da silme niyetiyle kilitler. Başka bir FOR UPDATE, FOR NO KEY UPDATE, FOR SHARE ile çakışır.
  2. FOR NO KEY UPDATE: Foreign key referans alanlarını değiştirmeyen güncellemeler için; daha az çakışma yaratır.
  3. FOR SHARE: Satırı okuma garantisi ister, başka transaction güncelleyemez ama paylaşımlı okuma yapılabilir.
  4. FOR KEY SHARE: En zayıf row lock, foreign key kontrolleri için kullanılır.

Table-level lock'lar (ACCESS SHARE, ROW EXCLUSIVE, SHARE, EXCLUSIVE, ACCESS EXCLUSIVE) ise DDL ve ağır bakım komutlarında devreye girer. VACUUM FULL, CREATE INDEX (CONCURRENTLY olmadan) ve ALTER TABLE gibi komutlar ACCESS EXCLUSIVE alır; bu sırada gelen basit bir SELECT bile bekler. Lock modlarının birbirleriyle nasıl çakıştığını gösteren tam matris için resmi dokümantasyonu referans olarak tutmak işe yarar.

PostgreSQL row-level lock türlerinin birbirleriyle çakışma matrisi tablosu

Birinci Çözüm: Tutarlı Lock Sırası

Deadlock'tan kurtulmanın en sağlam yolu, tüm transaction'ların kaynakları aynı sırada kilitlemesini garanti etmektir. Sıralama kuralı basit ve değişmez olmalı — örneğin primary key'e göre artan sıra.

Hatalı yaklaşım, A için UPDATE orders WHERE id=42 sonra UPDATE stock WHERE sku=42; B'de ise tersi. Doğru yaklaşım, iş mantığı ne olursa olsun her transaction'ın önce id'si küçük olanı kilitlemesidir:

SELECT id FROM accounts WHERE id IN (10, 27) ORDER BY id FOR UPDATE;

Bu küçük dokunuş, transfer benzeri senaryolarda deadlock'ları neredeyse tamamen ortadan kaldırır. Para transferinde "önce kaynak hesabı, sonra hedef hesabı" demek yerine "önce id'si küçük olan hesabı kilitle" demek deterministik bir sıra üretir.

İkinci Çözüm: Advisory Lock

Bazen kilitlenmesi gereken şey bir satır değil, soyut bir iş kaynağıdır: "aynı anda yalnızca bir worker bu kullanıcının raporunu üretsin" gibi. Burada pg_advisory_lock ve pg_try_advisory_lock devreye girer.

Advisory lock'ların ayırt edici özellikleri şunlardır:

  • Anlamı uygulama belirler; PostgreSQL sadece bir bigint anahtar tutar.
  • pg_advisory_lock(key) bloklayıcıdır, bekler. pg_try_advisory_lock(key) hemen true/false döner — non-blocking iş kuyrukları için idealdir.
  • Session düzeyinde ya da transaction düzeyinde (pg_advisory_xact_lock) alınabilir; transaction düzeyi tercih edilir çünkü commit/rollback ile otomatik bırakılır.
  • Tablo lock'larıyla çakışmaz; yani başka sorgular engellenmez, sadece aynı anahtarı isteyenler beklenir.

Klasik kullanım: cron benzeri bir job birden çok pod'da çalışıyor. Hepsi pg_try_advisory_xact_lock(hashtext('nightly-report')) dener. True dönen tek pod işi yapar, diğerleri sessizce çıkar. Redis ya da Zookeeper'a gerek kalmadan, veritabanı seviyesinde lider seçimi.

Lock'ları Gözlemlemek: pg_locks ve pg_stat_activity

Üretimde "kim kimi bekliyor" sorusunun cevabını bulmak için iki sistem view'ı vardır: pg_locks ve pg_stat_activity. İkisinin birleşimi, anlık bekleme grafını çıkarır.

Pratik bir tanı sorgusu, bloklanan ve blokleyen PID'leri eşleyen pg_blocking_pids() fonksiyonudur. SELECT pid, pg_blocking_pids(pid), query FROM pg_stat_activity WHERE cardinality(pg_blocking_pids(pid)) > 0; komutu, o an bekleyen tüm sorguları ve onları tutanları listeler. Olay anında bu sorgu, telemetri kadar değerli bilgi verir.

PostgreSQL performans ve transaction yönetimi konularını daha derinlemesine ele aldığımız PostgreSQL eğitimi içeriğinden lock davranışı, isolation level ve MVCC ilişkisi hakkında yararlanabilirsiniz.

Önleyici Pratikler

Lock kaynaklı sürprizleri azaltmak için kalıcı önlemler:

  • Transaction'ları kısa tutun; içlerinde HTTP çağrısı, dış API beklemesi yapmayın.
  • Foreign key tarafındaki sütunlara indeks koyun; eksik indeks gereksiz satır lock'larına yol açar.
  • SELECT ... FOR UPDATE SKIP LOCKED iş kuyrukları için biçilmiş kaftandır; meşgul satırı atlar, beklemez.
  • DDL'leri trafik penceresinin dışında ve lock_timeout ayarlayarak çalıştırın — kaçak bir ACCESS EXCLUSIVE tüm uygulamayı durdurabilir.
  • Uygulama tarafında deadlock retry'ı uygulayın; ama bunu sadece iyi tasarlanmış lock sırasının üzerine bir güvenlik ağı olarak ekleyin.
PostgreSQL tutarlı lock sırası ve advisory lock anahtar tabanlı koordinasyon

Isolation Level ile Lock İlişkisi

Son olarak çoğu kez gözden kaçan bir nokta: isolation level değiştirmek deadlock olasılığını yeniden şekillendirir. READ COMMITTED (varsayılan) her ifade için yeni snapshot alır, çakışmalar kısa sürer ama lost update riski vardır. REPEATABLE READ ve SERIALIZABLE ise daha katı doğruluk getirir; SERIALIZABLE'da deadlock yerine could not serialize access hatası alırsınız ve transaction'ı tekrar denemeniz gerekir.

Yani SERIALIZABLE seçtiğinizde uygulamanız mutlaka retry-aware olmalı. Doğru çözüm tek bir izolasyon seviyesi değil, iş kuralının tutarlılığa ne kadar duyarlı olduğuna göre yapılan bilinçli bir tercihtir. Deadlock, lock sırası ve advisory lock üçlüsünü iyi anlayan bir veritabanı mühendisi, gece 03:14 alarmlarını tasarım aşamasında söndürür — log'da değil.