Redis Lua-Scripting fuer atomare Operationen
AI generated
SET
TTL
Redis · Lua-Scripting · EVAL · Atomaritaet
Redis Lua-Scripting fuer atomare Operationen
von EVAL bis zur produktionsreifen Geschaeftslogik

Redis Lua-Scripting fuehrt komplette Skripte als einen einzigen atomaren Befehl aus, ohne dass ein anderer Client dazwischenfunken kann. Wer Increment-mit-Limit, bedingtes Loeschen oder andere Read-then-Write-Muster ohne echte Server-Logik umsetzt, riskiert Race-Conditions, die Lua-Skripte strukturell ausschliessen.

14 Min. Lesezeit EVAL · EVALSHA · SCRIPT LOAD · KEYS/ARGV Redis 6.x · 7.x · Lua 5.1

1. Was Redis Lua-Scripting wirklich leistet

Mit Redis Lua-Scripting wird ein komplettes Skript, geschrieben in Lua 5.1, direkt auf dem Redis-Server ausgefuehrt, statt mehrere einzelne Befehle vom Client aus zu senden. Der zentrale Vorteil: Das gesamte Skript laeuft als eine einzige, unteilbare Operation, waehrend derer kein anderer Client irgendeinen Befehl gegen den Datensatz ausfuehren kann. Das unterscheidet Lua-Scripting fundamental von einer Folge einzelner Client-Befehle, bei der zwischen jedem Befehl eine Race-Condition entstehen kann.

Der Anwendungsfall fuer Redis Lua-Scripting entsteht ueberall dort, wo eine Operation aus einem Lesen und einem darauf bedingten Schreiben besteht. Klassisches Beispiel: ein Zaehler soll erhoeht werden, aber nur, wenn er unter einem bestimmten Limit liegt. Ohne Lua-Scripting muesste der Client zuerst GET aufrufen, das Ergebnis pruefen und dann bedingt INCR senden, mit dem Risiko, dass zwischen GET und INCR ein anderer Client denselben Wert veraendert. Ein Lua-Skript fuehrt Lesen und Schreiben als einen einzigen, unterbrechungsfreien Schritt auf dem Server aus und schliesst diese Race-Condition strukturell aus.

Wichtig ist, dass Redis Lua-Scripting keine allgemeine Programmierumgebung ist. Skripte haben Zugriff auf eine eingeschraenkte Lua-Standardbibliothek, koennen keine Dateisystem- oder Netzwerkzugriffe durchfuehren und interagieren mit Redis ausschliesslich ueber die Funktion redis.call oder redis.pcall. Diese Beschraenkung ist bewusst gewaehlt, um Determinismus und Sicherheit zu garantieren, denn ein Skript, das auf externe Ressourcen zugreifen koennte, wuerde die Atomaritaetsgarantie sofort untergraben.

2. EVAL im Detail: Skripte direkt ausfuehren

Der Befehl EVAL nimmt den Lua-Quelltext als String entgegen, gefolgt von der Anzahl der Keys und den eigentlichen Keys und Argumenten. Innerhalb des Skripts stehen diese als globale Tabellen KEYS und ARGV zur Verfuegung. Der Rueckgabewert des Skripts wird automatisch in einen Redis-Antworttyp konvertiert: Lua-Strings werden zu Redis-Bulk-Strings, Lua-Zahlen zu Redis-Integers, Lua-Tabellen zu Redis-Arrays.

Ein einfaches EVAL-Beispiel zeigt den grundlegenden Mechanismus: Ein Skript liest einen Wert, verarbeitet ihn mit Lua-Logik und schreibt das Ergebnis zurueck, alles innerhalb eines einzigen Redis-Befehls, der vom Client aus gesehen atomar erscheint. Diese Eigenschaft macht Redis Lua-Scripting zum Werkzeug der Wahl fuer jede Geschaeftslogik, die klassische Race-Conditions bei nebenlaeufigen Zugriffen ausschliessen muss.


