为什么需要
Redis 单线程,但多个命令之间不原子:
> GET counter # 100
> SET counter 101
并发场景两个客户端都读到 100、各自写 101——丢一次自增。
Lua 脚本在 Redis 内部原子执行:脚本期间不会有别的命令插入。
EVAL 基础
> EVAL "return 'hello'" 0
"hello"
> EVAL "return ARGV[1]" 0 world
"world"
> EVAL "redis.call('SET', KEYS[1], ARGV[1]); return redis.call('GET', KEYS[1])" 1 mykey hello
"hello"
格式:EVAL script numkeys key1 ... keyN arg1 ... argN
KEYS[]:要操作的 key 列表(必须显式声明,集群模式下用来路由)ARGV[]:其他参数
原子自增 + 上限
-- KEYS[1] = 计数器 key
-- ARGV[1] = 上限
local current = tonumber(redis.call("GET", KEYS[1]) or "0")
if current >= tonumber(ARGV[1]) then
return -1
end
return redis.call("INCR", KEYS[1])
调用:
> SET counter 0
> EVAL "...上面脚本..." 1 counter 100
(integer) 1
整个判断 + 自增在 Redis 内一次性完成——不可能两个客户端都过了上限检查。
EVALSHA:缓存脚本
> SCRIPT LOAD "return redis.call('GET', KEYS[1])"
"abc123def..." (SHA)
> EVALSHA abc123def... 1 mykey
省网络传输。生产代码模式:
- 启动时
SCRIPT LOAD,存 SHA - 每次调用
EVALSHA - 失败时(Redis 重启后缓存丢)回退
EVAL
各语言 Redis 客户端通常封装好了这一步(如 redis-py 的 register_script)。
redis.call vs redis.pcall
redis.call("GET", "x") -- 命令失败 → 整个脚本失败
redis.pcall("GET", "x") -- 命令失败 → 返回 error 表,脚本继续
类似 Lua 自己的 error vs pcall。
类型转换
| Lua → Redis | Redis → Lua |
|---|---|
| number → integer | string |
| string → string | integer → number |
| table (array) → array | array → table |
| true → 1 | true → nil(!!) |
| false → nil | false → nil |
坑:true / false 在 Lua 内是布尔,传回客户端会变 1 / nil。最好用整数。
限制
Lua 脚本是 Redis 的阻塞操作——所有别的客户端都在等。
- 不要写超长脚本——超过
lua-time-limit(默认 5 秒)会被警告 - 默认不能 kill 已经写过数据的脚本(避免数据不一致)
- 慎用循环、大量
redis.call
函数(7.0+):FUNCTION 替代 EVAL
Redis 7.0 引入 FUNCTION——把脚本作为持久化对象注册:
#!lua name=mylib
redis.register_function('incr_capped',
function(keys, args)
local n = tonumber(redis.call('GET', keys[1]) or '0')
if n >= tonumber(args[1]) then return -1 end
return redis.call('INCR', keys[1])
end
)
> FUNCTION LOAD "$(cat mylib.lua)"
> FCALL incr_capped 1 counter 100
优势:脚本持久化、能命名、能版本管理。新项目优先用 FUNCTION。
何时不用 Lua 脚本
- 大量数据(脚本里 GET 一万个 key)—— 改用 MGET / Pipeline
- 复杂业务逻辑——放应用层;Redis 只存数据
- 需要 SUBSCRIBE / 等待——脚本阻塞期间不能 pub/sub
Lua 脚本的甜区:少量 key、原子组合 2-5 个 Redis 命令的小逻辑。
例:库存扣减
-- KEYS[1] = stock:product:123
-- ARGV[1] = 扣减数量
local stock = tonumber(redis.call('GET', KEYS[1]) or '0')
local need = tonumber(ARGV[1])
if stock < need then return 0 end
redis.call('DECRBY', KEYS[1], need)
return 1
# Python 调用(redis-py)
script = client.register_script(open('decr_stock.lua').read())
ok = script(keys=['stock:product:123'], args=[1])
整个"读 → 判断 → 扣减"原子完成——防止超卖。
调试
> EVAL "return redis.error_reply('boom')" 0
(error) boom
> SCRIPT EXISTS abc123 # 检查 SHA 是否已 load
> SCRIPT FLUSH # 清空脚本缓存(开发用)
→ 下一篇 OpenResty / Nginx + Lua