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.
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.PreparevsLoadedLuaScript: Prepare her çağrıda fallback EVAL yapabilir.LoadAsyncile 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 KILLsadece yazma yapmamış script'i durdurur. Yazma yapan script'i durdurmak içinSHUTDOWN NOSAVEgerekir (tehlikeli). Script'leri kısa tut!