RRedis Handbook

İLERİ

Distributed Lock

Microservice'ler arası koordinasyon. Redlock algoritması.

Kod örneği görünümü Bu sayfadaki eşleşen örnekleri seçilen istemciye göre gösterir.
Lock Timeline (Fencing Token olmadan) Client A Client B t0 t1 (expire) t2 t3 LOCK (token=1) GC pause ⚡ EXPIRE LOCK (token=2) WRITE ✗ WRITE ✓ ⚠ DATA CORRUPTION Çözüm: Fencing Token → resource eski token'ı reddeder ↓ Resource: "token=1 < son token=2 → REJECT"

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'de payment_idempotency_key UNIQUE 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.