Redis 脚本(Lua)完全攻略
“服务器端原子编程” —— 解决 事务无回滚、复杂逻辑、性能瓶颈 的终极武器!
一、Redis Lua 脚本核心优势
| 优势 | 说明 |
|---|---|
| 原子执行 | 脚本整体原子,无中间状态 |
| 减少网络 RTT | 一次调用 = 多条命令 |
| 复杂逻辑支持 | 条件、循环、数学运算 |
| 错误即中止 | 一处错误 → 整个脚本失败 |
| 内置 Redis 调用 | redis.call() / redis.pcall() |
| 支持 SHA1 缓存 | SCRIPT LOAD → EVALSHA 提速 |
一句话:Lua 脚本 = Redis 的存储过程
二、核心命令全表
| 命令 | 说明 | 示例 |
|---|---|---|
EVAL script numkeys key [key ...] arg [arg ...] | 执行脚本 | EVAL "return 'hello'" 0 |
EVALSHA sha1 numkeys key [key ...] arg [arg ...] | 执行缓存脚本 | EVALSHA abc123 1 user:1 |
SCRIPT LOAD script | 加载脚本 → 返回 SHA1 | SCRIPT LOAD "return KEYS[1]" |
SCRIPT EXISTS sha1 [sha1 ...] | 检查脚本是否存在 | SCRIPT EXISTS abc123 |
SCRIPT FLUSH | 清除所有脚本缓存 | SCRIPT FLUSH |
SCRIPT KILL | 终止正在运行的脚本 | SCRIPT KILL |
三、Lua 脚本语法速成
| 语法 | 说明 |
|---|---|
KEYS[1], ARGV[1] | 传入的 key 和参数 |
redis.call('GET', KEYS[1]) | 调用 Redis 命令 |
redis.pcall() | 忽略错误继续执行 |
return value | 返回值(支持 string、int、table) |
local x = 1 | 局部变量 |
if ... then ... end | 条件 |
for i=1,10 do ... end | 循环 |
四、核心实战场景(带完整脚本)
1. 原子扣库存(秒杀经典)
-- EVAL script 1 stock:1001 1
if redis.call('exists', KEYS[1]) == 1 then
local stock = tonumber(redis.call('get', KEYS[1]))
if stock >= tonumber(ARGV[1]) then
redis.call('decrby', KEYS[1], ARGV[1])
return 1
end
end
return 0
EVAL "..." 1 stock:1001 1
# 返回 1 = 扣减成功,0 = 库存不足
2. 限流(滑动窗口)
-- EVAL script 1 rate:limit:user:1001 100 60
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = redis.call('time')[1]
-- 清理过期
redis.call('zremrangebyscore', key, '-inf', now - window)
local current = redis.call('zcard', key)
if current + 1 > limit then
return 0
else
redis.call('zadd', key, now, now .. ':' .. redis.call('incr', key .. ':id'))
redis.call('expire', key, window)
return 1
end
EVAL "..." 1 rate:user:1001 100 60
3. 分布式锁(带自动续期)
-- 获取锁
EVAL "
if redis.call('set', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]) then
return 1
else
return 0
end
" 1 lock:1001 "worker:1" 30000
-- 释放锁(防误删)
EVAL "
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
" 1 lock:1001 "worker:1"
4. 排行榜加权得分
-- 文章得分 = 浏览×1 + 点赞×5 + 评论×10
EVAL "
local id = KEYS[1]
redis.call('zincrby', 'hot:articles', 1, id .. ':view')
redis.call('zincrby', 'hot:articles', 5, id .. ':like')
redis.call('zincrby', 'hot:articles', 10, id .. ':comment')
return redis.call('zscore', 'hot:articles', id .. ':view')
" 1 article:1001
5. 批量转移 Hash 字段
-- 将 user:1 的 score 移到 user:2
EVAL "
local score = redis.call('hget', KEYS[1], 'score')
if score then
redis.call('hset', KEYS[2], 'score', score)
redis.call('hdel', KEYS[1], 'score')
return score
end
return nil
" 2 user:1 user:2
五、脚本加载与缓存(生产必备)
# 1. 加载脚本
SCRIPT LOAD "return redis.call('incr', KEYS[1])"
# 2. 获取 SHA1
# 返回: "a420...f2c"
# 3. 客户端缓存 SHA1,后续用 EVALSHA
EVALSHA a420...f2c 1 counter:1
优势:
- 避免重复传输脚本
- 提升性能
- 支持热更新(
SCRIPT FLUSH+ 重新LOAD)
六、错误处理:redis.call vs redis.pcall
-- call:出错 → 脚本中止
redis.call('incr', 'not_number') --> 错误
-- pcall:出错 → 返回 error 对象,继续执行
local res = redis.pcall('incr', 'not_number')
if res['err'] then
return "error: " .. res['err']
end
七、性能与安全建议
| 建议 | 说明 |
|---|---|
| 脚本 < 1ms | 避免阻塞 Redis |
| 避免死循环 | 用 for i=1,1000 |
| 参数校验 | tonumber(ARGV[1]) |
| 用 SHA1 缓存 | 客户端管理脚本版本 |
| 限制脚本内存 | lua-memory-limit(Redis 7.0+) |
| 禁用危险命令 | rename-command FLUSHALL "" |
八、一键速查表
# 执行脚本
EVAL "return KEYS[1]" 1 hello # 返回 "hello"
EVAL "return #KEYS" 2 a b # 返回 2
# 加载与缓存
SCRIPT LOAD "return 'hi'"
EVALSHA <sha1> 0
# 管理
SCRIPT EXISTS <sha1>
SCRIPT FLUSH
SCRIPT KILL
九、客户端代码示例
Python (redis-py)
import redis
r = redis.Redis()
# 直接执行
script = """
local stock = tonumber(redis.call('get', KEYS[1]))
if stock >= 1 then
redis.call('decr', KEYS[1])
return 1
else
return 0
end
"""
deduct = r.register_script(script)
result = deduct(keys=['stock:1001'])
# 缓存 SHA1
sha = r.script_load(script)
result = r.evalsha(sha, 1, 'stock:1001')
Go (go-redis)
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
script := redis.NewScript(`
if redis.call('exists', KEYS[1]) == 1 then
local n = tonumber(redis.call('get', KEYS[1]))
if n > 0 then
redis.call('decr', KEYS[1])
return 1
end
end
return 0
`)
n, _ := script.Run(ctx, rdb, []string{"stock:1001"}).Int()
十、常见问题与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 脚本卡死 | 死循环 | 加超时 + SCRIPT KILL |
| 内存暴涨 | 大 table | 分批处理 |
| 脚本不一致 | 多实例不同版本 | 用 SCRIPT LOAD + 版本管理 |
| 错误中断 | call 报错 | 用 pcall 捕获 |
十一、Lua 脚本最佳实践清单
[ ] 脚本 < 100 行
[ ] 使用 SHA1 缓存
[ ] 参数校验
[ ] 避免阻塞命令(KEYS *, SLOWLOG)
[ ] 错误用 pcall
[ ] 生产前 SCRIPT LOAD 测试
完成!你已精通 Redis Lua 脚本!
# 一行命令体验脚本全功能
redis-cli <<EOF
EVAL "return {KEYS[1], ARGV[1], redis.call('ping')}" 1 hello world
SCRIPT LOAD "return 'cached'"
EVALSHA $(redis-cli SCRIPT LOAD "return 'cached'" | cut -d' ' -f2) 0
EOF
下一步推荐:
需要我送你:
- “企业级脚本管理平台(加载 + 监控 + 回滚)”?
- “秒杀全链路 Lua 脚本(防超卖 + 排行榜)”?
- “限流中间件(Lua + 滑动窗口)”?
回复:脚本平台 | 秒杀脚本 | 限流中间件 即可!