← Volver al listado de tecnologías
Scripting Lua en Valkey
Scripting Lua
¿Por qué Lua?
- Atomicidad: El script completo se ejecuta sin interrupciones
- Rendimiento: Reduce round-trips entre cliente y servidor
- Flexibilidad: Lógica condicional compleja en el servidor
EVAL básico
# Sintaxis: EVAL script numkeys [key ...] [arg ...]
EVAL "return 'Hola Mundo'" 0
# "Hola Mundo"
# Con argumentos
EVAL "return ARGV[1]" 0 "mi_argumento"
# "mi_argumento"
# Con claves
EVAL "return redis.call('GET', KEYS[1])" 1 mi_clave
Llamar comandos de Valkey
-- redis.call() - propaga errores
redis.call('SET', KEYS[1], ARGV[1])
-- redis.pcall() - captura errores
local resultado = redis.pcall('GET', 'clave_inexistente')
Ejemplo: Incremento condicional
EVAL "
local valor = redis.call('GET', KEYS[1])
if valor == false then
return redis.call('SET', KEYS[1], ARGV[1])
else
return redis.call('INCR', KEYS[1])
end
" 1 contador 10
Scripts útiles
Rate Limiter
-- rate_limiter.lua
-- KEYS[1] = clave del rate limit
-- ARGV[1] = límite máximo
-- ARGV[2] = ventana en segundos
local current = redis.call('GET', KEYS[1])
if current == false then
redis.call('SET', KEYS[1], 1, 'EX', ARGV[2])
return 1
end
if tonumber(current) >= tonumber(ARGV[1]) then
return 0 -- Límite alcanzado
end
return redis.call('INCR', KEYS[1])
# Permitir 100 requests por minuto
EVAL "$(cat rate_limiter.lua)" 1 rate:user:42 100 60
Lock distribuido
-- acquire_lock.lua
-- KEYS[1] = nombre del lock
-- ARGV[1] = identificador del owner
-- ARGV[2] = TTL en segundos
local lock = redis.call('SET', KEYS[1], ARGV[1], 'NX', 'EX', ARGV[2])
if lock then
return 1
end
return 0
-- release_lock.lua
-- Solo libera si eres el owner
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
end
return 0
Compare-and-swap
-- cas.lua
-- KEYS[1] = clave
-- ARGV[1] = valor esperado
-- ARGV[2] = nuevo valor
local actual = redis.call('GET', KEYS[1])
if actual == ARGV[1] then
redis.call('SET', KEYS[1], ARGV[2])
return 1
end
return 0
Dequeue condicional
-- dequeue_if.lua
-- Solo hace pop si el elemento cumple condición
local item = redis.call('LINDEX', KEYS[1], 0)
if item == false then
return nil
end
local data = cjson.decode(item)
if data.priority >= tonumber(ARGV[1]) then
return redis.call('LPOP', KEYS[1])
end
return nil
SCRIPT LOAD y EVALSHA
Para scripts que se usan frecuentemente:
# Cargar script (retorna SHA1)
SCRIPT LOAD "return redis.call('GET', KEYS[1])"
# "a42059b356c875f0717db19a51f6aaa9161e77a2"
# Ejecutar por SHA
EVALSHA a42059b356c875f0717db19a51f6aaa9161e77a2 1 mi_clave
# Verificar si existe
SCRIPT EXISTS a42059b356c875f0717db19a51f6aaa9161e77a2
# 1) (integer) 1
# Limpiar cache de scripts
SCRIPT FLUSH
Implementación en Python
import valkey
r = valkey.Valkey(host='localhost', port=6379, decode_responses=True)
# Script simple
script = """
local current = redis.call('GET', KEYS[1])
if current == false then
redis.call('SET', KEYS[1], ARGV[1])
return ARGV[1]
end
return current
"""
resultado = r.eval(script, 1, 'mi_clave', 'valor_default')
# Registrar script para reusar
get_or_set = r.register_script("""
local val = redis.call('GET', KEYS[1])
if val == false then
redis.call('SET', KEYS[1], ARGV[1])
return ARGV[1]
end
return val
""")
# Usar el script registrado
resultado = get_or_set(keys=['clave'], args=['default'])
Rate limiter completo
rate_limit_script = r.register_script("""
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local current = redis.call('GET', key)
if current == false then
redis.call('SET', key, 1, 'EX', window)
return {1, limit - 1}
end
if tonumber(current) >= limit then
local ttl = redis.call('TTL', key)
return {0, ttl}
end
local new_count = redis.call('INCR', key)
return {1, limit - new_count}
""")
def check_rate_limit(user_id, limit=100, window=60):
key = f"rate:{user_id}"
allowed, remaining = rate_limit_script(keys=[key], args=[limit, window])
return {
'allowed': bool(allowed),
'remaining': remaining
}
Implementación en Node.js
import { createClient } from 'valkey';
const client = createClient();
await client.connect();
// Definir script
const incrIfLess = `
local current = redis.call('GET', KEYS[1])
if current == false then
current = 0
end
if tonumber(current) < tonumber(ARGV[1]) then
return redis.call('INCR', KEYS[1])
end
return current
`;
// Cargar script
const sha = await client.scriptLoad(incrIfLess);
// Ejecutar
const result = await client.evalSha(sha, {
keys: ['contador'],
arguments: ['100']
});
Debug de scripts
# Modo debug (paso a paso)
valkey-cli --ldb --eval script.lua key1 , arg1
# Dentro del debugger
step # Siguiente línea
continue # Continuar
print # Imprimir variables
abort # Cancelar
Buenas prácticas
- Usar KEYS y ARGV: No hardcodear claves en el script
- Scripts cortos: Evitar bloquear el servidor
- Idempotencia: Diseñar scripts que puedan re-ejecutarse
- Precargar scripts: Usar SCRIPT LOAD en el arranque
# Precargar scripts al iniciar
class ValkeyScripts:
def __init__(self, redis_client):
self.r = redis_client
self.rate_limit = self.r.register_script(RATE_LIMIT_LUA)
self.lock = self.r.register_script(LOCK_LUA)
self.unlock = self.r.register_script(UNLOCK_LUA)
Ejercicios
- Implementa un script de “compare-and-swap”
- Crea un rate limiter con sliding window
- Implementa un lock distribuido con renovación
Resumen
EVALejecuta scripts Lua atómicamenteSCRIPT LOAD+EVALSHApara scripts frecuentesredis.call()para comandos de Valkey desde Lua- Los scripts reducen latencia y garantizan atomicidad