Redis Lua脚本编程
约 1712 字大约 6 分钟
redislua
2025-05-11
Redis 内嵌了 Lua 5.1 解释器,支持在服务端原子性地执行 Lua 脚本。这使得复杂的多步操作无需使用事务(MULTI/EXEC)就能保证原子性,是实现分布式锁、限流器等功能的基础。
基本命令
EVAL
# EVAL script numkeys key [key ...] arg [arg ...]
EVAL "return 'Hello World'" 0
# 带 KEYS 和 ARGV
EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 mykey myvalue
# 多个 KEYS 和 ARGV
EVAL "return redis.call('MSET', KEYS[1], ARGV[1], KEYS[2], ARGV[2])" 2 k1 k2 v1 v2EVALSHA(脚本缓存)
# 加载脚本到缓存,返回 SHA1
SCRIPT LOAD "return redis.call('GET', KEYS[1])"
# "e0e1f9fabfc9d4800c877a703b823ac0578ff831"
# 通过 SHA1 执行(避免重复传输脚本内容)
EVALSHA "e0e1f9fabfc9d4800c877a703b823ac0578ff831" 1 mykey
# 检查脚本是否已缓存
SCRIPT EXISTS "e0e1f9fabfc9d4800c877a703b823ac0578ff831"
# 清除所有脚本缓存
SCRIPT FLUSHKEYS 和 ARGV
Lua 脚本通过 KEYS 和 ARGV 两个全局表接收参数:
| 变量 | 说明 | 用途 |
|---|---|---|
KEYS[i] | Redis 键名 | 脚本操作的所有 key 必须通过 KEYS 传入 |
ARGV[i] | 附加参数 | 值、超时时间等非 key 参数 |
-- 正确:通过 KEYS 传递 key
redis.call('SET', KEYS[1], ARGV[1])
-- 错误:硬编码 key(Cluster 模式下可能路由错误)
redis.call('SET', 'mykey', 'value')为什么要区分 KEYS 和 ARGV:Redis Cluster 根据 KEYS 参数决定脚本发送到哪个节点。硬编码的 key 无法被正确路由。
redis.call 与 redis.pcall
-- redis.call: 执行命令,失败时抛出错误(脚本中止)
local value = redis.call('GET', KEYS[1])
-- redis.pcall: 执行命令,失败时返回错误对象(可处理错误)
local ok, err = pcall(function()
return redis.call('INVALID_CMD')
end)
if not ok then
return redis.error_reply('Command failed: ' .. tostring(err))
end类型转换
Redis 返回值和 Lua 类型之间的对应关系:
| Redis 类型 | Lua 类型 | 说明 |
|---|---|---|
| Integer | number | redis.call('INCR', 'k') → number |
| Bulk String | string | redis.call('GET', 'k') → string |
| Array | table | redis.call('LRANGE', 'k', 0, -1) → table |
| Status | redis.status_reply | OK → {ok='OK'} |
| Error | redis.error_reply | 错误 → {err='message'} |
| Nil | false/nil | 不存在的 key → false |
-- 注意:Redis 整数 → Lua number → 返回给客户端时为整数
-- 注意:Lua 的 number 为浮点数,但 Redis 不支持返回浮点
-- 浮点数会被截断为整数
EVAL "return 3.14" 0
-- 返回 (integer) 3
-- 返回浮点数需要转为字符串
EVAL "return tostring(3.14)" 0
-- 返回 "3.14"原子性保证
核心保证:Lua 脚本执行期间,Redis 不会处理其他客户端的命令。这提供了和 MULTI/EXEC 相同的原子性,但更灵活(支持条件判断、循环等逻辑)。
超时保护:
# 脚本最大执行时间(默认 5 秒)
lua-time-limit 5000
# 超时后 Redis 不会杀死脚本,但会:
# 1. 对其他客户端返回 BUSY 错误
# 2. 仅接受 SCRIPT KILL 或 SHUTDOWN NOSAVE 命令
# 杀死正在执行的脚本(如果脚本未写入数据)
SCRIPT KILLRedis 7.0+ Functions API
Redis 7.0 引入了 Functions,是对 EVAL 的升级替代方案。
# 注册函数库
FUNCTION LOAD "#!lua name=mylib
-- 限流函数
redis.register_function('rate_limit', function(keys, args)
local key = keys[1]
local limit = tonumber(args[1])
local window = tonumber(args[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
end)
"
# 调用函数
FCALL rate_limit 1 user:1001:api 100 60
# 管理函数
FUNCTION LIST # 列出所有函数库
FUNCTION DELETE mylib # 删除函数库
FUNCTION DUMP # 导出所有函数
FUNCTION RESTORE ... # 导入函数Functions vs EVAL
| 特性 | EVAL/EVALSHA | Functions |
|---|---|---|
| 持久化 | 不持久化(缓存) | 随 RDB/AOF 持久化 |
| 复制 | 复制脚本内容 | 复制函数定义 |
| 命名 | SHA1 哈希 | 有意义的函数名 |
| 库管理 | 无 | 支持函数库 |
| 标志 | 无 | 支持 no-writes 等标志 |
常见模式
分布式锁
-- 加锁脚本
-- KEYS[1]: 锁的 key
-- ARGV[1]: 锁的值(唯一标识,如 UUID)
-- ARGV[2]: 过期时间(秒)
local key = KEYS[1]
local value = ARGV[1]
local ttl = tonumber(ARGV[2])
if redis.call('SET', key, value, 'NX', 'EX', ttl) then
return 1
end
return 0-- 释放锁脚本(只有持有者才能释放)
-- KEYS[1]: 锁的 key
-- ARGV[1]: 锁的值
local key = KEYS[1]
local value = ARGV[1]
if redis.call('GET', key) == value then
return redis.call('DEL', key)
end
return 0滑动窗口限流
-- KEYS[1]: 限流 key
-- ARGV[1]: 窗口大小(秒)
-- ARGV[2]: 最大请求数
-- ARGV[3]: 当前时间戳(毫秒)
local key = KEYS[1]
local window = tonumber(ARGV[1]) * 1000
local limit = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
-- 移除窗口外的请求
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
-- 当前窗口内的请求数
local count = redis.call('ZCARD', key)
if count < limit then
-- 添加当前请求
redis.call('ZADD', key, now, now .. '-' .. math.random(1000000))
redis.call('PEXPIRE', key, window)
return 1 -- 允许
else
return 0 -- 拒绝
end条件更新
-- Compare-And-Set (CAS)
-- KEYS[1]: key
-- ARGV[1]: 期望的旧值
-- ARGV[2]: 新值
local key = KEYS[1]
local expected = ARGV[1]
local new_value = ARGV[2]
local current = redis.call('GET', key)
if current == expected then
redis.call('SET', key, new_value)
return 1
end
return 0批量操作
-- 批量检查并设置(避免多次 RTT)
-- KEYS: 多个 key
-- ARGV[1]: 默认值
-- ARGV[2]: 过期时间
local default_value = ARGV[1]
local ttl = tonumber(ARGV[2])
local results = {}
for i, key in ipairs(KEYS) do
local exists = redis.call('EXISTS', key)
if exists == 0 then
redis.call('SET', key, default_value, 'EX', ttl)
results[i] = 'SET'
else
results[i] = 'EXISTS'
end
end
return results调试 Lua 脚本
# Redis 内置 Lua 调试器(交互模式)
redis-cli --ldb --eval /path/to/script.lua key1 key2 , arg1 arg2
# 调试命令
# s - step(单步执行)
# n - next(下一行)
# c - continue(继续执行)
# p - print(打印变量)
# b N - breakpoint(设置断点)
# r - restart
# a - abort
# 非交互模式(脚本中使用 redis.log)
redis.log(redis.LOG_WARNING, "Debug: value = " .. tostring(value))性能注意事项
总结
- Lua 脚本在 Redis 中原子执行,是实现复杂原子操作的首选方式
- 所有 key 必须通过
KEYS参数传入,确保 Cluster 模式下正确路由 redis.call遇错中止,redis.pcall可捕获错误- Redis 7.0+ 的 Functions API 是 EVAL 的进化版,支持持久化和更好的管理
- 脚本应保持简短高效,避免超过
lua-time-limit阻塞整个 Redis - 常见应用:分布式锁、限流器、CAS 操作、批量原子操作
贡献者
更新日志
2026/3/14 13:09
查看所有更新日志
9f6c2-feat: organize wiki content and refresh site setup于