Bakiye güncellemede race condition: Pessimistic Lock mı, Redlock mı?
Soru
Kullanıcı bakiyesinden para düşen bir kuyruk işimiz var. Kullanıcı hızlı arka arkaya iki işlem tetiklerse kuyrukta aynı kullanıcı için iki job oluşuyor; 10 paralel worker var. Worker 1 ve 2 aynı anda eski bakiyeyi okuyor (100 TL), ikisi de 40 düşüp 60 TL yazıyor (olması gereken 20 TL). Bu race condition'ı önlemek için Pessimistic Locking mi yoksa Redis dağıtık kilit (Redlock) mı kullanmalıyım?
Cevap
Kısa cevap: Yaşadığın şey klasik lost update (kayıp güncelleme) problemi. Para söz konusuysa invariant’ı DB’de tut; en temiz çözüm atomik koşullu bir UPDATE, ikinci tercih SELECT ... FOR UPDATE. Redlock’a koşma.
Kısa cevap
Senaryonun özü: oku-değiştir-yaz (read-modify-write) döngüsü paralel worker’lar arasında bölünüyor. İkisi de eski değeri okuyor, ikisi de üstüne yazıyor; biri kayboluyor. Çözüm bu döngüyü atomik kılmak. FOR UPDATE ile satır kilitlemeye geçtiğinde kilit sırasının tutarlı olması gerekir; deadlock’un nasıl doğduğunu ve nasıl önlendiğini deadlock kaydında anlatmıştım.
Neden
- Invariant’ı uygulama değil veritabanı korumalı. Okuma ve yazma tek ifadede birleştiğinde DB satır kilidini kendi yönetir; hem race’i çözer hem de
balance >= 40gibi bir koşulla eksiye düşmeyi reddeder. - Redlock, DB içi bir invariant için fazla kırılgan. Redis dağıtık kilidi (Redlock), korunan şey tek bir DB satırı olmadığında (servisler/kaynaklar arası koordinasyon) anlamlıdır — ve clock kayması/timeout gibi köşe durumları vardır. Bir bakiye DB’nin transaction’ıyla zaten korunabiliyorken onu dağıtık bir kilide emanet etme.
- Optimistic locking yoğun rekabette geri teper.
versionkolonuna dayalı yaklaşım çakışma seyrekse ucuzdur, ama yoğun rekabette retry fırtınası yaratır — senin senaryonda aynı kullanıcı için art arda job geliyorsa bu risk gerçektir.
Ne yapmalı
- En iyisi: atomik koşullu
UPDATE.UPDATE accounts SET balance = balance - 40 WHERE id = ? AND balance >= 40yaz. Okuma ve yazma tek ifadede; uygulama tarafında kilit gerekmez. Etkilenen satır 0 ise işlem yetersiz bakiyeden başarısız demektir. - Alternatif:
SELECT ... FOR UPDATE. Mantık okuma ile yazma arasında karmaşıksa, transaction içinde bakiye satırınıFOR UPDATEile kilitle. İkinci worker o satıra geldiğinde birincinin commit’ini bekler; sıraya girerler, çakışma olmaz. Pessimistic locking budur. - Düşük çakışmada optimistic locking’i değerlendir. Satıra bir
versionkolonu ekle; güncellerkenWHERE version = ?koy, eşleşmezse retry et. - Redlock’u yalnızca DB dışı kaynak için kullan. Korunması gereken şey bir DB satırı değilse — örneğin servisler arası bir kaynak — o zaman dağıtık kilit anlamlı olur.
Sonuç: Atomik koşullu UPDATE (ya da SELECT FOR UPDATE) kullan; Redlock’u sadece DB dışı kaynaklar için sakla. Para için kuralı transaction’ın garanti ettiği yerde — DB’de — tut; dağıtık kilit, veritabanının kendisinin zaten zorlayabileceği bir invariant’ı korumak için fazla kırılgan bir araçtır.
İlgili Yazılar
Yorumlar
Yorum yapmak için GitHub hesabınızla giriş yapmanız yeterli. Yorumlar GitHub Discussions üzerinde saklanır.