RRedis Handbook

ORTA

Transactions & Lua Scripting

Her EVAL çağrısında Redis script'i SHA1'e compile eder. Production'da SCRIPT LOAD + EVALSHA ile compile maliyetini sıfırla.

Kod örneği görünümü Bu sayfadaki eşleşen örnekleri seçilen istemciye göre gösterir.

Ne Zaman MULTI/EXEC vs Lua Kullan

Senaryo MULTI/EXEC Lua Script Neden
Basit multi-set (bağımsız yazımlar) Lua overhead gereksiz, MULTI yeterli
Conditional logic (if/else, hesaplama) MULTI içinde koşul yazılamaz
Read → compute → write (atomik) MULTI içinde GET sonucu kullanılamaz
Optimistic lock (WATCH + retry) WATCH semantiği tam uyumlu
Rate limiter, sliding window ZREMRANGEBYSCORE + ZADD + PEXPIRE atomik olmalı
Inventory decrement (stok > 0 ise düş) Read-check-write atomik olmalı

MULTI/EXEC ≠ Rollback: Redis transaction'ları SQL gibi rollback yapmaz. EXEC içindeki komutlardan biri hata verirse diğerleri yine çalışır. "Ya hep ya hiç" sadece çalıştırma garantisi — hata geri alınmaz. Gerçek atomik iş mantığı için Lua kullan.

Gerçek hayat senaryosu — Envanter düşürme: Stok son 3 birim, 5 kullanıcı aynı anda sepete ekliyor. MULTI/EXEC ile: WATCH yaparsın ama yüksek contention'da sürekli retry → performans düşer. Lua ile: if tonumber(redis.call('GET', KEYS[1])) >= qty then redis.call('DECRBY', KEYS[1], qty); return 1 end; return 0 — tek roundtrip, atomik, retry gereksiz.

MULTI/EXEC (Optimistic Locking)

WATCH user:1:balance
MULTI
DECRBY user:1:balance 200
INCRBY user:2:balance 200
EXEC
# WATCH'taki key değiştiyse EXEC nil döner (retry gerekir)
public class TransferService
{
    private readonly IDatabase _redis;

    public TransferService(IConnectionMultiplexer mux)
        => _redis = mux.GetDatabase();

    public async Task<bool> TransferAsync(string fromUser, string toUser, long amount)
    {
        var tran = _redis.CreateTransaction();

        // Condition: balance değişmemişse devam et
        tran.AddCondition(Condition.StringGreaterThan($"user:{fromUser}:balance", amount - 1));

        _ = tran.StringDecrementAsync($"user:{fromUser}:balance", amount);
        _ = tran.StringIncrementAsync($"user:{toUser}:balance", amount);

        return await tran.ExecuteAsync(); // false → condition failed
    }
}

Lua Script (Atomik Operasyonlar)

-- Rate limiter (sliding window)
EVAL "
    local key = KEYS[1]
    local limit = tonumber(ARGV[1])
    local window = tonumber(ARGV[2])
    local now = tonumber(ARGV[3])
    redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
    local count = redis.call('ZCARD', key)
    if count < limit then
        redis.call('ZADD', key, now, now .. ':' .. math.random(1000000))
        redis.call('PEXPIRE', key, window)
        return 1
    end
    return 0
" 1 rate:api:user:1001 100 60000 1716825600000
public class LuaScriptService
{
    private readonly IDatabase _redis;

