English | 中文版
[TOC]
Redis introduced Lua scripting support starting from version 2.6.
Redis creates and customizes a Lua environment as follows:
- Create a base Lua state; all subsequent modifications are applied to this state.
- Load multiple standard and auxiliary libraries into the Lua state so scripts can use them to operate on data.
- Create a global
redistable exposing helper functions such asredis.callto execute Redis commands from scripts. - Replace Lua's built-in side-effecting random functions with Redis-provided deterministic replacements to avoid side effects.
- Create sorting helper utilities so certain Redis command results can be deterministically ordered.
- Create an error-wrapper helper for
redis.pcallto provide richer error reporting. - Protect the Lua global environment to prevent user scripts from accidentally introducing new globals.
- Store the prepared Lua environment on the server state for later use when executing scripts.
A new Lua state is created using Lua's C API lua_open.
The server loads the following libraries into the Lua state:
- base library
- table library
- string library
- math library
- debug library
- Lua CJSON library
- struct library
- Lua cmsgpack library
Redis exposes a global redis table providing helpers used by scripts:
redis.callandredis.pcall— execute Redis commands from Luaredis.log— log to the Redis logredis.sha1hex— compute SHA1 checksumsredis.error_replyandredis.status_reply— construct replies for errors and status messages
Redis requires that all scripts and functions executed by the server be side-effect free. To help ensure this, Redis replaces Lua's default random functions with server-provided deterministic equivalents.
Redis treats the following commands as non-deterministic (their result ordering may vary):
- SINTER, SUNION, SDIFF, SMEMBERS, HKEYS, HVALS, KEYS
Helper sorting functions are provided so script-visible results can be deterministically ordered when needed.
Redis installs an internal handler named __redis__err__handler to wrap errors produced by redis.pcall, improving error messages.
The server protects the global environment to avoid accidental global variable creation when a script omits local declarations.
The prepared Lua state is stored on the server redisServer structure:
/** @brief redis server */
struct redisServer {
/* Scripting */
lua_State *lua; /* Lua state */
redisClient *lua_client; /* pseudo-client for the Lua interpreter */
redisClient *lua_caller; /* the client currently running EVAL, or NULL */
dict *lua_scripts; /* scripts dictionary: key = SHA1, value = script */
mstime_t lua_time_limit; /* script timeout in milliseconds */
mstime_t lua_time_start; /* script start time (ms) */
int lua_write_dirty; /* true if a write command was called during the script */
int lua_random_dirty; /* true if a random command was called during the script */
int lua_timedout; /* true if the script reached the time limit */
int lua_kill; /* kill the script if true */
};When a Lua script calls redis.call or redis.pcall, the server uses a pseudo-client to execute the requested Redis command on behalf of the script:
Title: Steps when a Lua script executes a Redis command
Lua environment->pseudo-client: deliver the command requested by redis.call
pseudo-client-->command executor: forward command for execution
command executor->pseudo-client: return command result
pseudo-client-->Lua environment: return result to Lua
- The Lua environment sends the command requested by
redis.call/redis.pcallto the pseudo-client. - The pseudo-client forwards the command to the command executor.
- The executor runs the command and returns the result to the pseudo-client.
- The pseudo-client returns the result to the Lua environment.
- The Lua environment returns the result to the calling
redis.call/redis.pcallwrapper.
Redis stores any script executed via EVAL or loaded with SCRIPT LOAD in lua_scripts so the server can implement SCRIPT EXISTS and script replication:
/** @brief redis server */
struct redisServer {
...
dict *lua_scripts; /* key = script SHA1, value = script */
...
};Example:
redis> EVAL "return 'hello world'" 0EVAL execution steps:
- Define a Lua function in the Lua state using the supplied script.
- Save the script into
lua_scriptsfor future reference. - Call the defined Lua function to execute the script.
Redis defines the function name as f_ plus the script's SHA1 (40 hex chars); the function body is the script itself.
Example:
EVAL "return 'hello world'" 0Redis defines in Lua:
function f_5332031c6b470dc5a0dd9b4bf2030dea6d65de91()
return 'hello world'
endBenefits of storing the script as a function:
- Execution is trivial — just call the function.
- Function local scope keeps the Lua state clean, reduces GC pressure, and avoids globals.
- If the function exists, the server can execute the script by SHA1 without providing the script body (EVALSHA semantics).
EVAL saves the supplied script into the server lua_scripts dictionary for SCRIPT EXISTS and replication.
Execution flow:
- Populate
KEYSandARGVarrays in Lua from the EVAL's key and arg parameters; expose them as global variables to the script. - Install a timeout hook into the Lua state so the script can be killed via
SCRIPT KILLor server shutdown if it overruns. - Execute the script function.
- Remove the timeout hook.
- Place the result into the client's output buffer for the server to send back.
- Run garbage collection in the Lua state as needed.
EVALSHA <sha1> <numkeys> [keys..] [args..] executes a script identified by SHA1.
If the script is not present on a replica, EVALSHA may fail; the server handles this during replication (see replication notes) by substituting the full EVAL when necessary.
SCRIPT FLUSH clears all script-related state: it frees and recreates lua_scripts, closes the current Lua state and reinitializes a fresh one.
graph TD
free_scripts --> rebuild_scripts --> close_lua --> init_new_lua
SCRIPT EXISTS checks whether the provided SHA1s are present in lua_scripts. Multiple SHA1s can be checked at once.
SCRIPT LOAD defines the function in the Lua state and stores the script in lua_scripts.
Example:
redis> SCRIPT LOAD "return 'hi'"
"2f31ba2bb6d6a0f42cc159d2e2dad55440778de3"Lua function created:
function f_2f31ba2bb6d6a0f42cc159d2e2dad55440778de3()
return 'hi'
endIf lua-time-limit is configured, Redis installs a periodic timeout hook into the Lua state before executing a script. The hook checks script execution time and, if over the limit, inspects whether a SCRIPT KILL or SHUTDOWN command has arrived; if so it will terminate execution.
Hooked execution flow:
graph TD
start-->IsEnd{Script finished?}
IsEnd --yes--> return
IsEnd --no--> IsTimeout{Has the hook detected a timeout?}
IsTimeout --no--> Continue --> IsEnd
IsTimeout --yes--> IsArrive{Has SCRIPT KILL or SHUTDOWN arrived?}
IsArrive --no--> Continue
IsArrive --yes--> perform kill or shutdown
When propagating commands to replicas, the master forwards the executed command (EVAL, SCRIPT FLUSH, SCRIPT LOAD, etc.) to all slaves.
graph LR
Client --EVAL, SCRIPT FLUSH, SCRIPT LOAD,...--> Master
Master --EVAL, SCRIPT FLUSH, SCRIPT LOAD,...--> Slave1
Master --EVAL, SCRIPT FLUSH, SCRIPT LOAD,...--> Slave2
Master --EVAL, SCRIPT FLUSH, SCRIPT LOAD,...--> Slave3
EVAL: the script executed on the master is also executed on all slaves.SCRIPT FLUSH: the master propagatesSCRIPT FLUSHto all slaves.SCRIPT LOAD: the master propagatesSCRIPT LOADto ensure slaves load the same script.
Because masters and slaves may not have the same script caches, EVALSHA could fail on a slave if the script is missing. To ensure correctness, the master only propagates EVALSHA if it is safe — i.e., the master knows the script has already been propagated to all slaves. The master tracks propagated scripts using repl_scriptcache_dict:
/** @brief redis server */
struct redisServer {
...
/* Replication script cache. */
dict *repl_scriptcache_dict; /* scripts propagated to slaves: keys = SHA1 */
list *repl_scriptcache_fifo; /* FIFO LRU eviction */
...
};Replication rules:
- The master clears
repl_scriptcache_dictwhenever a new slave is added. - When executing
EVALSHA, the master checksrepl_scriptcache_dict:- If the SHA1 is present, propagate the
EVALSHAas-is. - If missing, convert to the equivalent
EVAL(using the script body fromlua_scripts), propagate theEVAL, and add the SHA1 torepl_scriptcache_dict.
- If the SHA1 is present, propagate the
Flow:
graph TD
start(After executing EVALSHA <sha> ...) --> is_in{Is sha in repl_scriptcache_dict?}
is_in --yes--> spread1(Propagate EVALSHA)
is_in --no--> trans(Convert EVALSHA to EVAL) --> spread2(Propagate EVAL) --> add(Add sha to repl_scriptcache_dict)
[1] Huang Jianhong. Redis Design and Implementation