PostgreSQL, gerçek serileştirme sağlamak için Serializable Snapshot Isolation (SSI) uygulamakta olup, geleneksel iki aşamalı kilitlemenin performans cezalarından kaçınmak için predikat kilitleme ve serileştirme grafiği testini kullanmaktadır. 40001 hatası (serialization_failure) özellikle yazma kayması veya okuma-yazma çatışmaları sırasında gerçekleşir; burada iki işlem bir rw-bağımlılık döngüsü oluşturur. Örneğin, İşlem A, bir koşulu karşılayan satırları okur (örneğin, WHERE color = 'red'), İşlem B, farklı bir koşulu karşılayan satırları okur (örneğin, WHERE color = 'blue'), ardından A 'mavi'ye, B 'kırmızı'ya güncelleme yapar. Hiçbir işlem diğerinin önünü kesmez, ancak sonuç serileştirilemez.
Bu desen, serileştirme grafiğinde tehlikeli bir yapıyı temsil eder: birbirini izleyen iki rw-anti bağımlılık potansiyel bir döngü oluşturur. PostgreSQL, bunu tespit eder ve anormal durumları önlemek için bir işlemi iptal eder. Sorun, işlemlerin farklı fiziksel satırları değiştirebilmesi nedeniyle ince bir yapıda olmasıdır; bu da çatışmayı daha düşük izolasyon seviyelerinde kullanılan satır kilitleme mekanizmalarına görünmez kılar.
Zorunlu çözüm, uygulamanın bir iyimser tekrar döngüsü uygulamasını gerektirir. SQL EXCEPTION '40001' yakalandığında, uygulama mevcut işlemi geri almalı ve tüm işlemi üstel gerileme ile tekrar denemelidir. Ölümcül kilitlerin aksine, genellikle hemen tekrar deneyerek çözülen, yüksek rekabet altında serileştirme hataları çatışmaları önlemek için düzensiz gecikmelerden fayda sağlar.
-- PL/pgSQL'de uygulama tekrar mantığı örneği DO $$ DECLARE retries INT := 0; max_retries INT := 3; BEGIN WHILE retries < max_retries LOOP BEGIN SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL SERIALIZABLE; PERFORM * FROM inventory WHERE category = 'electronics' AND count > 0; UPDATE inventory SET count = count - 1 WHERE item_id = 123; COMMIT; EXIT; EXCEPTION WHEN SQLSTATE '40001' THEN ROLLBACK; retries := retries + 1; PERFORM pg_sleep(power(2, retries) * 0.1); -- Üstel gerileme END; END LOOP; END $$;
Bir konser bilet değişim platformu, kullanıcıların koltuk kategorilerini değiştirmelerine izin vererek kontrol-et-ardından hareket mantığı ile çalışıyordu. İşlem A, VIP koltukların mevcut olduğunu doğruladıktan sonra, bir VIP koltuğu Standard'a düşürdü. Aynı anda, İşlem B, Standart görünürlüğü doğruladı ve bir Standart koltuğu VIP'ye yükseltti. READ COMMITTED altında, her iki işlem mevcutlığı doğru olarak okudu, güncellemeleri gerçekleştirdi ve sistem hem kategorilerde de negatif envanter ile sonuçlandı, her bir işlem kısıtlamaları kontrol ettiğine rağmen.
Üç çözüm tasarlandı. İlk çözüm, açık SELECT FOR UPDATE kilitlemesini kullandı, ancak mevcutlık sorguları sıfır satır döndürdüğünde başarısız oldu, kilitleri almadan sistemin hayali eklemelere karşı savunmasız kalmasına neden oldu. İkinci yaklaşım, pg_try_advisory_lock() kullanarak ADVISORY LOCKS uyguladı ve bu, koltuk kategorilerine erişimi serileştirdi, çatışmaları önledi fakat karmaşık kilit sırası riskleri getirdi ve tüm kategori kontrollerinin serileştirilmesi nedeniyle verimliliği %40 düşürdü.
Üçüncü çözüm, uygulama seviyesinde tekrar döngüsüyle beraber SERIALIZABLE izolasyonunu benimsedi. Bu, manuel kilit yönetimi olmadan doğruluk garanti ettiği için seçildi ve eşzamanlı değişimlerin düşük sıklığı göz önüne alındığında tekrar yükü kabul edilebilir bulundu. Uygulama, SQLException ile SQLState 40001 yakalayan bir JDBC tekrar yöneticisi kullanarak, 100ms * 2^deneme kadar bekleyip işlemi tekrar yürüttü. Bu, aşırı rezervasyon olaylarını tamamen ortadan kaldırdı, ancak p99 gecikmesi, zirve satış pencerelerinde 15ms arttı.
Serileştirilmiş izolasyondaki predikat kilitleri ile Tekrar Okuma’daki satır kilitleri arasındaki kesin fark nedir?
Tekrar Okuma, sorgu tarafından gerçekten döndürülen satırları kilitleyerek tekrar edilemeyen okumaları önler, ancak diğer işlemler tarafından sorgunun WHERE koşulunu karşılayan yeni satırların eklenmesini engellemez. Serileştirilebilir izolasyon, sorgu koşulunu karşılayan herhangi bir eklemeyi önlemek için arama aralığını kilitleyen predikat kilitleri kullanır; bu, sorgu çalıştırıldığında mevcut olmayan satırlarda bile geçerlidir. Adaylar genellikle bunları karıştırır, yanılarak Tekrar Okuma'nın hayali okumaları önlediğini veya Serileştirilebilir'in yalnızca mevcut satırları kilitlediğini düşünürler.
Bir döngü algısı tespit edildiğinde hangi işlemin iptal edileceğini serileştirme grafiği test algoritması nasıl belirliyor?
PostgreSQL, "ilk taahhüt eden kazanır" stratejisi ile tehlikeli yapı tespitini birleştirir. Eşzamanlı işlemler arasında bir rw-çatışma (okuma-yazma bağımlılığı) oluştuğunda, sistem bu kenarın serileştirme grafiğinde bir döngüyü tamamlayıp tamamlamadığını takip eder. Döngüyü tamamlayan işlem, SQLSTATE 40001 ile iptal edilir. Seçim, işlem yaşı yerine graf yapısına dayalı olarak belirleyicidir ve geri alma maliyetinin en düşük olduğu veya tespit edilen döngüde en son olan işlemlerin iptalini tercih eder. Bu, geçersiz bir geçmişi önleyen bir önleyici iptal olduğu, bekleyen kilitler değil, uygun hata yönetimi için önemlidir.
Neden SELECT FOR UPDATE, Serileştirilebilir izolasyonun bir çatışma tespit etmesi durumunda serileştirme hatalarını önlemekte başarısız olabilir?
SELECT FOR UPDATE, yalnızca yürütme anında mevcut olan satırlarda ROW SHARE kilitleri alır. Kontrol-et-ardından hareket mantıklarında, ilk sorgu sıfır satır döndürdüğünde (örneğin, mevcut koltuk sayısı sıfır olup olmadığını kontrol etme) FOR UPDATE hiç kilit almaz; bu da başka bir işlemin çelişkili bir satır eklemesine izin verir. Serileştirilebilir izolasyon, bu durumu bir predikat çatışması olarak tespit eder çünkü "sıfır satırlar" sonucu, eşzamanlı ekleme ile geçersiz kılınan geçerli bir okuma kümesini temsil eder. Adaylar sıklıkla, FOR UPDATE'nın kapsamlı bir koruma sağladığını varsayıyor ve başlangıçta hiçbir şeyi eşleşmediğinde hayali eklemelere karşı hiçbir savunma sunmadığını fark etmezler.