Distributed Locking: Redis Redlock ve ZooKeeper
Dağıtık sistemlerde, birden fazla uygulama instance'ı aynı kaynağa erişmeye çalıştığında, bu kaynağın değerinin bozulmasını veya tutarsız hale gelmesini engellemek gerekir. Tek bir JVM içinde synchronized veya ReentrantLock yeterli olurken, servis birden fazla sunucuya yayıldığında bu kilitler işe yaramaz. İşte Distributed Locking (Dağıtık Kilitleme), bu sorunu çözmek için paylaşılan bir harici depo (Redis, ZooKeeper, veritabanı) üzerinden mutex (karşılıklı dışlama) sağlar .
1. Dağıtık Kilitleme Neden Gerekli?
Kullanım Senaryoları:
-
Zamanlanmış bir görevin (cron job) 3 instance'ta birden tetiklenmesi, ancak yalnızca birinin çalışması gerektiği durumlar .
-
Birden fazla consumer'ın aynı kaynağı işlemeye çalışması; aynı kaynağın iki kez işlenmesinin engellenmesi .
-
Grayscale dağıtım veya ana-yedek (master-slave) geçişlerinde geçici bir "lider" seçilmesi .
-
E-ticaret stok yönetiminde aynı anda gelen siparişlerin aşırı satışa (oversell) yol açmaması .
Temel Gereksinimler:
-
Karşılıklı Dışlama (Mutual Exclusion): Aynı anda yalnızca bir istemci kilidi tutabilmeli .
-
Kilitlenme Koruması (Deadlock Freedom): Kilit tutan istemci çökse bile kilit bir süre sonra serbest bırakılmalı .
-
Hata Toleransı (Fault Tolerance): Çoğu düğüm çalıştığı sürece istemciler kilit alıp bırakabilmeli .
2. Redis Redlock Algoritması
A. Tek Instance Kilidin Sorunu
Basit bir Redis kilidi (SET resource NX PX 30000) tek bir Redis instance'ında çalışır. Ancak Redis ana-yedek (master-replica) mimarisinde şu yarış durumu (race condition) oluşur :
-
İstemci A, ana düğümde kilidi alır.
-
Ana düğüm, yazma işlemini yedeğe çoğaltmadan çöker.
-
Yedek düğüm ana düğüm olarak terfi eder.
-
İstemci B, aynı kilit için başarılı bir şekilde
SET NXyapar.
Sonuç: İki istemci de aynı kilidi tuttuğunu düşünür. Bu, karşılıklı dışlama garantisini ihlal eder .
B. Redlock Çözümü
Redlock, N adet bağımsız Redis ana düğümü (tipik olarak 5) kullanır. Bu düğümler birbirleriyle replikasyon yapmaz. Kilit, ancak istemci düğümlerin çoğunluğunda (N/2 + 1, yani 5 düğümden en az 3'ünde) kilidi alabildiğinde geçerli sayılır .
Kilit Alma Adımları:
-
İstemci mevcut zamanı milisaniye cinsinden alır (T1) .
-
Tüm N instance'a sırayla veya paralel olarak kilit isteği gönderir:
text
SET resource_name <random_value> NX PX 30000
Her instance için kısa bir timeout (5-50 ms) kullanılır.
random_valuebenzersiz olmalıdır . -
Zaman tekrar alınır (T2) ve geçen süre hesaplanır:
elapsed = T2 - T1. -
Kilit şu koşullarda başarılı sayılır:
-
En az N/2 + 1 instance'da kilit alınmış olmalı.
-
elapsed < TTL(kilit geçerlilik süresi) .
-
-
Kilit alındıysa, gerçek geçerlilik süresi:
text
validity_time = TTL - elapsed - clock_drift_allowance
-
Kilit alınamadıysa (çoğunluk sağlanamadı veya süre aşıldı), tüm instance'larda kilit derhal serbest bırakılır .
Kilit Bırakma:
Lua script ile yalnızca kendi random_value'si eşleşiyorsa silinir:
lua
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
Bu, başka bir istemcinin kilidini yanlışlıkla silmeyi engeller .
C. Redlock'un Sınırları
-
Saat Kayması (Clock Drift): Redlock, yerel saatlerin yaklaşık aynı hızda ilerlediğini varsayar. Ciddi saat kaymaları güvenliği riske atar .
-
Fencing Sorunu: İstemci A kilidi alır, sonra GC duraklaması yaşar, kilit süresi dolar, İstemci B kilidi alır, sonra İstemci A devam eder. İki istemci de aynı kaynağa erişir .
-
Martin Kleppmann'in Eleştirisi: Redlock'in saat varsayımlarına dayandığını ve fencing token olmadan güvenli olmadığını savunur .
3. ZooKeeper Tabanlı Dağıtık Kilit
ZooKeeper, veri modeli dosya sistemine benzeyen bir koordinasyon servisidir. Dağıtık kilit için geçici sıralı düğümler (ephemeral sequential znodes) ve Watcher mekanizması kullanılır .
A. Çalışma Prensibi
-
Tüm istemciler
/locks/job-lock/dizini altında geçici sıralı düğüm oluşturur:lock-0000000012. -
Her istemci, dizindeki tüm alt düğümleri okur ve sıralar .
-
Kendi düğümü en küçük sıraya sahipse, kilidi almıştır .
-
Değilse, yalnızca kendinden bir önceki düğümü dinler (izler) .
-
Önceki düğüm silindiğinde, istemci tekrar kontrol eder ve en küçük olup olmadığını görür .
-
Kilit bırakıldığında, istemci kendi düğümünü siler; istemci çökerse, oturum süresi dolunca düğüm otomatik silinir .
Kritik Tasarım Noktaları:
-
Sıralı Düğümler: Doğal bir kuyruk oluşturur; herkes aynı düğüm için yarışmaz .
-
Önceki Düğümü Dinleme: Tüm istemcilerin ebeveyn dizini dinlemesi "sürü etkisi" (herd effect) yaratır. Yalnızca bir önceki düğümü dinlemek, her düğüm değişiminde yalnızca bir istemcinin uyanmasını sağlar .
-
Geçici Düğümler: İstemci çöktüğünde otomatik temizlik sağlar; deadlock riskini azaltır .
B. ZooKeeper'ın Avantajları
-
CP Sistemi (Tutarlılık + Bölünme Toleransı): Yazma işlemleri çoğunluk düğüme senkronize edilmeden onaylanmaz. Lider çöktüğünde, en güncel veriye sahip düğüm lider seçilir (ZAB protokolü) .
-
Kilit Sahibinin Çökmesi Durumunda Anında Temizlik: Geçici düğüm, oturum süresi dolunca otomatik silinir; Redis'teki gibi TTL süresini beklemek gerekmez .
-
Adil Kilit (Fair Lock): Sıralı düğümler sayesinde istemciler talepleri sırasına göre kilidi alır .
C. ZooKeeper'ın Sınırları
-
Performans: Yazma işlemleri çoğunluk düğüme senkronize edildiği için Redis'e göre daha yüksek gecikme ve daha düşük QPS .
-
GC ve Ağ Gecikmesi: İstemci GC duraklaması veya ağ bölünmesi yaşarsa, oturum süresi dolabilir ve kilit kaybedilebilir; ancak istemci hala kilidi tuttuğunu düşünebilir .
-
Sürü Etkisi Riskleri: Yanlış tasarım (tüm istemcilerin aynı düğümü dinlemesi) durumunda performans sorunları .
4. Redlock vs. ZooKeeper: Karşılaştırma
| Kriter | Redis Redlock | ZooKeeper |
|---|---|---|
| CAP Modeli | AP (Yüksek kullanılabilirlik) | CP (Güçlü tutarlılık) |
| Performans (QPS) | Çok yüksek (10万+) | Orta-yüksek (çoğunluk yazma nedeniyle düşük) |
| Kilit Bırakma | TTL ile otomatik (gecikmeli) | Oturum sonlanınca anında (geçici düğüm) |
| Adillik (Fairness) | Genelde adil değil (Redisson ile opsiyonel) | Doğal adil (sıralı düğümler) |
| Kilit Türleri | Mutex, ReadWriteLock, Semaphore (Redisson) | Mutex, ReadWriteLock, Semaphore, MultiLock (Curator) |
| .NET Desteği | StackExchange.Redis, RedLock.net, DistributedLock | Apache Curator (Java); .NET için DistributedLock |
| En Uygun Senaryo | Yüksek performans, hafif tutarlılık gereksinimleri | Güçlü tutarlılık, koordinasyon görevleri |
5. Fencing Token ile Çit Sorunu Çözümü
Tüm otomatik süreli kilitlerde görülen çit sorunu (fencing problem), kilit süresi dolduktan sonra eski sahibinin hala kaynağa erişmesi durumudur .
Çözüm: Fencing Token (Monotonik Artan Token)
-
Kilit alındığında, monoton olarak artan bir token (ör. 33, 34, 35...) alınır .
-
Kaynağa erişirken bu token da gönderilir.
-
Kaynak, kendisine gelen en yüksek token'ı saklar ve daha düşük token'lı istekleri reddeder .
csharp
// Fencing token örneği (kavramsal)
var lockHandle = await distributedLock.AcquireAsync("resource");
var fencingToken = lockHandle.FencingToken; // Monotonik artan
await resource.WriteAsync(data, fencingToken);
// Kaynak, fencingToken < sonGorulenToken ise reddeder
6. .NET'te Dağıtık Kilit: DistributedLock Kütüphanesi
DistributedLock (madelson/DistributedLock), .NET ekosisteminde Redis, ZooKeeper, SQL Server, PostgreSQL ve dosya sistemi gibi farklı arka uçları destekleyen olgun bir kütüphanedir .
Kurulum:
bash
dotnet add package DistributedLock dotnet add package DistributedLock.Redis dotnet add package DistributedLock.ZooKeeper
Redis ile Kullanım:
csharp
using DistributedLock;
using StackExchange.Redis;
var connection = ConnectionMultiplexer.Connect("localhost:6379");
var lockProvider = new RedisDistributedLockProvider(connection.GetDatabase());
// Acquire (bloklar)
await using (var handle = await lockProvider.AcquireLockAsync("my-resource-key"))
{
// Kritik bölge - yalnızca bir istemci burada olabilir
await ProcessOrderAsync();
}
ZooKeeper ile Kullanım:
csharp
using DistributedLock;
using DistributedLock.ZooKeeper;
var lockProvider = new ZooKeeperDistributedLockProvider("localhost:2181");
await using (var handle = await lockProvider.AcquireLockAsync("my-resource-key"))
{
// Kritik bölge
await ProcessOrderAsync();
}
TryAcquire (Bloklamadan Deneme):
csharp
using (var handle = await lockProvider.TryAcquireLockAsync("my-resource-key"))
{
if (handle != null)
{
// Kilit alındı
await ProcessOrderAsync();
}
else
{
// Kilit başkasında - atla veya bekle
_logger.LogInformation("Kilit alınamadı, başka bir instance işliyor.");
}
}
ABP Framework ile Entegrasyon:
csharp
// ABP'nin IAbpDistributedLock servisi
public class MyService : ITransientDependency
{
private readonly IAbpDistributedLock _distributedLock;
public async Task MyMethodAsync()
{
await using (var handle = await _distributedLock.TryAcquireAsync("MyLockName"))
{
if (handle != null)
{
// Kilit alındı
}
}
}
}
ABP, DistributedLock kütüphanesini sarmalayarak Redis ve ZooKeeper desteği sunar .
7. En İyi Pratikler
| Pratik | Açıklama |
|---|---|
| Kilit Adını Doğru Seçin | Kaynağa özgü, benzersiz ve anlamlı isimler kullanın. |
| TTL Belirleyin | İşlem süresinden uzun, ancak makul bir TTL (ör. 30 saniye) ayarlayın. |
| Fencing Token Kullanın | Kritik kaynaklar için monotonik token ile eski sahiplerin erişimini engelleyin . |
| Idempotency Sağlayın | Kilit süresi dolduktan sonra işlem devam ederse, işlemin tekrarı zararsız olsun. |
| Kilit Süresini Uzatın (Redisson Watchdog) | Uzun süren işlemler için kilit otomatik yenilenmeli . |
| Granülerliği Doğru Ayarlayın | Çok geniş kilit (tüm tablo) yerine kaynak bazlı kilit (satır bazlı) tercih edin. |
| Hata Yönetimi | Kilit alınamazsa ne yapılacağını netleştirin (bekle, atla, hata dön). |
| Monitoring ve Loglama | Kilit bekleme sürelerini ve başarısızlıkları izleyin . |
| Kritik Bölgeyi Kısa Tutun | Kilit içindeki işlem ne kadar kısa olursa, çekişme o kadar az olur . |
8. Ne Zaman Hangisi?
| Senaryo | Öneri |
|---|---|
| Yüksek performans, hafif tutarlılık yeterli | Redis Redlock (veya Redisson) |
| Güçlü tutarlılık, koordinasyon görevi | ZooKeeper |
| Zaten Redis altyapısı varsa | Redis |
| Zaten ZooKeeper altyapısı varsa | ZooKeeper |
| .NET projesi, kolay entegrasyon | DistributedLock kütüphanesi |
| Kritik finansal işlem | ZooKeeper + Fencing Token |
Sonuç
Dağıtık kilitleme, çok instance'lı sistemlerde kaynak tutarlılığını korumak için vazgeçilmez bir yapı taşıdır. Redis Redlock, yüksek performans ve düşük gecikme sunar; ancak AP modeli nedeniyle ana-yedek geçişlerinde kilit kaybı riski taşır . ZooKeeper, CP modeli ve geçici sıralı düğümler sayesinde güçlü tutarlılık ve otomatik kilit temizliği sağlar; ancak performansı Redis'e göre daha düşüktür . Her iki yaklaşımda da fencing token kullanarak çit sorununu çözmek ve idempotency ile işlem tekrarlarına karşı dayanıklılık sağlamak kritik öneme sahiptir. .NET ekosisteminde DistributedLock kütüphanesi, her iki arka ucu da destekleyerek entegrasyonu kolaylaştırır .