İLERİ
Distributed Lock
Microservice'ler arası koordinasyon. Redlock algoritması.
Ne Zaman Distributed Lock Kullan / Kullanma
| Kullan | Kullanma | Gerçek Hayat |
|---|---|---|
| Aynı kaynağa eşzamanlı yazma riski var | İdempotent operasyon (aynı işlem tekrar çalışsa sorun yok) | Payment: Aynı sipariş için 2× ödeme çekmeyi engelle |
| Expensive operation'ı 1 kez çalıştırmak yeterli | Read-only operasyonlar | Rapor: Günlük rapor oluşturma — 10 pod'dan sadece 1'i çalışsın |
| Leader election (scheduler, cron job) | Yüksek contention (100+ client aynı lock) → bottleneck | Cron: Her dakika 1 pod "düşen siparişleri iptal et" job'ını çalıştırır |
| Kısa süreli koordinasyon (<30s) | Uzun süreli lock (dakikalar) → expire riski + starvation | Inventory: Stok son 1 birim — 2 kişi aynı anda almaya çalışıyor |
KRİTİK: Lock ≠ veri güvenliği. Lock expire olabilir (GC pause, network partition). Fencing token olmadan distributed lock, yanlış güvenlik hissi verir. Yukarıdaki SVG'de gösterildiği gibi: Client A lock tutarken GC pause yaşar → lock expire olur → Client B lock alır → A uyanır ve stale lock ile yazma yapar → DATA CORRUPTION. Çözüm: Her lock'a monoton artan fencing token ekle, resource tarafında token kontrolü yap.
Gerçek hayat senaryosu — Ödeme duplicate'ini engelleme: Kullanıcı "Öde" butonuna 3 kez basar. Her request
lock:payment:{orderId}alır → sadece ilki başarılı. Lock TTL 30s. İşlem bitince lock release. 2. ve 3. request "lock alınamadı" → HTTP 409 Conflict. Ek güvenlik: DB'depayment_idempotency_keyUNIQUE constraint — lock başarısız olsa bile DB katmanı korur.
# Lock al
SET lock:order-process:5432 "owner-uuid" NX EX 30
# İşlem yap...
# Lock bırak (sadece sahibiysen — Lua ile)
EVAL "
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
else
return 0
end
" 1 lock:order-process:5432 "owner-uuid"
public class DistributedLockService
{
private readonly IDatabase _redis;
private static readonly LuaScript _releaseScript = LuaScript.Prepare(@"
if redis.call('GET', @lockKey) == @lockValue then
return redis.call('DEL', @lockKey)
else
return 0
end
");
public DistributedLockService(IConnectionMultiplexer mux)
=> _redis = mux.GetDatabase();
public async Task<IAsyncDisposable?> AcquireAsync(
string resource, TimeSpan expiry,
TimeSpan? waitTimeout = null, CancellationToken ct = default)
{
var lockKey = $"lock:{resource}";
var lockValue = Guid.NewGuid().ToString("N");
var deadline = DateTime.UtcNow + (waitTimeout ?? TimeSpan.FromSeconds(10));
while (DateTime.UtcNow < deadline)
{
ct.ThrowIfCancellationRequested();
var acquired = await _redis.StringSetAsync(
lockKey, lockValue, expiry, When.NotExists);
if (acquired)
return new LockHandle(_redis, lockKey, lockValue, _releaseScript);
await Task.Delay(50, ct);
}
return null; // Timeout — lock alınamadı
}
}
public class LockHandle : IAsyncDisposable
{
private readonly IDatabase _redis;
private readonly string _lockKey;
private readonly string _lockValue;
private readonly LuaScript _releaseScript;
private bool _released;
public LockHandle(IDatabase redis, string lockKey, string lockValue, LuaScript releaseScript)
{
_redis = redis;
_lockKey = lockKey;
_lockValue = lockValue;
_releaseScript = releaseScript;
}
public async ValueTask DisposeAsync()
{
if (_released) return;
_released = true;
await _redis.ScriptEvaluateAsync(_releaseScript, new
{
lockKey = (RedisKey)_lockKey,
lockValue = _lockValue
});
}
}
// Kullanım
public class OrderProcessor
{
private readonly DistributedLockService _locks;
public async Task ProcessOrderAsync(int orderId)
{
await using var lockHandle = await _locks.AcquireAsync(
$"order-process:{orderId}", TimeSpan.FromSeconds(30));
if (lockHandle is null)
throw new InvalidOperationException("Could not acquire lock");
// Güvenli alan — aynı anda sadece 1 worker burada
await DoProcessing(orderId);
} // DisposeAsync → lock release
}
Lock release mutlaka Lua ile yapılmalı. Aksi halde başkasının lock'unu silebilirsin. GET+DEL atomik değil!
Redlock: Çoklu Instance'da Distributed Lock
Tek Redis instance'da lock alan yukarıdaki pattern, o instance çökerse lock'u kaybeder. Redlock algoritması N bağımsız Redis node'undan N/2+1 quorum ile lock alır.
Ne zaman Redlock gerekir? Sadece Redis master'un çökmesi durumunda bile lock garantisinin devam etmesi gereken senaryolar için. Çoğu uygulamada tek instance lock yeterlidir — Redlock karmaşıklık ekler.
RedLock.net paketi Nisan 2022'den beri güncellenmemiştir (4+ yıl). Production'da kullanıyorsanız fork/maintain riski değerlendirin veya kendi Redlock implementasyonunuzu StackExchange.Redis üzerinde yazın.
dotnet add package RedLock.net
// RedLock.net ile Redlock implementasyonu
var endPoints = new List<RedLockEndPoint>
{
new DnsEndPoint("redis-1", 6379),
new DnsEndPoint("redis-2", 6379),
new DnsEndPoint("redis-3", 6379)
};
builder.Services.AddSingleton<IDistributedLockFactory>(sp =>
RedLockFactory.Create(endPoints));
// Kullanım
public class PaymentProcessor
{
private readonly IDistributedLockFactory _lockFactory;
public async Task ProcessAsync(int orderId)
{
var resource = $"payment:{orderId}";
var expiry = TimeSpan.FromSeconds(30);
var wait = TimeSpan.FromSeconds(10);
var retry = TimeSpan.FromMilliseconds(200);
await using var redLock = await _lockFactory.CreateLockAsync(
resource, expiry, wait, retry);
if (!redLock.IsAcquired)
throw new InvalidOperationException("Lock alınamadı");
// N/2+1 node'dan lock alındı — güvenli alan
await DoPayment(orderId);
} // DisposeAsync → tüm node'lardan release
}
Fencing Token: Lock Expire Sonrası Güvenlik
Kritik senaryo: Client A lock alır → GC pause / network partition → lock expire olur → Client B lock alır → Client A "hâlâ lock'lu" sanarak resource'a yazar → veri bozulması!
Bu sorunu çözmek için fencing token pattern'ı kullanılır:
// Fencing Token Pattern — lock ile korunan resource'a
// monoton artan bir token gönderilir. Resource, eski token'ları reddeder.
public class FencedLockService
{
private readonly IDatabase _redis;
public FencedLockService(IConnectionMultiplexer mux)
=> _redis = mux.GetDatabase();
public async Task<(bool Acquired, long FenceToken)> AcquireWithFenceAsync(
string resource, TimeSpan expiry)
{
var lockKey = $"lock:{resource}";
var fenceKey = $"fence:{resource}";
// Monoton artan fence token üret
var fenceToken = await _redis.StringIncrementAsync(fenceKey);
var acquired = await _redis.StringSetAsync(
lockKey, fenceToken.ToString(), expiry, When.NotExists);
return (acquired, fenceToken);
}
}
// Korunan resource tarafı (DB, API, file system):
public class ProtectedResourceService
{
private long _lastFenceToken = 0;
public bool TryWrite(long fenceToken, Action writeAction)
{
// Eski token'ları reddet — stale lock owner yazamaz
if (fenceToken <= _lastFenceToken)
return false; // REJECTED — bu eski bir lock owner
_lastFenceToken = fenceToken;
writeAction();
return true;
}
}
Ne zaman fencing gerekir? Lock ile korunan işlem idempotent değilse (ör: ödeme, stok düşme, dosya yazma). Idempotent işlemlerde (cache invalidation gibi) fencing gereksiz overhead'dir.