Redis 脚本

Redis 脚本(Lua)完全攻略

“服务器端原子编程” —— 解决 事务无回滚、复杂逻辑、性能瓶颈 的终极武器!


一、Redis Lua 脚本核心优势

优势说明
原子执行脚本整体原子,无中间状态
减少网络 RTT一次调用 = 多条命令
复杂逻辑支持条件、循环、数学运算
错误即中止一处错误 → 整个脚本失败
内置 Redis 调用redis.call() / redis.pcall()
支持 SHA1 缓存SCRIPT LOADEVALSHA 提速

一句话: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加载脚本 → 返回 SHA1SCRIPT 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

下一步推荐

  1. Redisson 分布式锁深度解析
  2. Lua 脚本热更新架构
  3. 高并发秒杀系统全套方案

需要我送你

  • “企业级脚本管理平台(加载 + 监控 + 回滚)”
  • “秒杀全链路 Lua 脚本(防超卖 + 排行榜)”
  • “限流中间件(Lua + 滑动窗口)”

回复:脚本平台 | 秒杀脚本 | 限流中间件 即可!

文章已创建 2481

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部