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:

  1. Atomicity: Script execution cannot be interrupted by other commands
  2. Reduced Network Round Trips: Multiple commands packaged into one request
  3. 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 ...]
ParameterDescription
scriptLua 5.1 script code
numkeysNumber 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:

Featureredis.callredis.pcall
On ErrorAborts script, returns error to clientCatches error, returns error object, script continues
Use CaseStrong consistency, any error should abortFault 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

  1. Do not use global variables in scripts, always declare local variables with local to avoid polluting the global namespace
  2. Script execution is blocking, excessive execution time causes Redis to be unable to respond to other requests, recommend setting reasonable timeouts
  3. Redis errors in Lua will terminate the script by default (redis.call), use redis.pcall when fault tolerance is needed
  4. Prohibit using random commands (like RANDOMKEY) or time-dependent commands, otherwise results will be inconsistent during master-slave replication