· kaynak dev.to (home feed)
Postgres 18'in RETURNING old ve new desteği bir read-modify-write denetim günlüğü tuzağını kapatıyor
Bir dev.to yazısı, uygulama tarafından yazılan denetim günlüklerinin eşzamanlılık altında makul ama yanlış bir geçmiş kaydedebileceğini ve Postgres 18'in RETURNING old/new desteğinin bu düzeltmeyi tek bir ifadeye indirgediğini gösteriyor.

Sessizce başarısız olan örüntü
Yaygın bir denetim günlüğü (audit log) tarifi şöyle işler: uygulama bir satırı okur, yeni değeri kendi kodunda hesaplar, sonucu geri yazar ve öncesi/sonrası değerleriyle birlikte denetim tablosuna bir satır ekler. dev.to'daki bir yazı, bunun eşzamanlılık altında neden bozulduğunu — ve bu tür sorunları yakalamak için eklenen denetim günlüğünün aslında onları gizlediğini inceliyor.
Yazarın yeniden ürettiği senaryoda, bir hesap 100 ile başlıyor ve on eşzamanlı işçi (worker) her biri 10 çekiyor. Her işçi read-modify-write dizisini çalıştırıyor; zamanlamanın belirleyici olması için bilinçli olarak 50 ms'lik bir boşluk bırakılıyor. Sonuç: 90 olan nihai bir bakiye ve hepsi aynı 100'den 90'a geçişi kaydeden on denetim satırı. Dokuz para çekme işlemi kayboldu; çünkü her işçi aynı başlangıç bakiyesini okudu ve diğerlerinin üzerine yazdı.
Yazıya göre asıl zarar veren ayrıntı şu: günlük sağlıklı görünüyor — on satır, boşluk yok, null yok, bir tutarlılık kontrolünün yakalayacağı hiçbir şey yok. Bir olay sırasında bu tablo araştırmacıları bir retry hatasına yönlendirirken gerçek hata kayıp güncelleme (lost-update) yarışıdır. Yapay gecikmeyi kaldırmak ise sonucu temiz yerine belirleyici olmaktan çıkarıyor: art arda üç çalışma 50, 40 ve 60 nihai bakiyeler üretti ve on para çekme işleminden yalnızca dört ile altı farklı eski değer günlüğe kaydedildi.
Postgres 18 neyi değiştiriyor
Bunu iki şey düzeltiyor ve yalnızca biri yeni. Aritmetiği UPDATE'in içine taşımak — SET balance = balance - 10 — her zaman mümkündü ve kayıp güncellemeleri gerçekten önleyen şey budur; çünkü okuma ve yazma tek bir atomik işlem haline gelir. Postgres 18'in eklediği şey ise değişim öncesi ve sonrası satıra RETURNING içinde old. ve new. önekleriyle referans verebilme yeteneğidir; böylece tek bir ifade, uygulama kodunun hatırladığı bayat bir değer yerine değiştirdiği değeri raporlar.
Bu, denetim kaydını tek ifadede güncellemeyle birleştirilebilir hale getirir:
sql WITH moved AS ( UPDATE accounts SET balance = balance - 10 WHERE id = 1 RETURNING old.balance AS was, new.balance AS now ) INSERT INTO audit_log (account_id, old_balance, new_balance) SELECT 1, was, now FROM moved;
Aynı on eşzamanlı işçiyle sonuç tersine döner: nihai bakiye 0, on denetim satırı, on farklı eski değer — 100'den sıfıra inen adım adım bir kayıt. Yazar, 18. sürümden önce aynı garantiyi sağlamanın OLD ve NEW okuyan bir trigger gerektirdiğini, bunun da denetim mantığını uygulamanın okuyucularının nadiren incelediği veritabanı taraflı koda yerleştirdiğini belirtiyor.
Upsert'ler ve xmax çözümü
INSERT'in önceki bir satırı olmadığından, insert işlemlerinde old altındaki her şey null'dır — bu da old.id IS NULL ifadesini bir upsert'in hangi dalı izlediğine dair okunabilir bir teste dönüştürür. Yazı, bunu insert ile update'i ayırt etmek için xmax sistem kolonunu inceleyen (text ve bigint üzerinden dönüştürerek) uzun süredir kullanılan deyimin yerine geçirilebilecek şekilde konumlandırıyor. Yazar, bu hilenin işe yaradığını ve eski kod tabanlarında her yerde görüldüğünü, ancak dahili bir kolon bilgisini gerektirdiğini ve bu bağlama sahip olmayan herkese bir hata gibi göründüğünü yazıyor.
Sürüm 18'in ince ayrıntıları
Yazı ayrıca küçük puntoyu da listeliyor. old altındaki kolonlar INSERT'te, new altındaki kolonlar DELETE'te null'dur; dolayısıyla her ifadeye yönlendirilen genel amaçlı bir denetim yardımcısı sonsuza dek sessizce null kaydedebilir. Adı tam olarak old olan bir kolona sahip tablo, çıplak bir old referansını yine o kolona çözümler ve mevcut sorguları çalışır tutar; yeni takma adları devreye sokan şey old. ve new. önekli biçimlerdir ve hem kolon hem de takma adın aynı ifadede gerekli olduğu durumlarda RETURNING WITH (OLD AS prev, NEW AS cur) ile yeniden adlandırılabilirler. Bu özellik yalnızca 18'e ait: Postgres 17'de aynı sorgu "missing FROM-clause entry for table 'old'" hatasıyla başarısız olur; bu hata bariz biçimde bir sürüm sorununa işaret etmez. Yazar davranışı Docker'daki postgres:18 üzerinde, 18.6 sürümünde doğrulamıştır.
Neden önemli
Read-modify-write dizilerinden kaynaklanan kayıp güncellemeler, işlemsel (transactional) veritabanlarındaki en eski eşzamanlılık hataları arasındadır ve uygulama tarafındaki denetim günlükleri, kendileriyle tutarlı bir kurgu kaydettikleri için bunları teşhis etmeyi zorlaştırabilir. Postgres 18'in RETURNING içindeki old ve new referansları, yazma işleminin ve denetim kaydının tek ifadede gerçekleşmesini sağlar; böylece günlük yalnızca gerçekte meydana gelen değişiklikleri betimleyebilir. Xmax temelli upsert tespitine veya trigger tabanlı denetleme güvenen ekipler, daha net bir alternatif elde eder — bir kez sürüm 18'e standartlaşabildiklerinde.
- #postgresql
- #sql
- #databases
- #concurrency
- #audit-logs