Lua 脚本

EVAL 基础

脚本以原子方式运行:脚本执行期间不会执行其他命令。KEYS[n] 和 ARGV[n] 从 1 开始索引。

# EVAL script numkeys key [key ...] arg [arg ...]
EVAL "return 'hello'" 0

# Access keys and arguments
EVAL "return {KEYS[1], ARGV[1]}" 1 mykey myvalue

# Call Redis commands from Lua
EVAL "redis.call('SET', KEYS[1], ARGV[1]); return 1" 1 counter 42

# redis.call vs redis.pcall
# redis.call  → raises error on failure (propagates to client)
# redis.pcall → returns error table, script continues

EVAL "
  local v = redis.call('GET', KEYS[1])
  if v then
    return tonumber(v) + 1
  end
  return 1
" 1 mykey

原子比较并设置

经典使用场景:无需事务即可原子地进行读-改-写。

# Atomic CAS: set new value only if current equals expected
EVAL "
  local cur = redis.call('GET', KEYS[1])
  if cur == ARGV[1] then
    redis.call('SET', KEYS[1], ARGV[2])
    return 1
  end
  return 0
" 1 mykey expected_value new_value

# Rate limiter: allow N requests per window
EVAL "
  local key     = KEYS[1]
  local limit   = tonumber(ARGV[1])
  local window  = tonumber(ARGV[2])
  local current = redis.call('INCR', key)
  if current == 1 then
    redis.call('EXPIRE', key, window)
  end
  if current > limit then
    return 0
  end
  return 1
" 1 ratelimit:user:42 100 60

使用 Lua 实现分布式锁

# Acquire lock (SET NX EX is atomic but Lua allows custom logic)
# SET key value NX EX seconds
SET lock:resource unique_token NX EX 30

# Release lock: only release if we own it (atomic check-and-delete)
EVAL "
  if redis.call('GET', KEYS[1]) == ARGV[1] then
    return redis.call('DEL', KEYS[1])
  else
    return 0
  end
" 1 lock:resource unique_token

# Extend lock TTL atomically
EVAL "
  if redis.call('GET', KEYS[1]) == ARGV[1] then
    return redis.call('PEXPIRE', KEYS[1], ARGV[2])
  else
    return 0
  end
" 1 lock:resource unique_token 30000

SCRIPT LOAD 与 EVALSHA

加载一次脚本并通过其 SHA1 哈希执行,以减少网络开销。

# Load script into server cache, returns SHA1
SCRIPT LOAD "return redis.call('GET', KEYS[1])"
# Returns: "e0e1f9fabfa9d353eca4c6f67a4a3c35c92fa08d"

# Execute by SHA1
EVALSHA e0e1f9fabfa9d353eca4c6f67a4a3c35c92fa08d 1 mykey

# Check if script is cached
SCRIPT EXISTS sha1 sha2 sha3    # returns 0/1 per sha

# Flush all cached scripts
SCRIPT FLUSH

# Using EVALSHA in Node.js (ioredis)
const sha = await client.script("load", luaScript);
const result = await client.evalsha(sha, 1, "mykey");

Lua 最佳实践

最佳实践原因
始终通过 KEYS[] 传递键Redis 集群键路由所必需
保持脚本简短长脚本会阻塞 Redis(单线程)
生产中使用 EVALSHA避免每次调用重新发送脚本字节
脚本中无 I/O 或睡眠会阻塞事件循环
使用 tonumber()/tostring()Redis 返回批量字符串,按需转换类型