Kodeloka
  1. Kodeloka
  2. Kelas Redis
  3. Materi

Advanced ยท Modul 14 dari 21

Lua Scripting & Redis Functions

Menjalankan logika multi-perintah secara atomik di dalam server memakai EVAL, EVALSHA, dan Redis Functions (FUNCTION LOAD) di Redis 7+.

๐Ÿ“Œ Ringkasan Eksekutif Modul:
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".

Cara yang SALAH โ€” ada celah balapan
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

redis-cli
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.
๐Ÿ’ก Kenapa ini aman: Redis single-threaded dan menjalankan seluruh skrip sebagai satu unit. Tidak ada perintah client lain yang bisa menyelip antara GET dan DECR di dalam skrip.

3. KEYS vs ARGV โ€” Jangan Sampai Tertukar

KEYSARGV
IsiNama key yang disentuh skripNilai/parameter biasa
Wajib?Ya, untuk semua keyOpsional
Kenapa pentingRedis Cluster memakainya untuk menentukan node tujuanTidak memengaruhi routing
๐Ÿšซ Kesalahan fatal di Redis Cluster

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 skrip hilang saat restart

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

rate_limit.lua
-- 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
๐Ÿ“Œ Ambil waktu dari aplikasi, bukan dari Lua

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.

perpustakaan.lua
#!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-threshold tercapai, 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.