-- Simple script: read a value, double it, write it back, return old and new
-- Called via: redis-cli EVAL "$(cat script.lua)" 1 mykey
local old_value = tonumber(redis.call("GET", KEYS[1])) or 0
local new_value = old_value * 2
redis.call("SET", KEYS[1], new_value)
return {old_value, new_value}

redis-cli> SET counter 21
OK

redis-cli> EVAL "local old = tonumber(redis.call('GET', KEYS[1])) or 0; \
  local new = old * 2; redis.call('SET', KEYS[1], new); return {old, new}" \
  1 counter
1) (integer) 21
2) (integer) 42

3. KEYS und ARGV: Parameter sauber uebergeben

Ein haeufiger Anfaengerfehler bei Redis Lua-Scripting ist es, Key-Namen direkt im Skript-Text zu verketten, statt sie ueber KEYS zu uebergeben. Das funktioniert zwar technisch, bricht aber die Redis-Cluster-Kompatibilitaet, weil Redis Cluster anhand der deklarierten Keys entscheidet, auf welchem Shard ein Skript ausgefuehrt werden darf. Ein Skript mit hartkodierten Key-Namen kann Redis nicht korrekt zu einem Shard routen und schlaegt im Cluster-Modus fehl oder liefert falsche Ergebnisse.

Die klare Regel lautet daher: Alle Keys, auf die das Skript zugreift, gehoeren in die KEYS-Tabelle, alle sonstigen Werte wie Limits, Zeitstempel oder Zeichenketten in die ARGV-Tabelle. Diese Trennung erlaubt es Redis, statisch zu erkennen, welche Keys betroffen sind, was sowohl fuer Cluster-Routing als auch fuer die interne Nebenlaeufigkeitskontrolle wichtig ist. In der Praxis bedeutet das: Auch bei einem Single-Node-Setup sollte man von Anfang an KEYS und ARGV konsequent nutzen, um spaetere Migrationsprobleme zu vermeiden.


-- Correct: key comes from KEYS, values come from ARGV
-- redis-cli EVAL "..." 1 rate:limit:user:42 100 60
local current = redis.call("INCR", KEYS[1])
if current == 1 then
  redis.call("EXPIRE", KEYS[1], ARGV[2])
end
if current > tonumber(ARGV[1]) then
  return 0
end
return 1

4. EVALSHA und SCRIPT LOAD fuer Performance

Bei jedem EVAL-Aufruf sendet der Client den vollstaendigen Lua-Quelltext ueber das Netzwerk und Redis kompiliert ihn neu, was bei haeufig ausgefuehrten Skripten unnoetigen Overhead erzeugt. SCRIPT LOAD kompiliert ein Skript einmalig, cacht es serverseitig und gibt einen SHA1-Hash zurueck. Mit EVALSHA kann dieses Skript anschliessend nur ueber den Hash aufgerufen werden, ohne den Quelltext erneut zu uebertragen, was Netzwerklast und Parse-Zeit spart.

Der Redis-Skript-Cache ist server-lokal und wird bei einem Neustart oder FLUSHALL geleert, weshalb produktive Clients auf den Fehler NOSCRIPT vorbereitet sein muessen: Tritt dieser Fehler bei EVALSHA auf, faellt der Client auf ein regulaeres EVAL mit dem vollstaendigen Quelltext zurueck und cacht das Skript damit implizit erneut. Die meisten Redis-Clientbibliotheken wie Jedis, redis-py oder Predis implementieren dieses Fallback-Verhalten bereits automatisch.

# Load the script once, get back its SHA1 hash
redis-cli> SCRIPT LOAD "local c = redis.call('INCR', KEYS[1]); \
  if tonumber(c) > tonumber(ARGV[1]) then redis.call('DECR', KEYS[1]); \
  return 0 else return 1 end"
"a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0"

# Reuse the cached script by hash, no source code transferred
redis-cli> EVALSHA a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0 1 counter:daily 1000
(integer) 1

