Redis Lua betikleri, EVAL komutuyla sunucuya gönderilip orada, gömülü Lua 5.1 yorumlayıcısında çalıştırılan küçük programlardır. Betik çalışırken sunucu başka hiçbir komutu işlemez; bu yüzden betiğin yaptığı tüm okuma ve yazmalar atomiktir, yani etkiler ya henüz hiç olmamıştır ya da hepsi olmuştur. Betiğe anahtar adlarını KEYS, diğer değerleri ARGV tablosuyla verirsiniz; aynı betiği her seferinde yeniden göndermemek için SCRIPT LOAD ile önbelleğe yükleyip SHA1 özetiyle EVALSHA çağırırsınız.
Bu yazıda EVAL sözdizimini, redis.call ile redis.pcall farkını, Lua ile Redis arasındaki veri tipi dönüşümlerini, betik önbelleğini ve NOSCRIPT hatasını, en uzun çalışma süresini ve BUSY hatasını, betik bayraklarını, salt okunur betikleri ve hata ayıklayıcıyı belgelerdeki örneklerle ele alıyoruz. Redis 7 ile gelen ve sunucuda kalıcı olarak saklanan alternatif için Redis Functions yazımıza, serinin tamamı için Redis öğrenme rehberi sayfasına bakın.
Neden sunucu tarafında betik?

Belgeler betiklerin üç temel faydasını sayar. Birincisi veri yerelliğidir: mantık verinin bulunduğu yerde çalıştığı için ağ trafiği ve toplam gecikme azalır. İkincisi atomik yürütmedir: betik çalışırken sunucu bloklanır, araya başka bir istemcinin komutu giremez. Üçüncüsü bileşimdir: Redis'te bulunmayan ya da çekirdeğe eklenemeyecek kadar özel olan bir işlemi mevcut komutları birleştirerek kendiniz kurabilirsiniz. Örneğin birden fazla anahtar üzerinde, farklı veri tiplerini birlikte kullanan koşullu bir güncelleme tek bir betikle yapılabilir.
Bu blokaj davranışının bir bedeli de vardır: betik ne kadar uzun sürerse diğer tüm istemciler o kadar bekler. Belgeler betik yükünün çok düşük olduğunu, hızlı betik yazmanın zor olmadığını, ama yavaş bir betik çalıştırmanın iyi bir fikir olmadığını vurgular.
İlk betik: EVAL sözdizimi
EVAL komutunun ilk argümanı betiğin Lua kaynak kodudur, ikincisi ise ardından gelecek anahtar adı argümanlarının sayısıdır (numkeys). Betiğin bir Lua fonksiyonu tanımlaması gerekmez; doğrudan çalışacak bir Lua programıdır.
> EVAL "return 'Hello, scripting!'" 0
"Hello, scripting!"Burada 0, hiçbir anahtar adı verilmediği anlamına gelir. Betiğe değer geçirmek için kaynağı her seferinde değiştirmek yerine parametre kullanılır. Belgeler betik kaynağını uygulamada dinamik olarak üretmeyi açıkça bir anti-desen olarak tanımlar, çünkü her farklı kaynak önbellekte ayrı bir betik olarak yer kaplar:
redis> EVAL "return ARGV[1]" 0 Hello
"Hello"
redis> EVAL "return ARGV[1]" 0 Parameterization!
"Parameterization!"KEYS ve ARGV: anahtar adları neden ayrı verilir?
Redis, anahtar adı olan argümanlarla diğer argümanları birbirinden ayırır. Anahtar adı da bir dizedir, ama veritabanındaki bir anahtarı temsil eder ve Redis Cluster'ın çalışma biçiminin temelidir. numkeys kadar argüman KEYS tablosuna, kalanlar ARGV tablosuna yerleşir. Lua tabloları 1'den başlayarak numaralanır:
redis> EVAL "return { KEYS[1], KEYS[2], ARGV[1], ARGV[2], ARGV[3] }" 2 key1 key2 arg1 arg2 arg3
1) "key1"
2) "key2"
3) "arg1"
4) "arg2"
5) "arg3"redis.call ve redis.pcall
Betik içinden Redis komutu çalıştırmanın iki yolu vardır: redis.call() ve redis.pcall(). İkisi de komutu argümanlarıyla çalıştırır ve yanıtı döndürür. Fark çalışma zamanı hatalarında ortaya çıkar. redis.call() bir hata oluşursa bu hatayı doğrudan betiği çalıştıran istemciye iletir ve betik orada durur. redis.pcall() ise hiçbir zaman istisna fırlatmaz; hatayı err alanı olan bir tablo olarak betiğe geri verir, böylece betik hatayı ele alabilir.
> EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 foo bar
OKBelgedeki hata örneğinde ECHO tam olarak bir argüman beklediği halde dört argümanla çağrılır ve redis.call() hatayı istemciye taşır:
redis> EVAL "return redis.call('ECHO', 'Echo,', 'echo... ', 'eco... ', 'o...')" 0
(error) ERR Wrong number of args calling Redis command from script script: b0345693f4b77517a711221050e76d24ae60b7f7, on @user_script:1.Aynı durumu redis.pcall() ile yakalayıp sunucu günlüğüne yazan ve kendi hata mesajını döndüren betik belgede şöyle verilir:
local reply = redis.pcall('ECHO', unpack(ARGV))
if reply['err'] ~= nil then
-- Hatayı şimdilik yalnızca günlüğe yaz
redis.log(redis.LOG_WARNING, reply['err'])
reply['err'] = 'ERR Something is wrong, but no worries, everything is under control'
end
return replyKendi hata ya da durum yanıtınızı döndürmek için redis.error_reply() ve redis.status_reply() yardımcıları vardır; ikisi de sırasıyla { err = ... } ve { ok = ... } tablosu döndürmekle eşdeğerdir. Belgeler, hata dizesinin ilk kelimesinin hata kodu olması geleneğine (genel hatalar için ERR) uymanızı önerir:
redis> EVAL "return redis.error_reply('ERR My very special reply error')" 0
(error) ERR My very special reply error
redis> EVAL "return redis.status_reply('TOCK')" 0
TOCKLua ve Redis arasında veri tipi dönüşümü

