POSTGRESQL LOCKING VE DEADLOCK

Veri Akademi8 dk okuma
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.

1. 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.

2. 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

3. 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.

4. İ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.

5. 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.

6. Ö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

7. 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.

Sıkça Sorulan Sorular

deadlock_timeout parametresini düşürmek deadlock'ları azaltır mı?

Hayır, sadece tespiti hızlandırır. Deadlock yine oluşur ama PostgreSQL döngüyü daha çabuk fark edip kurbanı iptal eder. Çok düşük değer (örneğin 100ms) verirseniz, normal lock beklemelerini bile deadlock taraması yapmak için CPU harcarsınız. Varsayılan 1 saniye çoğu üretim ortamı için doğru dengedir.

pg_advisory_lock ile pg_advisory_xact_lock arasındaki fark nedir?

İlki session düzeyinde tutulur ve siz pg_advisory_unlock çağırana ya da session kapanana kadar serbest kalmaz. İkincisi transaction düzeyindedir; COMMIT veya ROLLBACK ile otomatik bırakılır. Uygulamada unutmaya açık olduğu için neredeyse her zaman xact varyantı tercih edilmelidir.

SELECT FOR UPDATE NOWAIT ile SKIP LOCKED arasında ne fark var?

NOWAIT, kilitli satıra rastlarsa hata fırlatır ve transaction'ı düşürür. SKIP LOCKED ise kilitli satırı sessizce atlar ve geri kalanını döndürür. Birincisi tutarlılığın kritik olduğu yerlerde; ikincisi paralel iş kuyruğu pattern'inde, birden fazla worker'ın aynı anda farklı işleri alması için kullanılır.

Foreign key sütununa indeks koymazsam neden deadlock riski artar?

Parent tabloda bir satır güncellendiğinde PostgreSQL child tabloda referans veren satırları bulmak için tarama yapar. İndeks yoksa daha geniş satır kümesini kilitler ya da tablo lock'ı seviyesine çıkar. Bu, beklenmedik yerlerde çakışma ve deadlock üretir; FK sütun indeksleri opsiyonel değil, neredeyse zorunludur.

Deadlock yakaladığımda retry yapmam yeterli mi, yoksa kodu da düzeltmeli miyim?

Retry sadece bir güvenlik ağıdır, çözüm değildir. Lock sırası deterministik değilse retry sonsuza dek tekrarlayabilir ve gecikmeyi büyütür. Önce nedeni bulun: hangi iki transaction hangi satırlara hangi sırayla dokunuyor? Sıralamayı sabitleyin (örneğin id ASC), sonra retry'ı sadece nadir yarış durumları için bırakın.

VACUUM çalışırken neden tablom kilitleniyor gibi görünüyor?

Normal VACUUM tabloyu kilitlemez, sadece SHARE UPDATE EXCLUSIVE alır ve DDL'leri engeller. Ama VACUUM FULL tamamen farklıdır: ACCESS EXCLUSIVE alır, tabloya hiçbir okuma/yazma yaklaşamaz. Üretimde VACUUM FULL yerine pg_repack veya pg_squeeze gibi araçlar tercih edilir.

pg_blocking_pids() boş dönüyor ama sorgum yavaş, neden?

Bekleme her zaman lock kaynaklı olmaz. Disk I/O, paralel worker eşitlemesi, ağ ya da büyük sıralama operasyonları da yavaşlık yaratır. pg_stat_activity'deki wait_event_type ve wait_event sütunlarına bakın; LWLock, IO, Client veya Timeout gibi değerler asıl darboğazın lock değil başka bir kaynak olduğunu gösterir.

Advisory lock anahtarı için bigint yerine string kullanabilir miyim?

Doğrudan değil, fonksiyon bigint bekler. Ancak hashtext('my-job-name') ya da hashtextextended ile string'i bigint'e çevirip kullanabilirsiniz. Aynı string'i her zaman aynı sayıya çevirir, yani uygulamanın farklı yerlerinden tutarlı kilit alabilirsiniz. Çakışma olasılığı kuramsal olarak vardır ama pratik iş yüklerinde yok denecek kadar düşüktür.