# Check whether a script hash is still cached on the server
redis-cli> SCRIPT EXISTS a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
1) (integer) 1

5. Warum Lua-Skripte wirklich atomar sind

Die Atomaritaetsgarantie von Redis Lua-Scripting basiert auf demselben Single-Threaded-Ausfuehrungsmodell, das auch normale Redis-Befehle atomar macht. Waehrend ein Skript laeuft, blockiert der Redis-Hauptprozess alle anderen Client-Anfragen vollstaendig, es gibt keine Moeglichkeit fuer parallele Ausfuehrung innerhalb desselben Redis-Prozesses. Das unterscheidet Lua-Scripting von MULTI/EXEC-Transaktionen dahingehend, dass ein Skript bei einem Laufzeitfehler seine Ausfuehrung sofort abbricht, waehrend MULTI/EXEC die restlichen Befehle trotzdem ausfuehrt.

Diese Atomaritaet hat einen Preis: Ein lang laufendes Skript blockiert den gesamten Redis-Server fuer die Dauer seiner Ausfuehrung, inklusive aller anderen Clients. Deshalb gilt die klare Empfehlung, Lua-Skripte kurz zu halten und keine teuren Operationen wie das Iterieren ueber Millionen von Keys innerhalb eines einzigen Skripts durchzufuehren. Der Parameter lua-time-limit in der Redis-Konfiguration definiert eine Warnschwelle, ab der Redis ein laufendes Skript als potenziell problematisch markiert, ohne es jedoch automatisch zu beenden, da ein Abbruch mitten in Schreiboperationen die Datenintegritaet gefaehrden wuerde.

6. Praxisbeispiel: atomares Increment mit Limit

Ein typischer Anwendungsfall fuer Redis Lua-Scripting ist ein Rate-Limiter, der einen Zaehler pro Zeitfenster erhoeht, aber ab einem definierten Limit weitere Erhoehungen ablehnt. Ohne Lua-Skript muesste die Anwendung GET, eine Vergleichsoperation und INCR als separate Befehle ausfuehren, mit dem Risiko, dass zwei parallele Anfragen beide den Vergleich erfolgreich passieren, bevor eine von ihnen den Zaehler erhoeht, wodurch das Limit ueberschritten wird.

Das folgende Skript loest dieses Problem, indem es den Zaehler zuerst erhoeht und danach prueft, ob das Limit ueberschritten wurde. Wird das Limit ueberschritten, wird der Zaehler wieder dekrementiert und 0 zurueckgegeben, andernfalls wird die aktuelle Anzahl der erlaubten Aufrufe zurueckgegeben. Der EXPIRE-Aufruf beim ersten Increment sorgt dafuer, dass der Zaehler am Ende des Zeitfensters automatisch verschwindet, ohne dass ein separater Cleanup-Job noetig ist.


-- Atomic rate limiter: increment counter with limit and TTL window
-- redis-cli EVAL "$(cat rate_limit.lua)" 1 rate:api:user:42 100 60
local current = redis.call("INCR", KEYS[1])

-- Set expiry only on the very first increment of the window
if current == 1 then
  redis.call("EXPIRE", KEYS[1], ARGV[2])
end

-- Reject and roll back the increment if the limit is exceeded
if current > tonumber(ARGV[1]) then
  redis.call("DECR", KEYS[1])
  return 0
end

return current

7. Praxisbeispiel: bedingtes Loeschen (Compare-and-Delete)

Ein zweites klassisches Muster ist das bedingte Loeschen, auch als Compare-and-Delete bekannt: Ein Key soll nur geloescht werden, wenn sein aktueller Wert einem erwarteten Wert entspricht. Dieses Muster ist zentral fuer verteilte Locks, bei denen nur der Prozess, der den Lock urspruenglich gesetzt hat, ihn auch wieder freigeben darf, erkennbar an einem eindeutigen Token als Lock-Wert. Ein einfaches DEL ohne Wertepruefung wuerde riskieren, dass ein Prozess versehentlich den Lock eines anderen Prozesses aufhebt.

