Skrip Lua dieksekusi secara atomik: tidak ada perintah client lain yang menyela di tengah skrip. Ini menyelesaikan masalah race condition yang tidak bisa dipecahkan MULTI/EXEC karena MULTI tidak bisa membaca nilai lalu mengambil keputusan. EVAL mengirim skrip tiap kali, EVALSHA memakai hash agar hemat bandwidth. Redis 7 memperkenalkan Functions yang tersimpan permanen dan ikut direplikasi.
1. Kenapa MULTI/EXEC Saja Tidak Cukup
Transaksi MULTI/EXEC menjalankan sekumpulan perintah tanpa disela, tetapi tidak bisa membaca nilai lalu mengambil keputusan berdasarkan nilai itu โ karena semua perintah baru dieksekusi saat EXEC.
Contoh kasus yang mustahil dengan MULTI: "kurangi stok hanya bila stok masih lebih besar dari 0".
GET stok:produk-1 # aplikasi membaca: 1 # <-- di detik ini, client lain juga membaca 1 dan ikut mengurangi DECR stok:produk-1 # hasilnya bisa jadi -1. Stok minus!
2. Solusinya: Skrip Lua yang Atomik
EVAL "
local stok = tonumber(redis.call('GET', KEYS[1]))
if stok == nil or stok <= 0 then
return -1
end
redis.call('DECR', KEYS[1])
return stok - 1
" 1 stok:produk-1
# Angka 1 setelah skrip = berapa banyak KEYS yang dikirim.
# KEYS[1] = 'stok:produk-1'. Sisanya masuk ke ARGV.
GET dan DECR di dalam skrip.
3. KEYS vs ARGV โ Jangan Sampai Tertukar
| KEYS | ARGV | |
|---|---|---|
| Isi | Nama key yang disentuh skrip | Nilai/parameter biasa |
| Wajib? | Ya, untuk semua key | Opsional |
| Kenapa penting | Redis Cluster memakainya untuk menentukan node tujuan | Tidak memengaruhi routing |
Kalau Anda menyusun nama key di dalam skrip (misal redis.call('GET', 'stok:' .. ARGV[1])) alih-alih melewatkannya lewat KEYS, skrip akan berjalan mulus di Redis satu node tetapi rusak di Cluster karena Redis tidak tahu key itu ada di node mana. Selalu lewatkan setiap key lewat KEYS.
4. EVALSHA: Hemat Bandwidth
# Daftarkan skrip sekali, dapatkan SHA1-nya
SCRIPT LOAD "return redis.call('INCR', KEYS[1])"
# => "e0e1f9fabfc9d4800c877a703b823ac0578ff831"
# Panggil dengan hash saja -- jauh lebih ringan daripada
# mengirim seluruh teks skrip setiap kali
EVALSHA e0e1f9fabfc9d4800c877a703b823ac0578ff831 1 counter:halaman
# Cek apakah skrip sudah ada di cache server
SCRIPT EXISTS e0e1f9fabfc9d4800c877a703b823ac0578ff831
Cache SHA bersifat sementara. Setelah Redis restart atau SCRIPT FLUSH, EVALSHA akan menjawab NOSCRIPT. Library klien yang baik menangani ini otomatis dengan jatuh kembali ke EVAL lalu menyimpan ulang hash-nya โ pastikan library Anda melakukannya, atau tangani sendiri.
5. Contoh Nyata: Rate Limiter Sliding Window
-- KEYS[1] = kunci rate limit, misal 'rate:user:42'
-- ARGV[1] = batas maksimum request
-- ARGV[2] = panjang jendela dalam detik
-- ARGV[3] = timestamp sekarang (ms) dari aplikasi
-- ARGV[4] = ID unik request ini
local batas = tonumber(ARGV[1])
local jendela = tonumber(ARGV[2])
local sekarang = tonumber(ARGV[3])
local id = ARGV[4]
-- Buang jejak request yang sudah lewat dari jendela
redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, sekarang - (jendela * 1000))
local jumlah = redis.call('ZCARD', KEYS[1])
if jumlah >= batas then
return 0 -- ditolak
end
redis.call('ZADD', KEYS[1], sekarang, id)
redis.call('EXPIRE', KEYS[1], jendela)
return 1 -- diizinkan
Jangan memakai redis.call('TIME') di dalam skrip yang mengubah data. Nilai waktu bersifat non-deterministik dan dulu membuat replikasi tidak konsisten. Kirimkan timestamp dari aplikasi lewat ARGV agar hasilnya deterministik.
6. Redis Functions (Redis 7+)
Kelemahan skrip Lua: ia hanya "menumpang" di cache dan bisa lenyap. Redis Functions menyimpan pustaka fungsi secara permanen โ ikut tersimpan di RDB/AOF dan ikut direplikasi ke replika.
#!lua name=tokolib
local function kurangi_stok(keys, args)
local stok = tonumber(redis.call('GET', keys[1]))
if stok == nil or stok <= 0 then
return -1
end
redis.call('DECR', keys[1])
return stok - 1
end
redis.register_function('kurangi_stok', kurangi_stok)
# Muat pustaka ke server redis-cli -x FUNCTION LOAD < perpustakaan.lua # Panggil fungsinya (FCALL untuk yang menulis, FCALL_RO untuk read-only) FCALL kurangi_stok 1 stok:produk-1 # Kelola pustaka FUNCTION LIST FUNCTION STATS FUNCTION DELETE tokolib
7. Aturan Main yang Wajib Dipatuhi
- Buat skrip sesingkat mungkin. Selama skrip berjalan, seluruh Redis terblokir. Skrip yang berjalan 100 ms berarti 100 ms downtime untuk semua client.
- Jangan ada loop tak berujung. Redis akan menolak perintah lain sampai
busy-reply-thresholdtercapai, lalu hanya SCRIPT KILL dan SHUTDOWN NOSAVE yang diterima. - Jaga determinisme. Hindari waktu acak, angka acak tanpa seed, dan iterasi atas struktur yang urutannya tak terjamin.
- Jangan pakai untuk pekerjaan berat. Lua bukan tempat memproses ribuan key. Kalau butuh itu, pindahkan logikanya ke aplikasi dan pakai pipeline.