Skip to content

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 是利器也是险器):

  1. 脚本要短平快:单线程执行意味着长脚本阻塞所有请求——脚本内禁循环大数据集、禁慢命令。我们规约"脚本执行 P99 超 5ms 告警";
  2. 脚本即部署物:Lua 要纳入版本管理、评审、灰度——它和代码一样有 bug 风险,EVAL 直接拼脚本的写法必须禁止(用 EVALSHA + 脚本缓存);
  3. 集群模式注意:脚本涉及的所有 key 必须同 slot(hash tag 保证),跨 slot 直接报错——这个约束在分片设计阶段就要考虑;
  4. 失败要有兜底:脚本返回失败值后业务怎么走(返回"已抢完"还是重试)要明确定义——原子性保证执行不被打断,不保证业务结果正确,逻辑正确仍是业务的责任。

一句话: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 1

Spring 里的用法:

java
DefaultRedisScript<Long> script = new DefaultRedisScript<>(LUA, Long.class);
Long allowed = redisTemplate.execute(script, List.of("rate:" + api), "60", "100");

工程红线:

  1. 脚本要短:长脚本阻塞所有客户端(单线程),死循环脚本直接锁死 Redis(busy-script 时用 SCRIPT KILL,写入过的只能 shutdown);
  2. Cluster 下所有 KEYS 必须同 slot(用 hash tag {uid}),否则报错;
  3. 脚本执行到一半宕机不保证原子(无回滚),脚本里别做太重的写;
  4. 生产脚本纳入版本管理,别在代码里散落字符串拼 Lua。