Das folgende Skript prueft den aktuellen Wert des Keys gegen den uebergebenen erwarteten Wert und loescht nur bei Uebereinstimmung. Da GET und DEL innerhalb desselben atomaren Skripts laufen, kann zwischen der Pruefung und dem Loeschen kein anderer Client den Wert veraendern, was bei getrennten GET- und DEL-Befehlen theoretisch moeglich waere.


-- Compare-and-delete: only remove the key if its value matches the expected token
-- redis-cli EVAL "$(cat cas_delete.lua)" 1 lock:order:9931 "worker-a1b2c3"
if redis.call("GET", KEYS[1]) == ARGV[1] then
  return redis.call("DEL", KEYS[1])
else
  return 0
end

redis-cli> SET lock:order:9931 "worker-a1b2c3" NX EX 30
OK

# Correct owner releases the lock, deletion succeeds
redis-cli> EVAL "if redis.call('GET', KEYS[1]) == ARGV[1] then \
  return redis.call('DEL', KEYS[1]) else return 0 end" \
  1 lock:order:9931 worker-a1b2c3
(integer) 1

# Wrong owner tries to release, deletion is safely rejected
redis-cli> EVAL "if redis.call('GET', KEYS[1]) == ARGV[1] then \
  return redis.call('DEL', KEYS[1]) else return 0 end" \
  1 lock:order:9931 worker-zzzzzz
(integer) 0

8. Grenzen und Fallstricke von Lua-Skripten

Redis Lua-Scripting kennt einige harte Grenzen, die vor dem produktiven Einsatz bekannt sein sollten. Skripte duerfen keine nicht-deterministischen Lua-Funktionen wie os.time oder math.random ohne besondere Vorsichtsmassnahmen verwenden, weil Replikation und AOF-Persistenz voraussetzen, dass ein Skript bei jeder Ausfuehrung dasselbe Ergebnis liefert. Seit Redis 5 nutzt effect replication, bei der die tatsaechlich ausgefuehrten Schreibbefehle repliziert werden statt des Skript-Textes selbst, was diese Einschraenkung deutlich entschaerft hat, aber weiterhin gilt: Determinismus innerhalb des Skripts bleibt best practice.

Ein weiterer Fallstrick betrifft Fehlerbehandlung: redis.call wirft bei einem Redis-Fehler eine Lua-Exception, die das gesamte Skript sofort abbricht, waehrend redis.pcall den Fehler als Lua-Tabelle zurueckgibt, die im Skript selbst behandelt werden kann. Wer robuste Skripte schreiben will, die auf bestimmte Fehlertypen reagieren sollen, statt bei jedem Fehler abzubrechen, muss bewusst redis.pcall statt redis.call verwenden und den Rueckgabewert explizit auf ein err-Feld pruefen.

9. Lua-Skripte im Vergleich zu MULTI/EXEC und Functions

Redis bietet mehrere Mechanismen fuer serverseitige Logik, die sich in Atomaritaet, Flexibilitaet und Performance unterscheiden. Die folgende Tabelle vergleicht die wichtigsten Optionen.

Kriterium MULTI/EXEC Lua-Skript (EVAL) Redis Functions (7.0+)
Bedingte Logik zwischen Befehlen Nicht moeglich Voll unterstuetzt Voll unterstuetzt
Abbruch bei Laufzeitfehler Kein Abbruch der Restbefehle Skript stoppt sofort Funktion stoppt sofort
Persistenz des Codes Nicht zutreffend Cache kann geleert werden (NOSCRIPT) Dauerhaft mit RDB/AOF gespeichert
Verwaltung und Versionierung Nicht zutreffend Nur ueber SHA1-Hash im Cache Benannte Libraries mit FUNCTION LOAD
Netzwerk-Roundtrips Mehrere Ein einziger Ein einziger

