This is article 43 in the Big Data series. This article introduces Redis Lua scripting usage, including EVAL command, error handling functions, and typical atomic operation practice cases.
Complete illustrated version: CSDN Original | Juejin
Why Use Lua Scripts
Redis itself is single-threaded, but when business needs “read-compute-write” multi-step operations, there are race conditions between multiple network round trips between the client and Redis. Lua scripts execute atomically on the Redis server, solving:
- Atomicity: Script execution cannot be interrupted by other commands
- Reduced Network Round Trips: Multiple commands packaged into one request
- Reusability: Scripts can be cached via
SCRIPT LOAD, then called by SHA1
Lua Introduction
Lua is a lightweight scripting language created in 1993, with dynamic typing, automatic memory management, and first-class function support. Redis embeds a Lua 5.1 interpreter, and scripts run inside the Redis process, with direct access to Redis data.
EVAL Command Syntax
EVAL script numkeys key [key ...] arg [arg ...]
| Parameter | Description |
|---|---|
script | Lua 5.1 script code |
numkeys | Number of key parameters |
key [key ...] | Accessed in script via KEYS[1], KEYS[2]… (1-indexed) |
arg [arg ...] | Accessed in script via ARGV[1], ARGV[2]… (1-indexed) |
Example:
EVAL "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
# Returns: ["key1","key2","first","second"]
redis.call vs redis.pcall
There are two ways to execute Redis commands in Lua scripts:
| Feature | redis.call | redis.pcall |
|---|---|---|
| On Error | Aborts script, returns error to client | Catches error, returns error object, script continues |
| Use Case | Strong consistency, any error should abort | Fault tolerance, partial failure acceptable |
-- redis.call: throws directly on error
redis.call('SET', KEYS[1], ARGV[1])
-- redis.pcall: returns error table on error, script continues
local ok, err = pcall(redis.call, 'SET', KEYS[1], ARGV[1])
if err then
return redis.error_reply(err)
end
Practical Cases
Case 1: Atomic Counter
local key = KEYS[1]
local increment = tonumber(ARGV[1])
local current = tonumber(redis.call('GET', key) or 0)
local new_value = current + increment
redis.call('SET', key, new_value)
return new_value
EVAL "<above script>" 1 counter:hits 5
Case 2: CAS (Compare-And-Swap)
Atomically “if current value equals expected value, then update to new value”:
local key = KEYS[1]
local expected = ARGV[1]
local new_val = ARGV[2]
local current = redis.call('GET', key)
if current == expected then
redis.call('SET', key, new_val)
return 1
else
return 0
end
Returns 1 for successful update, 0 for CAS failure.
Case 3: List Batch Insert
local key = KEYS[1]
-- unpack expands table to multiple arguments
redis.call('LPUSH', key, unpack(ARGV))
return redis.call('LLEN', key)
EVAL "<above script>" 1 mylist a b c d e
Case 4: Hash Batch Write
local key = KEYS[1]
for i = 1, #ARGV, 2 do
redis.call('HSET', key, ARGV[i], ARGV[i+1])
end
return redis.call('HLEN', key)
SCRIPT Commands: Caching and Management
# Preload script, return SHA1 digest
SCRIPT LOAD "return redis.call('GET', KEYS[1])"
# Execute cached script by SHA1 (avoids transmitting script content each time)
EVALSHA <sha1> 1 mykey
# Check if script exists in cache
SCRIPT EXISTS <sha1>
# Flush all cached scripts
SCRIPT FLUSH
Recommended Practice: In production, warm up commonly used scripts via SCRIPT LOAD, business code only sends SHA1, reducing network transmission.
Notes
- Do not use global variables in scripts, always declare local variables with
localto avoid polluting the global namespace - Script execution is blocking, excessive execution time causes Redis to be unable to respond to other requests, recommend setting reasonable timeouts
- Redis errors in Lua will terminate the script by default (
redis.call), useredis.pcallwhen fault tolerance is needed - Prohibit using random commands (like
RANDOMKEY) or time-dependent commands, otherwise results will be inconsistent during master-slave replication