Lua scripts and functions

Redis · Advanced · 14 min

Scripts turn read-decide-write sequences into one atomic, single-round-trip operation.

EVAL script numkeys key... arg... runs a Lua script on the server. Inside, KEYS[1] and ARGV[1] hold the inputs and redis.call("GET", KEYS[1]) runs commands (redis.pcall returns errors instead of raising). The whole script is atomic: no other command runs while it executes, so it can read, decide and write without races or WATCH retries.

Rules that matter:

  • pass every key the script touches in KEYS, never build key names inside; Redis Cluster uses them to route the script, and all keys must be in the same hash slot,
  • keep scripts short: a long-running script blocks the whole server (busy-reply-threshold, default 5 seconds, after which only SCRIPT KILL or SHUTDOWN NOSAVE are accepted),
  • scripts should be deterministic with respect to their inputs.

SCRIPT LOAD returns a SHA1 and EVALSHA sha ... runs the cached script; clients handle the NOSCRIPT fallback for you.

Redis Functions (7.0+) are the managed successor: FUNCTION LOAD registers a library of named functions that persist with the data and replicate, and FCALL name numkeys ... calls one. Prefer functions for logic that is part of your application's API.

A classic use is a safe lock release: delete the lock only if it still holds your token, which a plain GET then DEL from the client cannot do atomically.

Example

# release a lock only if we still own it (atomic compare-and-delete)
redis-cli SET lock:report tok-123 NX EX 30            # OK
redis-cli EVAL "if redis.call('GET', KEYS[1]) == ARGV[1] then
  return redis.call('DEL', KEYS[1]) else return 0 end" 1 lock:report tok-999
# (integer) 0   wrong token, lock kept
redis-cli EVAL "if redis.call('GET', KEYS[1]) == ARGV[1] then
  return redis.call('DEL', KEYS[1]) else return 0 end" 1 lock:report tok-123
# (integer) 1   released

# the same as a named Redis Function
redis-cli FUNCTION LOAD "#!lua name=locks
redis.register_function('unlock', function(keys, args)
  if redis.call('GET', keys[1]) == args[1] then return redis.call('DEL', keys[1]) end
  return 0 end)"                                       # "locks"
redis-cli FCALL unlock 1 lock:report tok-123          # (integer) 0  already gone

Remember

  • Scripts run atomically on the server: read, decide, write in one step
  • Pass all keys via KEYS so clusters can route them
  • Long scripts block everyone; keep them fast
  • Functions (7.0+) are named, persisted, replicated scripts called with FCALL

Watch out for

  • Building key names inside a script from ARGV: breaks cluster routing and hides keys from Redis.

Check yourself

  1. Why can't a client safely release a lock with GET followed by DEL when the value matches?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: redis.io/docs/latest/develop/interact/programmability/