Redis 的 Lua 脚本功能是什么?如何使用?
- 标签:后端、Redis
🏛 业务架构师
Lua 脚本在 Redis 的价值一句话:多个命令原子执行、中间状态不可见——单线程执行脚本期间不会插入其他客户端命令,相当于免费的"事务 + 业务逻辑"。
我按真实业务场景讲用法:
- 库存扣减(最经典):
查库存→判断→扣减三步必须原子,否则超卖:
lua
local stock = tonumber(redis.call('get', KEYS[1]))
if stock and stock >= tonumber(ARGV[1]) then
return redis.call('decrby', KEYS[1], ARGV[1])
end
return -1- 分布式锁安全释放:验证"锁是自己的"再删,两步必须原子;
- 限流器:滑动窗口的"计数 + 判断 + 设过期"原子化;
- 复合业务判断:券包核销的"查资格 + 核销 + 计数"打包执行。
业务架构上的四条纪律(Lua 是利器也是险器):
- 脚本要短平快:单线程执行意味着长脚本阻塞所有请求——脚本内禁循环大数据集、禁慢命令。我们规约"脚本执行 P99 超 5ms 告警";
- 脚本即部署物:Lua 要纳入版本管理、评审、灰度——它和代码一样有 bug 风险,EVAL 直接拼脚本的写法必须禁止(用 EVALSHA + 脚本缓存);
- 集群模式注意:脚本涉及的所有 key 必须同 slot(hash tag 保证),跨 slot 直接报错——这个约束在分片设计阶段就要考虑;
- 失败要有兜底:脚本返回失败值后业务怎么走(返回"已抢完"还是重试)要明确定义——原子性保证执行不被打断,不保证业务结果正确,逻辑正确仍是业务的责任。
一句话:Lua 把"查改写"业务规则原子化,是 Redis 从缓存升级为轻量业务引擎的关键能力。
🏗 系统架构师
Lua 脚本让 Redis 原子地执行一段多命令逻辑——脚本执行期间不会被其他命令插入,且能读取中间结果做条件判断,这是 MULTI/EXEC 事务做不到的(事务命令间无法交互)。
工作机制:EVAL 执行脚本(脚本缓存 EVALSHA 复用),KEYS 与 ARGV 分离传参(集群模式下保证 key 同槽约束可被检查);脚本内可调用全部 Redis 命令。
我的典型用法:
- 分布式锁释放:比对 value 防误删,get+del 原子化:
lua
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1]) else return 0 end- 限流器:令牌桶/滑动窗口的"读计数 → 判断 → 增减"原子化,天然防并发超放;
- 库存扣减:判断余量再 DEC,避免超卖;
- 复合原子操作:转账双方余额校验 + 扣加一条脚本完成。
工程红线(重要):
- 脚本必须短小、复杂度可控——单线程模型下慢脚本阻塞整个实例;
- 脚本要确定性(相同输入相同输出),避免随机/时间函数(影响主从复制一致性);
- 集群下操作的 key 必须同槽(hash tag);
- 脚本要有超时配置(busy-reply-threshold 后可 SCRIPT KILL),并纳入版本管理(当作代码部署,不能线上现写)。
💻 全栈开发专家
Lua 让多条 Redis 命令原子执行(单线程里脚本执行期间不插入其他命令),还能读中间结果做逻辑判断——这是 MULTI/EXEC 做不到的。另外一次网络传整个脚本(EVALSHA 传 hash),减少 RTT。
典型用途一:安全的分布式锁解锁(判断是自己的锁才删):
lua
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end典型用途二:限流器(固定窗口):
lua
local current = tonumber(redis.call("INCR", KEYS[1]))
if current == 1 then
redis.call("EXPIRE", KEYS[1], ARGV[1]) -- 第一次设置过期
end
if current > tonumber(ARGV[2]) then
return 0 -- 超限
end
return 1Spring 里的用法:
java
DefaultRedisScript<Long> script = new DefaultRedisScript<>(LUA, Long.class);
Long allowed = redisTemplate.execute(script, List.of("rate:" + api), "60", "100");工程红线:
- 脚本要短:长脚本阻塞所有客户端(单线程),死循环脚本直接锁死 Redis(
busy-script时用SCRIPT KILL,写入过的只能 shutdown); - Cluster 下所有 KEYS 必须同 slot(用 hash tag
{uid}),否则报错; - 脚本执行到一半宕机不保证原子(无回滚),脚本里别做太重的写;
- 生产脚本纳入版本管理,别在代码里散落字符串拼 Lua。