Redis ile ciddi işler yapıyorsanız, er ya da geç şu sorunla karşılaşırsınız: iki ayrı komut arasında başka bir istemci gelip durumu bozuyor. Bir değeri okuyorsunuz, işlem yapıyorsunuz, geri yazıyorsunuz – ama tam o esnada başka biri aynı anahtara dokunmuş. Race condition’ın klasik hali. İşte Lua scripting bu sorunu Redis’in kalbinde çözüyor.
Redis’te Atomiklik Neden Bu Kadar Önemli
Redis tek thread’li bir mimari üzerine kurulu, bu doğru. Ama uygulamanız onlarca, yüzlerce bağlantıyla Redis’e erişiyorsa, her komut ayrı ayrı işleniyor demektir. Yani şöyle bir senaryo düşünün:
İstemci A: GET stock_count -> 1 döndü
İstemci B: GET stock_count -> 1 döndü
İstemci A: SET stock_count 0 (stok düştü)
İstemci B: SET stock_count 0 (hata! ikisi de -1 gitmesi gerekirdi)
Her iki istemci de stoğu 1 gördü ve ikisi de 0 yazdı. Oysa biri 0, diğeri -1 yazmış olmalıydı. Tek ürün iki kez satıldı. Bu sadece e-ticaret için değil, rate limiting, oturum yönetimi, kilit mekanizmaları – her yerde bu sorun çıkabilir.
MULTI/EXEC ile transaction kullanabilirsiniz, ama bu yöntem optimistik locking gerektirir (WATCH komutuyla) ve retry mantığı yazmanız gerekir. Lua scripting ise farklı bir yaklaşım sunar: script Redis’e gönderilir ve Redis onu tek bir atomik işlem olarak çalıştırır. Script çalışırken hiçbir başka komut araya giremez.
Lua Script Temelleri
Redis, Lua 5.1 yorumlayıcısını gömülü olarak barındırır. Script içinde Redis komutlarını redis.call() ya da redis.pcall() fonksiyonuyla çağırırsınız.
# Basit bir Lua script örneği - değer oku ve döndür
redis-cli EVAL "return redis.call('GET', KEYS[1])" 1 mykey
EVAL komutunun yapısı şöyle:
- EVAL script numkeys key [key…] arg [arg…]: Temel çağrı formatı
- script: Lua kodu, string olarak
- numkeys: Kaç tane anahtar geçtiğiniz
- KEYS[1], KEYS[2]…: Geçilen anahtarlara erişim (1-indexed)
- ARGV[1], ARGV[2]…: Anahtarlar dışındaki argümanlara erişim
# KEYS ve ARGV kullanımı
redis-cli EVAL "
local current = redis.call('GET', KEYS[1])
if current == false then
redis.call('SET', KEYS[1], ARGV[1])
return 1
end
return 0
" 1 mykey defaultvalue
Bu script şunu yapıyor: eğer anahtar yoksa verilen değeri set et ve 1 döndür, varsa dokunma ve 0 döndür. Klasik “set if not exists” mantığı ama biraz daha kontrollü.
redis.call ile redis.pcall Farkı
Bu ikisini doğru kullanmak kritik. redis.call() bir hata aldığında script çalışmayı durdurur ve hata fırlatır. redis.pcall() ise hatayı yakalar ve bir tablo olarak döndürür, script devam eder.
# redis.call - hata durumunda script durur
redis-cli EVAL "
redis.call('SET', KEYS[1], 'test')
redis.call('LPUSH', KEYS[1], 'item') -- String'e list komutu, hata verir
return 'bitti'
" 1 mykey
# redis.pcall - hata yakalanır, devam edilir
redis-cli EVAL "
redis.call('SET', KEYS[1], 'test')
local ok, err = pcall(redis.pcall, 'LPUSH', KEYS[1], 'item')
if err then
return 'hata yakalandi: ' .. tostring(err)
end
return 'bitti'
" 1 mykey
Üretim ortamında kritik işlemler için genellikle redis.call() tercih edilir çünkü hata aldığında direkt başarısız olmak, yarım kalmış bir işlemi tamamlamış gibi göstermekten daha güvenlidir.
Gerçek Dünya Senaryosu 1: Stok Yönetimi
E-ticaret projelerinde en sık karşılaşılan atomiklik ihtiyacı stok kontrolü. Hem stoğu kontrol et hem düş, hem de log tut – hepsini tek atomik işlemde yapmak istiyorsunuz.
# Stok düşme scripti - stok varsa düş, yoksa hata döndür
redis-cli EVAL "
local stock_key = KEYS[1]
local log_key = KEYS[2]
local amount = tonumber(ARGV[1])
local order_id = ARGV[2]
local current_stock = tonumber(redis.call('GET', stock_key))
if current_stock == nil then
return redis.error_reply('STOCK_NOT_FOUND')
end
if current_stock < amount then
return redis.error_reply('INSUFFICIENT_STOCK')
end
local new_stock = current_stock - amount
redis.call('SET', stock_key, new_stock)
-- Log kaydı
local timestamp = redis.call('TIME')[1]
local log_entry = order_id .. ':' .. amount .. ':' .. timestamp
redis.call('LPUSH', log_key, log_entry)
redis.call('LTRIM', log_key, 0, 99) -- Son 100 log
return new_stock
" 2 product:123:stock product:123:stock_log 3 ORDER-456
Bu scriptte birden fazla Redis komutu çalışıyor ama tamamı atomik. Script çalışırken product:123:stock anahtarına başka kimse dokunamaz.
Gerçek Dünya Senaryosu 2: Rate Limiting
Rate limiting için sliding window algoritması Lua ile çok temiz yazılır. Klasik yaklaşım: son X saniye içindeki istek sayısını tut.
# Sliding window rate limiter
redis-cli EVAL "
local key = KEYS[1]
local window = tonumber(ARGV[1]) -- saniye cinsinden pencere
local limit = tonumber(ARGV[2]) -- max istek sayisi
local now = tonumber(redis.call('TIME')[1])
local window_start = now - window
-- Pencere disindaki eski kayitlari temizle
redis.call('ZREMRANGEBYSCORE', key, 0, window_start)
-- Mevcut istek sayisini al
local count = redis.call('ZCARD', key)
if count >= limit then
return 0 -- Rate limit asimdi
end
-- Bu istegi ekle (score = timestamp, member = timestamp + random suffix)
local member = now .. ':' .. math.random(100000)
redis.call('ZADD', key, now, member)
redis.call('EXPIRE', key, window)
return limit - count - 1 -- Kalan istek hakki
" 1 ratelimit:user:42 60 100
Bu script her çağrıda eski kayıtları temizliyor, mevcut sayıyı kontrol ediyor ve yeni kaydı ekliyor. Hepsi atomik. 0 dönerse rate limit aşılmış, pozitif değer dönerse kalan hak.
EVALSHA ile Script Yönetimi
Her seferinde script kodunu göndermek verimsiz. Redis, scriptleri SHA1 hash’iyle önbelleğe alır. SCRIPT LOAD ile scripti yükler, sonra EVALSHA ile çağırırsınız.
# Scripti yükle
SCRIPT_SHA=$(redis-cli SCRIPT LOAD "
local key = KEYS[1]
local increment = tonumber(ARGV[1])
local limit = tonumber(ARGV[2])
local current = tonumber(redis.call('GET', key) or '0')
if current + increment > limit then
return redis.error_reply('LIMIT_EXCEEDED')
end
return redis.call('INCRBY', key, increment)
")
echo "Script SHA: $SCRIPT_SHA"
# Sonra her seferinde SHA ile çağır
redis-cli EVALSHA $SCRIPT_SHA 1 counter:daily 5 1000
redis-cli EVALSHA $SCRIPT_SHA 1 counter:daily 3 1000
# Script cache'i kontrol et
redis-cli SCRIPT EXISTS $SCRIPT_SHA
# Sonuç: 1 (var) veya 0 (yok)
# Redis restart olduysa script gitti, yeniden yüklemek gerekir
# Tüm script cache'ini temizle (dikkatli kullanın!)
redis-cli SCRIPT FLUSH
Üretim ortamında deployment scriptlerinizde önce SCRIPT LOAD, sonra SHA’yı uygulama config’ine yazma yaklaşımı yaygın kullanılır.
Gerçek Dünya Senaryosu 3: Dağıtık Kilit (Distributed Lock)
Microservice mimarilerinde aynı işin iki servis tarafından eş zamanlı yapılmaması için dağıtık kilit kullanılır. Lua ile basit ama güvenilir bir implementasyon:
# Kilit edinme scripti (Lock Acquire)
LOCK_ACQUIRE=$(redis-cli SCRIPT LOAD "
local lock_key = KEYS[1]
local lock_value = ARGV[1] -- benzersiz token (UUID)
local ttl = tonumber(ARGV[2]) -- saniye
-- SET NX EX atomik ama biz burada ekstra kontrol istiyoruz
local existing = redis.call('GET', lock_key)
if existing == false then
redis.call('SET', lock_key, lock_value, 'EX', ttl)
return 1 -- Kilit alindi
end
if existing == lock_value then
-- Bizim kilidimiz, TTL'i yenile (kilit yenileme)
redis.call('EXPIRE', lock_key, ttl)
return 2 -- Kilit yenilendi
end
return 0 -- Kilit baskasinda
")
# Kilit bırakma scripti (Lock Release) - sadece bizim kilidimizi silebiliriz
LOCK_RELEASE=$(redis-cli SCRIPT LOAD "
local lock_key = KEYS[1]
local lock_value = ARGV[1]
local existing = redis.call('GET', lock_key)
if existing == lock_value then
redis.call('DEL', lock_key)
return 1 -- Kilit birakildi
end
return 0 -- Kilit bizde degil, dokunma
")
echo "Acquire SHA: $LOCK_ACQUIRE"
echo "Release SHA: $LOCK_RELEASE"
# Kullanim ornegi
LOCK_TOKEN=$(cat /proc/sys/kernel/random/uuid)
LOCK_KEY="lock:email_processor"
# Kilit al
RESULT=$(redis-cli EVALSHA $LOCK_ACQUIRE 1 $LOCK_KEY $LOCK_TOKEN 30)
if [ "$RESULT" = "1" ] || [ "$RESULT" = "2" ]; then
echo "Kilit alindi, is yapiliyor..."
# ... asil is buraya ...
sleep 2
# Kilit birak
redis-cli EVALSHA $LOCK_RELEASE 1 $LOCK_KEY $LOCK_TOKEN
echo "Kilit birakildi"
else
echo "Kilit baskasinda, atlanıyor..."
fi
Buradaki kritik nokta: kilit bırakma sırasında önce değeri kontrol edip sonra sil. Bu iki adım atomik olmadan yapılırsa, bizim TTL süremiz dolarken başkası kilidi alabilir ve biz onun kilidini silmiş oluruz. Lua bunu önlüyor.
Hata Yakalama ve Debug
Script geliştirirken redis.log() fonksiyonu hayat kurtarır. Redis log seviyelerine göre yazabilirsiniz.
redis-cli EVAL "
redis.log(redis.LOG_WARNING, 'Script basliyor')
local val = redis.call('GET', KEYS[1])
redis.log(redis.LOG_DEBUG, 'Deger: ' .. tostring(val))
if val == false then
redis.log(redis.LOG_NOTICE, 'Anahtar bulunamadi: ' .. KEYS[1])
return redis.status_reply('nil')
end
return val
" 1 testkey
Log seviyeleri:
- redis.LOG_DEBUG: En detaylı, geliştirme için
- redis.LOG_VERBOSE: Orta detay
- redis.LOG_NOTICE: Önemli durumlar
- redis.LOG_WARNING: Hatalar ve kritik durumlar
Redis config’de loglevel ayarınız debug değilse DEBUG loglar görünmez. Üretimde genellikle notice seviyesi kullanılır.
Zaman Aşımı ve Güvenlik Ayarları
Lua scriptlerin sonsuz döngüye girme ihtimali var. Redis bunu lua-time-limit ile kontrol eder.
# redis.conf ayarları
# Varsayılan 5000ms (5 saniye)
# Bu süre aşılırsa Redis BUSY hatası verir ama script çalışmaya devam eder
lua-time-limit 5000
# Çalışan scripti zorla durdur
redis-cli SCRIPT KILL
# Eğer script veri yazdıysa SCRIPT KILL çalışmaz, sunucuyu kapatmak gerekebilir
redis-cli DEBUG SLEEP 0 # Bazen durumu resetlemek için
Önemli uyarı: SCRIPT KILL sadece yazma yapmamış (read-only) scriptleri durdurabilir. Bir script veri yazdıktan sonra yarıda durdurulursa atomiklik bozulur. Bu yüzden Redis, yazma yapan scriptlerin bitmesini bekler.
# Script güvenlik ayarları - redis.conf
# Tehlikeli komutların script içinden çağrılmasını engelle
rename-command EVAL "" # EVAL'i tamamen devre dışı bırak (aşırı kısıtlama)
rename-command SCRIPT "gizli_script_komutu" # Yeniden adlandır
Cluster Ortamında Lua Scripting
Redis Cluster kullanıyorsanız, Lua scriptleri sadece aynı slot’taki anahtarlarla çalışabilir. Bu önemli bir kısıtlama.
# Cluster'da sorun çıkaran kullanım
redis-cli -c EVAL "
redis.call('SET', KEYS[1], '1') -- slot 5649
redis.call('SET', KEYS[2], '2') -- slot 8193 - HATA!
" 2 key1 key2
# Çözüm: Hash tag kullan
# {user:123} hash tag'i ile tüm anahtarlar aynı slot'a gider
redis-cli -c EVAL "
redis.call('SET', KEYS[1], '1')
redis.call('SET', KEYS[2], '2')
" 2 {user:123}:profile {user:123}:settings
Hash tag süslü parantez içindeki kısma göre slot hesaplar. {user:123}:profile ve {user:123}:settings her ikisi de user:123 üzerinden aynı slot’a düşer.
Performans Optimizasyonu
Lua scriptlerin Redis’te çalışması çok hızlı ama bazı noktalara dikkat etmek gerekiyor.
# Kötü: Döngüde çok fazla Redis çağrısı
redis-cli EVAL "
for i=1,1000 do
redis.call('SET', 'key:' .. i, i)
end
return 'ok'
" 0
# İyi: Pipeline mantığını script içinde uygula, işlemleri toplu yap
redis-cli EVAL "
local keys = {}
local values = {}
-- Önce tüm değerleri oku
for i=1,#KEYS do
values[i] = redis.call('GET', KEYS[i])
end
-- Sonra toplu işle
local total = 0
for i=1,#values do
if values[i] ~= false then
total = total + tonumber(values[i])
end
end
return total
" 5 counter1 counter2 counter3 counter4 counter5
Bir diğer performans ipucu: EVALSHA kullanmak, her seferinde script kodunu ağdan göndermekten çok daha hızlı. Özellikle büyük scriptlerde bu fark belirginleşir.
Python ile Entegrasyon
Gerçek projelerde Redis Lua scriptlerini uygulama kodundan çağırırsınız. Python örneği:
# requirements: redis-py
# pip install redis
python3 << 'EOF'
import redis
import uuid
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
# Script tanımı
RATE_LIMIT_SCRIPT = """
local key = KEYS[1]
local window = tonumber(ARGV[1])
local limit = tonumber(ARGV[2])
local now = tonumber(redis.call('TIME')[1])
local window_start = now - window
redis.call('ZREMRANGEBYSCORE', key, 0, window_start)
local count = redis.call('ZCARD', key)
if count >= limit then
return 0
end
local member = tostring(now) .. ':' .. tostring(math.random(999999))
redis.call('ZADD', key, now, member)
redis.call('EXPIRE', key, window)
return limit - count - 1
"""
# Script'i kaydet
rate_limit = r.register_script(RATE_LIMIT_SCRIPT)
# Kullan
user_id = "user:42"
for i in range(5):
result = rate_limit(keys=[f"ratelimit:{user_id}"], args=[60, 3])
if result > 0:
print(f"İstek {i+1} kabul edildi. Kalan hak: {result}")
else:
print(f"İstek {i+1} REDDEDILDI - Rate limit aşıldı")
EOF
register_script() metodu sha’yı cache’ler ve her çağrıda EVALSHA kullanır, EVAL değil. Otomatik olarak script yönetimini halleder.
Yaygın Hatalar ve Çözümleri
Lua scripting yaparken sık karşılaşılan durumlar:
Tip dönüşümü sorunları: Redis her şeyi string döndürür, Lua’da aritmetik yapmadan önce tonumber() kullanın.
redis-cli EVAL "
local val = redis.call('GET', KEYS[1])
-- Yanlış:
-- return val + 10 -- string + number = hata
-- Doğru:
return tonumber(val or '0') + 10
" 1 mycount
Nil değer kontrolü: Redis’te olmayan anahtar false döner, nil değil.
redis-cli EVAL "
local val = redis.call('GET', KEYS[1])
-- Yanlış:
-- if val == nil then ...
-- Doğru:
if val == false then
return 'anahtar yok'
end
return val
" 1 existingkey
Global değişken kirliliği: Lua’da local kullanmadan tanımlanan değişkenler global olur ve farklı script çağrıları arasında sorun yaratabilir. Her zaman local kullanın.
Sonuç
Lua scripting Redis’in gizli silahlarından biri. Çoğu sysadmin Redis’i basit bir key-value store olarak kullanıp geçiyor, ama gerçek gücü tam da bu tür atomik işlem kabiliyetlerinde yatıyor. Stok yönetiminden rate limiting’e, dağıtık kilitlerden karmaşık sayaç mantığına kadar birçok kritik senaryoyu güvenli ve tutarlı biçimde çözüyor.
Birkaç temel noktayı aklınızda tutun: EVALSHA kullanarak network trafiğini azaltın, cluster ortamında hash tag ile anahtarları aynı slot’a yönlendirin, lua-time-limit’in farkında olun ve local değişken disiplinine uyun. Bu dört kural çoğu üretim sorununu baştan engeller.
Özellikle yüksek eş zamanlılık gerektiren sistemlerde MULTI/EXEC+WATCH kombinasyonunu Lua script ile değiştirdiğinizde hem kod karmaşıklığı düşer hem de retry mantığından kurtulursunuz. Bir kere alışınca, “bunu başka nasıl yapardım” diye düşündüğünüz durumlar olacak.