← Volver al listado de tecnologías

Scripting Lua en Valkey

Por: Artiko
valkeyluascriptingatomicidad

Scripting Lua

¿Por qué Lua?

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

  1. Usar KEYS y ARGV: No hardcodear claves en el script
  2. Scripts cortos: Evitar bloquear el servidor
  3. Idempotencia: Diseñar scripts que puedan re-ejecutarse
  4. 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

  1. Implementa un script de “compare-and-swap”
  2. Crea un rate limiter con sliding window
  3. Implementa un lock distribuido con renovación

Resumen