Redis Lua 脚本

业务里常有"读-判断-写"的复合操作,逐条发命令既不安全也不高效。Lua 脚本把逻辑写进一段脚本交给服务器整体执行:运行期间不会被其他命令插队(原子性),还省去多次网络往返,适合扣库存、限流、分布式锁续期、防重复提交等必须原子的场景。

EVAL 执行脚本

EVAL 固定格式:EVAL 脚本 参数个数 key... arg...,脚本内用 KEYS[n] 与 ARGV[n] 引用参数:

SET mykey "hello"
# 输出:OK
EVAL "return redis.call('GET', KEYS[1])" 1 mykey
# 输出:"hello"

key 必须通过 KEYS 传入而不是写死在脚本里:一是方便集群按 key 路由到同一分片,二是保证脚本可复用。

典型场景 1:固定窗口限流

窗口内放行 N 次:首次访问时 INCR 并设置过期,之后返回是否放行:

-- KEYS[1]=限流键;ARGV[1]=窗口秒数;ARGV[2]=窗口内最大次数
local c = redis.call('INCR', KEYS[1])
if c == 1 then
    redis.call('EXPIRE', KEYS[1], ARGV[1])
end
if c > tonumber(ARGV[2]) then
    return 0               -- 超限,拒绝
end
return 1                   -- 放行
EVAL "local c=redis.call('INCR',KEYS[1]); if c==1 then redis.call('EXPIRE',KEYS[1],ARGV[1]) end; if c>tonumber(ARGV[2]) then return 0 end; return 1" 1 rate:user:1001 60 10
# 输出:(integer) 1        # 窗口内第 1 次,放行

典型场景 2:分布式锁续期

"只有持有者才能续期"必须原子完成,否则可能误续别人的锁:

-- KEYS[1]=锁键;ARGV[1]=持有者标识;ARGV[2]=续期毫秒
local v = redis.call('GET', KEYS[1])
if v == ARGV[1] then
    return redis.call('PEXPIRE', KEYS[1], ARGV[2])
end
return 0
SET lock:order "owner-001" NX PX 30000
# 输出:OK
EVAL "local v=redis.call('GET',KEYS[1]); if v==ARGV[1] then return redis.call('PEXPIRE',KEYS[1],ARGV[2]) end; return 0" 1 lock:order owner-001 30000
# 输出:(integer) 1        # 是自己的锁,续期成功
# 锁已被别人持有时返回 0,不会误续

错误处理:redis.call 与 redis.pcall

redis.call 出错会终止脚本并向上返回错误;redis.pcall 把错误装进一张表返回,脚本可继续执行:

EVAL "return redis.call('INCR', KEYS[1])" 1 strkey
# 输出:(error) ERR value is not an integer or out of range(脚本终止)
EVAL "local r=redis.pcall('INCR',KEYS[1]); if r.err then return 'handled' end; return r" 1 strkey
# 输出:"handled"          # 捕获错误后继续执行

脚本缓存:SCRIPT LOAD 与 EVALSHA

每次 EVAL 都要传脚本全文。SCRIPT LOAD 先把脚本存进服务端并返回 SHA1,之后用 EVALSHA 只传哈希:

SCRIPT LOAD "return redis.call('GET', KEYS[1])"
# 输出:"b8d4f6c7…(40 位十六进制 SHA1,由脚本内容计算)"
EVALSHA b8d4f6c7 1 mykey
# 输出:"hello"
SCRIPT EXISTS b8d4f6c7
# 输出:1) (integer) 1      # 脚本仍在服务端缓存
SCRIPT FLUSH
# 输出:OK                  # 清空所有缓存脚本

脚本缓存被清(重启、SCRIPT FLUSH)后 EVALSHA 会报 NOSCRIPT,程序需先用 EVAL 兜底或重新 LOAD。

主从复制与注意事项

  • 主从复制默认同步脚本"产生的写命令"(效果复制,7.0 起为唯一模式),因此别用 TIME、RANDOMKEY 等不确定命令驱动写逻辑,防止主从数据不一致。
  • 脚本原子但会阻塞其他请求:不要写大循环、KEYS * 或大范围遍历。
  • 可复用的正式业务脚本,Redis 7.0 起可用 Functions(FUNCTION LOAD)管理,支持集中更新与权限控制,是脚本缓存方案的演进。

小结

Lua 脚本 = 把"读-判-写"交给服务器原子完成。掌握 EVAL/EVALSHA、KEYS/ARGV、redis.call 与 redis.pcall 的取舍,限流、锁续期、防超卖这类高并发原子操作就有了通用解法;注意脚本别写长循环,正式场景优先 EVALSHA/Functions 以省带宽、便于维护。

笔记加载中…