Fuer einfache Read-then-Write-Operationen mit klarer, unbedingter Logik ist ein Lua-Skript mit EVAL oft die pragmatischste Loesung. Wer Code dauerhaft versionieren und ueber mehrere Deployments hinweg konsistent verfuegbar halten will, sollte auf Redis Functions ab Version 7.0 setzen, da diese Skripte persistieren, statt nur im fluechtigen Skript-Cache zu liegen.

10. Zusammenfassung

Redis Lua-Scripting fuehrt komplette Skripte als eine einzige, unteilbare Server-Operation aus und schliesst dadurch Race-Conditions bei Read-then-Write-Mustern strukturell aus. EVAL fuehrt Skripte direkt aus, EVALSHA nutzt den serverseitigen Skript-Cache fuer bessere Performance, und die klare Trennung zwischen KEYS und ARGV ist Voraussetzung fuer Cluster-Kompatibilitaet. Praktische Muster wie atomares Increment-mit-Limit und Compare-and-Delete zeigen, wie sich klassische Race-Conditions mit wenigen Zeilen Lua-Code zuverlaessig vermeiden lassen.

Wichtig fuer die Praxis: Skripte sollten kurz und deterministisch bleiben, da sie den gesamten Redis-Server fuer ihre Laufzeit blockieren. Wer Skripte dauerhaft versionieren will, sollte den Wechsel zu Redis Functions in Betracht ziehen. Fuer einfache, atomare Geschaeftslogik bleibt EVAL jedoch das direkteste und am weitesten verbreitete Werkzeug in produktiven Redis-Umgebungen.

Redis Lua-Scripting, das Wichtigste auf einen Blick

Echte Atomaritaet

Ein Skript laeuft als eine unteilbare Operation, kein anderer Client kann dazwischenfunken, Fehler stoppen die Ausfuehrung sofort.

EVAL vs. EVALSHA

EVAL sendet den vollen Quelltext, EVALSHA nutzt den Skript-Cache per SHA1-Hash und spart Netzwerklast bei haeufigen Aufrufen.

KEYS und ARGV

Keys immer ueber KEYS, sonstige Werte ueber ARGV uebergeben, sonst funktioniert das Skript im Cluster-Modus nicht korrekt.

Skripte kurz halten

Lang laufende Skripte blockieren den gesamten Server. Fuer versionierten, persistenten Code Redis Functions ab 7.0 nutzen.

11. FAQ: Redis Lua-Scripting fuer atomare Operationen

1Warum sind Lua-Skripte atomar?
Single-Threaded-Ausfuehrung, kein anderer Client kann waehrend des Skripts eingreifen.
2EVAL vs. EVALSHA?
EVAL sendet vollen Quelltext, EVALSHA nutzt den Cache per Hash.
3Was bei NOSCRIPT?
Client faellt auf normales EVAL mit vollstaendigem Quelltext zurueck.
4Warum KEYS statt Hartkodierung?
Cluster-Routing basiert auf deklarierten KEYS, sonst falsches Shard-Routing.
5Rollback bei Fehler?
Skript stoppt sofort, aber bereits erfolgte Schreibungen werden nicht automatisch zurueckgerollt.
6redis.call vs. redis.pcall?
call wirft Exception und bricht ab, pcall faengt Fehler als Tabelle ab.
7Maximale Laufzeit?
Kein hartes Limit, aber lua-time-limit warnt, Skripte kurz halten.
8Zufallswerte im Skript?
Moeglich, aber mit Vorsicht wegen Determinismus, effect replication entschaerft das Problem.
9Functions besser als EVAL?
Fuer dauerhaften Code ja, da persistiert. Fuer einfache Skripte reicht EVAL.
10Rate-Limiting ohne Race-Condition?
Increment, Pruefung und Rollback laufen atomar in einem Skript, kein Client kann dazwischenfunken.