    // Script'i bir kez hazırla, SHA ile çağır (bandwidth tasarrufu)
    private static readonly LuaScript _rateLimitScript = LuaScript.Prepare(@"
        local key = @key
        local limit = tonumber(@limit)
        local window = tonumber(@window)
        local now = tonumber(@now)
        redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
        local count = redis.call('ZCARD', key)
        if count < limit then
            redis.call('ZADD', key, now, now .. ':' .. math.random(1000000))
            redis.call('PEXPIRE', key, window)
            return 1
        end
        return 0
    ");

    public LuaScriptService(IConnectionMultiplexer mux)
        => _redis = mux.GetDatabase();

    public async Task<bool> IsAllowedAsync(string clientId, int limit = 100, int windowMs = 60000)
    {
        var result = await _redis.ScriptEvaluateAsync(_rateLimitScript, new
        {
            key = (RedisKey)$"rate:{clientId}",
            limit,
            window = windowMs,
            now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
        });
        return (int)result == 1;
    }
}

Lua atomik çalışır — script sırasında başka komut araya giremez. Ama uzun script = tüm Redis'i bloklar. Kısa tut (<5ms).

Script Caching (EVALSHA)

# 1. Script'i yükle (SHA1 döner)
SCRIPT LOAD "return redis.call('INCR', KEYS[1])"
# "e0e1f9fabfc9d4800c877a703b823ac0578ff831"

# 2. SHA ile çalıştır (compile yok → hızlı)
EVALSHA e0e1f9fabfc9d4800c877a703b823ac0578ff831 1 mycounter

# Script var mı kontrol
SCRIPT EXISTS e0e1f9fabfc9d4800c877a703b823ac0578ff831

# Tüm script cache'i temizle (failover sonrası otomatik olur)
SCRIPT FLUSH
// StackExchange.Redis LuaScript.Prepare() otomatik EVALSHA kullanır.
// İlk çalıştırmada EVALSHA dener, script yoksa otomatik EVAL yapar ve cache'ler.
// Manuel kontrol gerekmez — kütüphane yönetir.

// Ancak LoadedLuaScript ile açıkça preload yapabilirsin:
private static readonly LuaScript _script = LuaScript.Prepare(@"
    local current = redis.call('INCRBY', @key, @amount)
    if current == tonumber(@amount) then
        redis.call('EXPIRE', @key, @ttl)
    end
    return current
");

// Startup'ta server'a yükle (SHA cache'le)
private LoadedLuaScript? _loaded;

public async Task InitializeAsync()
{
    var server = _mux.GetServers().First();
    _loaded = await _script.LoadAsync(server);
}

// Kullanım: EVALSHA ile çalışır (script gövdesi gönderilmez)
public async Task<long> IncrementWithTtlAsync(string key, int amount, int ttlSeconds)
{
    var result = await _loaded!.EvaluateAsync(_db, new
    {
        key = (RedisKey)key,
        amount,
        ttl = ttlSeconds
    });
    return (long)result;
}

LuaScript.Prepare vs LoadedLuaScript: Prepare her çağrıda fallback EVAL yapabilir. LoadAsync ile preload edersen ilk çağrıda bile SHA kullanır → bandwidth tasarrufu (büyük script'lerde önemli).

Lua Error Handling

Lua script hataları RedisServerException olarak fırlatılır. Failover sonrası script cache silinir → NOSCRIPT hatası gelir.

public class SafeScriptExecutor
{
    private readonly IDatabase _redis;
    private readonly ILogger<SafeScriptExecutor> _logger;

    private static readonly LuaScript _myScript = LuaScript.Prepare(@"
        local val = redis.call('GET', @key)
        if val == false then return -1 end
        return tonumber(val)
    ");

    public SafeScriptExecutor(IConnectionMultiplexer mux,
        ILogger<SafeScriptExecutor> logger)
    {
        _redis = mux.GetDatabase();
        _logger = logger;
    }

    public async Task<long> ExecuteSafeAsync(string key)
    {
        try
        {
            var result = await _redis.ScriptEvaluateAsync(_myScript, new
            {
                key = (RedisKey)key
            });
            return (long)result;
        }
        catch (RedisServerException ex) when (ex.Message.StartsWith("NOSCRIPT"))
        {
            // Failover sonrası script cache silindi
            // LuaScript.Prepare otomatik fallback yapar,
            // ama LoadedLuaScript kullanıyorsan reload gerekir:
            _logger.LogWarning("Script cache miss — NOSCRIPT. Retrying with EVAL...");
            var result = await _redis.ScriptEvaluateAsync(_myScript, new
            {
                key = (RedisKey)key
            });
            return (long)result;
        }
        catch (RedisServerException ex) when (ex.Message.Contains("ERR"))
        {
            // Lua runtime error (nil access, type mismatch, vb.)
            _logger.LogError(ex, "Lua script error for key {Key}: {Msg}", key, ex.Message);
            throw new InvalidOperationException($"Redis Lua error: {ex.Message}", ex);
        }
    }
}
Hata Mesaj Prefix Çözüm
Script yok NOSCRIPT LuaScript.Prepare otomatik retry yapar. LoadedLuaScript ise reload gerekir.
Lua runtime ERR user_script Script logic hatası — debug et (redis.log veya EVAL ile test)
Timeout BUSY Script çok uzun sürüyor. SCRIPT KILL ile durdur.
Readonly READONLY Replica'ya script gönderilmiş — master'a yönlendir.

SCRIPT KILL sadece yazma yapmamış script'i durdurur. Yazma yapan script'i durdurmak için SHUTDOWN NOSAVE gerekir (tehlikeli). Script'leri kısa tut!