Retour au cours

data / redis

Scripting Lua dans Redis

Leçon 91 exercice

Explication

Ce que vous allez apprendre

  • Exécuter un script Lua atomiquement côté serveur avec EVAL
  • Comprendre pourquoi un script Lua permet une logique conditionnelle impossible avec MULTI/EXEC seul
  • Charger un script une fois et le réexécuter efficacement via son SHA avec EVALSHA
  • Écrire un rate limiter simple entièrement en Lua, en une seule opération atomique
  • Comprendre le risque d'un script Lua trop long sur un serveur mono-thread

Dans quel contexte ?

Une API doit limiter chaque utilisateur à 100 requêtes par minute. Cette logique nécessite de lire un compteur, vérifier s'il dépasse la limite, l'incrémenter, et poser un TTL uniquement au tout premier appel de la fenêtre — plusieurs étapes conditionnelles qu'aucune combinaison de MULTI/EXEC ne peut exprimer proprement, puisque MULTI/EXEC ne permet aucun if entre les commandes mises en file.

D'abord, comprendre ce qu'apporte réellement EVAL

EVAL envoie un script Lua au serveur Redis, qui l'exécute intégralement de façon atomique : aucune commande d'un autre client ne peut s'intercaler pendant son exécution, même si le script contient plusieurs appels redis.call() en son sein. C'est une garantie plus forte et plus flexible que MULTI/EXEC, puisque le script peut contenir de la vraie logique conditionnelle.

Une fois cette possibilité comprise, le vrai avantage devient clair : la logique conditionnelle

Un script Lua peut faire un if/else en fonction du résultat d'une commande Redis précédente, dans le même aller-retour réseau et la même garantie d'atomicité. C'est exactement le problème du rate limiter : incrémenter, vérifier si on dépasse la limite, poser le TTL uniquement au premier appel — tout ça en une seule opération indivisible, impossible à décomposer en commandes MULTI/EXEC séparées sans introduire une fenêtre de concurrence.

BesoinMULTI/EXECScript Lua (EVAL)
Exécuter plusieurs commandes sans interférenceOuiOui
Logique conditionnelle (if/else) entre les commandesNonOui
Décider d'une commande suivante selon le résultat d'une précédenteNonOui

Ensuite, une optimisation réseau simple mais utile en production

Envoyer le texte complet du script à chaque appel gaspille de la bande passante si le même script est exécuté des milliers de fois. SCRIPT LOAD charge le script une fois et renvoie un identifiant SHA1 ; EVALSHA réexécute ensuite ce même script en n'envoyant que ce hash court, beaucoup plus léger sur le réseau.

Prérequis

Cette leçon suppose que tu es à l'aise avec MULTI/EXEC/WATCH (leçon précédente) : le scripting Lua résout précisément la limite de logique conditionnelle que ces commandes ne peuvent pas exprimer.

Il reste un risque important à garder à l'esprit avant d'en abuser

Redis exécute les commandes de façon mono-thread : pendant qu'un script Lua s'exécute, absolument aucune autre commande d'aucun autre client ne peut être traitée, même une simple lecture anodine. Un script trop long ou qui fait une boucle sur un très grand nombre d'éléments bloque littéralement tout le serveur pendant sa durée d'exécution.

Piège fréquent

Écrire un script Lua qui boucle sur des milliers d'éléments (par exemple pour parcourir un très gros hash ou une très grosse liste) peut geler le serveur Redis entier pendant plusieurs centaines de millisecondes, un temps qui semble court en isolation mais qui peut suffire à faire timeout des centaines de requêtes applicatives en attente sur un service à fort trafic.

Bonne pratique

Utilise register_script côté client (comme dans redis-py) plutôt que de manipuler manuellement EVAL/EVALSHA et de gérer toi-même le cache de SHA : la bibliothèque cliente s'occupe automatiquement de charger le script si nécessaire et de basculer sur EVALSHA ensuite.

Maintenant que tu sais exécuter de la logique atomique complexe, la prochaine leçon change de sujet : la persistance des données sur disque, avec RDB et AOF, pour survivre à un redémarrage du serveur.

Commandes & code

Scripting Lua dans Redis

bash
# EVAL exécute un script Lua atomiquement côté serveur (aucune commande d'un autre client ne s'intercale)
EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 maCle maValeur
#     ^script Lua                                  ^nb de clés  ^clés puis args

# Exemple : incrément avec plafond (impossible atomiquement avec INCR seul)
EVAL "
local val = tonumber(redis.call('GET', KEYS[1]) or '0')
local max = tonumber(ARGV[1])
if val < max then
    return redis.call('INCR', KEYS[1])
else
    return val
end
" 1 compteur:api 100

# Charger un script une fois, l'exécuter via son SHA (évite de renvoyer le code à chaque appel)
SCRIPT LOAD "return redis.call('GET', KEYS[1])"
# -> renvoie un SHA1, ex: "a1b2c3..."
EVALSHA a1b2c3... 1 maCle

# Vérifier si des scripts sont déjà en cache serveur
SCRIPT EXISTS a1b2c3...
SCRIPT FLUSH                        # vide le cache de scripts (attention en prod)
python
import redis

r = redis.Redis(decode_responses=True)

# Script Lua : rate limiter "sliding window" simplifié, tout en une opération atomique
RATE_LIMIT_SCRIPT = '''
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])

local current = redis.call('INCR', key)
if current == 1 then
    redis.call('EXPIRE', key, window)   -- ne définit le TTL qu'au premier appel de la fenêtre
end

if current > limit then
    return 0    -- refusé
else
    return 1    -- autorisé
end
'''

rate_limit = r.register_script(RATE_LIMIT_SCRIPT)

def autoriser_requete(user_id: str) -> bool:
    resultat = rate_limit(keys=[f"rl:{user_id}"], args=[100, 60])  # 100 req / 60s
    return bool(resultat)
bash
# Pourquoi Lua plutôt que MULTI/EXEC pour ces cas ?
# - Logique CONDITIONNELLE (if/else) impossible à exprimer avec MULTI/EXEC seul
# - Une seule aller-retour réseau, exécution garantie atomique sur le serveur
# - Attention : un script Lua LONG bloque le serveur entier pendant son exécution (single-threaded)

Résumé

  • EVAL/EVALSHA exécutent du Lua atomiquement côté serveur, avec accès aux commandes Redis via redis.call.
  • Le scripting permet une logique conditionnelle atomique impossible avec MULTI/EXEC seul.
  • Redis étant mono-thread pour l'exécution des commandes, un script trop long bloque tous les autres clients.

Exercices pratiques

1 disponible
1

Mission : implémenter un rate limiter atomique sans double comptage

Objectif : Écrire et exécuter un rate limiter Lua atomique côté serveur, et éviter le piège d'un script trop long sur un Redis mono-thread.

Contexte

Une première version d'un rate limiter d'API fait un GET puis un INCR séparés côté application, et laisse parfois passer plus de requêtes que la limite autorisée sous forte concurrence. Tu dois la remplacer par un script Lua atomique, l'exécuter efficacement, et évaluer une proposition d'extension risquée sur un serveur mono-thread.

Résoudre l’exercice →