redis.call() ve redis.pcall() yanıtları otomatik olarak Lua tiplerine, betiğin return ile döndürdüğü değer de otomatik olarak Redis protokolüne çevrilir. Betik çalışırken varsayılan protokol RESP2'dir. En sık karşılaşacağınız kurallar şunlardır:
| Redis (RESP2) yanıtı | Lua karşılığı |
|---|---|
| Integer reply | Lua number |
| Bulk string reply | Lua string |
| Array reply | Lua table (iç içe olabilir) |
| Status reply (OK gibi) | ok alanı olan tablo |
| Error reply | err alanı olan tablo |
| Null bulk / null multi-bulk | Lua false |
Ters yönde üç kural özellikle sürpriz yaratır. Lua'da tek bir sayı tipi vardır ve Redis bunu her zaman tamsayı yanıtına çevirir, ondalık kısmı atar; ondalıklı bir değer döndürmek istiyorsanız dize olarak döndürmelisiniz. Lua dizisinde bir nil değeri varsa dönüşüm orada durur. İlişkisel (anahtar-değer) tablo alanları ise yanıta hiç alınmaz. Belgedeki örnek üçünü birden gösterir:
redis> EVAL "return { 1, 2, 3.3333, somekey = 'somevalue', 'foo', nil , 'bar' }" 0
1) (integer) 1
2) (integer) 2
3) (integer) 3
4) "foo"3,3333 değeri 3 olmuş, somekey alanı atılmış ve nil sonrasındaki "bar" dönmemiştir. Ayrıca Lua true değeri RESP2'de 1 tamsayısına, false ise null yanıtına çevrilir. Betik redis.setresp(3) çağırarak komut yanıtlarını RESP3 olarak almayı seçebilir; bu durumda map, set ve double gibi RESP3 tipleri de tablo olarak gelir.
Betik önbelleği, SCRIPT LOAD ve EVALSHA
Her EVAL çağrısında betiğin kaynak kodu da gönderilir. Aynı betiği sürekli göndermek ağ bant genişliğini boşa harcar ve sunucuda ek yük oluşturur. Redis bu yüzden çalıştırdığı her betiği, betiğin SHA1 özetiyle tanımlanan özel bir önbellekte tutar. SCRIPT LOAD betiği çalıştırmadan derleyip önbelleğe yükler ve özetini döndürür; sonra betik yalnızca bu özetle çağrılabilir:
redis> SCRIPT LOAD "return 'Immabe a cached script'"
"c664a3bf70bd1d45c4284ffebb65a6f2299bfc9f"
redis> EVALSHA c664a3bf70bd1d45c4284ffebb65a6f2299bfc9f 0
"Immabe a cached script"EVALSHA komutu sözdizimi bakımından EVAL ile aynıdır, yalnızca ilk argüman kaynak kod yerine SHA1 özetidir. INFO çıktısındaki used_memory_scripts_eval ve number_of_cached_scripts alanları her yeni betikle büyür; dinamik betik üretmenin neden zararlı olduğunu buradan izleyebilirsiniz. EVAL sayfasına göre Redis 7.4 ile birlikte, EVAL veya EVAL_RO ile yüklenen betikler önbellek belli bir boyuta ulaştığında en az kullanılandan başlayarak önbellekten çıkarılır ve bunların sayısı INFO içindeki evicted_scripts alanında görünür. INFO alanlarının ayrıntısı için INFO komutu yazımıza bakın.
Önbellek kalıcı değildir: NOSCRIPT hatası
Betik önbelleği veritabanının parçası sayılmaz ve diske yazılmaz. Sunucu yeniden başladığında, bir replika primary rolünü üstlendiğinde ya da biri SCRIPT FLUSH çalıştırdığında boşalır. Bu durumda EVALSHA hata döndürür. Hata sayfasındaki örnek:
> EVALSHA ffffffffffffffffffffffffffffffffffffffff 0
(error) NOSCRIPT No matching script. Please use EVAL.Kodunuz bu hatayı her an bekleyecek biçimde yazılmalıdır: NOSCRIPT aldığınızda betiği SCRIPT LOAD ile yükleyin ya da bir kez EVAL ile çalıştırın, sonra EVALSHA'yı tekrarlayın. Çoğu istemci bunu kendiliğinden yapar; hata sayfasına göre redis-py'ın register_script() yönteminin döndürdüğü betik nesnesi bu hatayı aldığında betiği yeniden yükler. Betiklerin yeniden başlatma ve failover sonrasında da sunucuda kalmasını istiyorsanız belgeler fonksiyonları önerir.
SCRIPT ailesinin diğer komutları şunlardır: SCRIPT EXISTS bir veya daha fazla SHA1 özeti alır ve her biri için önbellekte varsa 1, yoksa 0 içeren bir dizi döndürür; bir pipeline öncesinde gerekli betiklerin yüklü olup olmadığını denetlemek için kullanışlıdır. SCRIPT FLUSH önbelleği tamamen boşaltır; ASYNC veya SYNC seçeneği alır ve varsayılanı lazyfree-lazy-user-flush ayarı belirler. SCRIPT KILL uzun süren bir betiği durdurur (aşağıda), SCRIPT DEBUG ise hata ayıklayıcıyı yönetir.
En uzun çalışma süresi ve BUSY hatası
Betikler varsayılan olarak beş saniyelik bir en uzun çalışma süresine tabidir. Bu değer çok büyüktür, çünkü bir betik normalde milisaniyenin altında biter; sınır geliştirme sırasında yanlışlıkla yazılan sonsuz döngüler içindir. Ayarın adı busy-reply-threshold olup milisaniye hassasiyetle redis.conf ya da CONFIG SET ile değiştirilebilir.
Betik bu süreyi aştığında Redis onu kendiliğinden sonlandırmaz, çünkü bu atomiklik sözleşmesini bozar ve veriyi yarım yazılmış bırakabilir. Bunun yerine durumu günlüğe yazar, diğer istemcilerden yeniden komut kabul etmeye başlar ama normal komutların hepsine BUSY hatası verir:
> GET mykey
(error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.
> SCRIPT KILL
OKSCRIPT KILL yalnızca henüz hiçbir yazma yapmamış betikleri durdurabilir. Betik veriyi değiştirmişse komut UNKILLABLE hatasıyla reddedilir ve geriye iki seçenek kalır: betiğin bitmesini beklemek ya da sunucuyu SHUTDOWN NOSAVE ile durdurmak. İkincisi diske kaydedilmemiş tüm değişiklikleri kaybettirir. Kalıcı çözüm betiği bu kadar uzun süremeyecek biçimde yazmaktır: sonsuz döngüleri önleyin, büyük işleri tek çağrı yerine küçük parçalara bölün.
Sandbox: neler yapılamaz?
Betikler sandbox içinde çalışır ve yalnızca Redis'teki veriyle, kendilerine argüman olarak verilen veriyle çalışmalıdır; dosya sistemine, ağa ya da başka sistem çağrılarına erişmeye çalışmamalıdır. Sandbox global değişken ve fonksiyon tanımlamayı engeller; tüm tanımlar local olmalıdır. Örneğin my_global_variable = 'some value' satırı "Script attempted to create global variable" hatası üretir. require devre dışıdır, bu yüzden harici Lua modülleri yüklenemez.
Kullanabileceğiniz kütüphaneler belgelerde listelenir: Lua'nın string, table ve math kütüphaneleri ile struct, cjson, cmsgpack ve bit. os kütüphanesi Redis 7.4 ile eklenmiştir ve güvenlik gereği yalnızca os.clock() fonksiyonunu sunar. JSON işlemek için belgedeki cjson örneği:
redis> EVAL "return cjson.decode(ARGV[1])['foo']" 0 '{"foo":"bar"}'
"bar"Betik bayrakları ve salt okunur betikler
Redis varsayılan olarak her betiğin hem okuyup hem yazdığını varsayar. Redis 7.0 ile betiğin ilk satırına bir shebang yazarak bayrak tanımlamak mümkün hale geldi:
#!lua flags=no-writes,allow-stale
local x = redis.call('get','x')
return xBelgelerde tanımlı bayraklar no-writes (betik yalnızca okur), allow-oom (bellek yetersizken de çalışabilir), allow-stale (eski veri sunan replikada çalışabilir), no-cluster (cluster modunda hata verir) ve allow-cross-slot-keys (betik farklı slot'lardaki anahtarlara erişebilir) bayraklarıdır. Dikkat edilecek bir nokta: Redis #! satırını gördüğü anda, hiç bayrak yazılmamış olsa bile betiği bayrak tanımlamış sayar ve farklı varsayılanlar uygular. Shebang'siz betikler farklı hash slot'lardaki anahtarlara erişebilirken shebang'li betikler varsayılan bayrakları devraldığı için erişemez.
Salt okunur betik çalıştırmanın ikinci yolu EVAL_RO ve EVALSHA_RO komutlarıdır. Bu komutlarla çalışan betik veri değiştiren bir komut çağırırsa hata alır:
> SET mykey "Hello"
OK
> EVAL_RO "return redis.call('GET', KEYS[1])" 1 mykey
"Hello"
> EVAL_RO "return redis.call('DEL', KEYS[1])" 1 mykey
(error) ERR Error running script (call to b0d697da25b13e49157b2c214a4033546aba2104): @user_script:1: @user_script: 1: Write commands are not allowed from read-only scripts.Salt okunur betikler her zaman replikalarda çalıştırılabilir, her zaman SCRIPT KILL ile durdurulabilir, bellek sınırı aşıldığında OOM hatasıyla başarısız olmaz ve yazma duraklatmaları sırasında bloklanmaz. Bir ayrıntı: betik içinde PUBLISH, SPUBLISH ve PFCOUNT de yazma komutu sayılır. Belgelerin önerisi, özel bir ihtiyacınız yoksa standart komutları no-writes bayrağıyla kullanmaktır; _RO komutları ise ACL ile bir kullanıcıyı yalnızca salt okunur betiklerle sınırlamak ve okumaları replikalara yönlendirmek için avantaj sağlar.
Replikasyon ve bellek sınırı
Betikler veriyi değiştirebildiği için Redis, betiğin yaptığı yazmaları replikalara ve AOF'a da iletir. Redis 5.0'dan beri varsayılan yöntem "etki replikasyonu"dur: betiğin kendisi değil, çalışırken ürettiği yazma komutları bir MULTI/EXEC bloğu içinde gönderilir. Redis 7.0 ile betiğin kaynak kodunu olduğu gibi gönderen eski yöntem tamamen kaldırıldı. Bunun pratik sonucu, betik içinde TIME ya da SRANDMEMBER gibi deterministik olmayan komutları kullanmanın artık sorun olmamasıdır.
Bellek maxmemory sınırını aştığında, betikte ek bellek kullanan ilk yazma komutu betiği durdurur (redis.pcall kullanılmadıysa). İlk yazma bellek kullanmayan bir komutsa (örneğin DEL) atomikliği korumak için betiğin tamamı çalışır ve bellek sınırı aşılabilir. Belgeler bu senaryolar için maxmemory-policy değerini noeviction dışında bir değere ayarlamayı ve betikleri olabildiğince hızlı tutmayı önerir. Ayrıntılar için maxmemory ve eviction yazımıza bakın.
Küçük bir uygulama: koşullu stok düşürme
Şimdi öğrendiklerimizi bir araya getirelim. Bir ürünün stok adedini, ancak istenen miktar kadar stok varsa düşüren bir betik yazalım. Bu işlem "oku, karar ver, yaz" türündedir; pipeline ile yapılamaz, transaction ile de ancak WATCH ve yeniden deneme döngüsüyle yapılabilir. Betikle ise tek adımda ve atomik olarak yapılır. Anahtar adı KEYS[1] ile, miktar ARGV[1] ile gelir:
-- stok_dus.lua
local stok = tonumber(redis.call('GET', KEYS[1]) or '0')
local istenen = tonumber(ARGV[1])
if stok < istenen then
return redis.error_reply('ERR yetersiz stok')
end
return redis.call('DECRBY', KEYS[1], istenen)Anahtar yoksa GET Lua'ya false olarak döner; or '0' ifadesi bu durumda stoku sıfır kabul eder. Stok yeterliyse betik DECRBY komutunun tamsayı yanıtını, yani kalan stoğu döndürür; yetersizse kendi hata yanıtını üretir. Betiği önce yükleyip sonra özetiyle çağırabilirsiniz:
$ redis-cli SCRIPT LOAD "$(cat stok_dus.lua)"
$ redis-cli EVALSHA <dönen-sha1> 1 stok:urun:42 3Geliştirme sırasında redis-cli --eval seçeneği daha pratiktir. Bu seçenekte anahtar sayısını yazmazsınız; anahtarlarla argümanları virgülle ayırırsınız. Belgedeki örnekte location:hastings:temp anahtarı KEYS tablosuna, 23 ise ARGV tablosuna gider:
$ cat /tmp/script.lua
return redis.call('SET',KEYS[1],ARGV[1])
$ redis-cli --eval /tmp/script.lua location:hastings:temp , 23
OKHata ayıklama: LDB
Redis, LDB kod adlı uzaktan çalışan bir Lua hata ayıklayıcısı içerir; sunucu tarafı Redis'in kendisi, varsayılan istemcisi redis-cli'dır. Oturum ./redis-cli --ldb --eval /tmp/script.lua mykey somekey , arg1 arg2 biçiminde başlatılır. Varsayılan oturum çatallanmış (forked) bir oturumdur: hata ayıklama sırasında sunucu bloklanmaz ve oturum bittiğinde betiğin yaptığı değişiklikler geri alınır. --ldb-sync-mode ile başlatılan eşzamanlı modda ise değişiklikler kalıcı olur ama sunucu oturum boyunca erişilemez.
Hata ayıklayıcı adım adım çalıştırma (step), kesme noktası (break), yerel değişkenleri yazdırma (print), Redis komutu çalıştırma (redis) gibi komutlar sunar ve her komut tek harfle kısaltılabilir. Betik içinden redis.breakpoint() ile koşullu kesme noktası, redis.debug() ile konsola günlük yazılabilir; bu fonksiyonlar hata ayıklama oturumu dışında etkisizdir.
Sık yapılan hatalar

- Değerleri betik kaynağına gömüp her çağrıda farklı betik üretmek: önbellek şişer. Değerleri ARGV ile verin.
- Anahtar adlarını ARGV ile vermek ya da betik içinde üretmek: cluster'da yanlış çalışır. Tüm anahtarlar KEYS ile gelmeli.
- NOSCRIPT hatasını ele almamak: önbellek yeniden başlatma, failover ve SCRIPT FLUSH ile her an boşalabilir.
- Ondalıklı sayı döndürmek: Lua sayısı tamsayıya çevrilir; ondalıklı sonucu dize olarak döndürün.
- Global değişken tanımlamak: sandbox buna izin vermez, local kullanın.
- Uzun süren döngüler yazmak: betik çalışırken tüm sunucu bekler ve beş saniyeden sonra diğer istemciler BUSY alır.
Sık Sorulan Sorular
EVAL mi, Redis Functions mı kullanmalıyım?
EVAL betikleri uygulamanın parçası sayılır; adlandırılmaz, sürümlenmez, kalıcı değildir. Redis 7 ve sonrasında betiğin sunucuda adıyla, kalıcı ve replike edilmiş biçimde durmasını istiyorsanız Functions daha uygundur. Kısa, uygulamayla birlikte dağıtılan betikler için EVAL hâlâ pratiktir.
Betik çalışırken diğer istemciler bekler mi?
Evet. Betik çalıştığı sürece sunucu başka komut işlemez; atomiklik bu blokajdan gelir. Bu yüzden betikleri kısa tutun.
EVALSHA pipeline içinde güvenli midir?
Betik önceden SCRIPT LOAD ile yüklendiyse kullanılabilir. Yüklenmemişse NOSCRIPT hatası pipeline ortasında ele alınamaz; SCRIPT EXISTS ile kontrol edip eksikleri önceden yükleyin.
Betik içinde TIME veya rastgele komut kullanabilir miyim?
Redis 5.0'dan beri varsayılan olan, 7.0'dan beri de tek seçenek olan etki replikasyonu sayesinde evet; replikalara betik değil, ürettiği yazma komutları gider.
Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.
İlgili içerikler: Redis öğrenme rehberi · Redis Functions · MULTI, EXEC ve WATCH · Redis pipelining · Redis ile dağıtık kilit · Redis hata mesajları