前言、为什么要用 Lua?
多步操作合并为一步,保证原子性。
减少网络通信次数。
下推逻辑到 Redis,提高性能。
一、Redis 使用 Lua 脚本的两种方式
方式一:使用
--eval
执行脚本文件这种方式 需要先写一个 Lua 文件。
📌 示例:创建一个
setname.lua
文件,内容如下:-- KEYS[1] 表示 key -- ARGV[1] 表示 value -- ARGV[2] 表示过期时间(秒) return redis.call("set", KEYS[1], ARGV[1], "EX", ARGV[2])
在命令行执行:
redis-cli --eval setname.lua name , czq 5
📌 解释:
--eval setname.lua name
→name
传给KEYS[1]
逗号 , 后面的是
ARGV
→"czq"
给ARGV[1]
,5
给ARGV[2]
执行效果:
OK
再验证:
get name "czq" ttl name (integer) 5 # 有效期 5 秒
方式二:使用
eval
命令直接写脚本这种方式 直接在 redis-cli 里执行 Lua 代码,不需要写文件。
📌 示例:
redis-cli
进入 redis-cli 后执行:
eval "return redis.call('set', KEYS[1], ARGV[1], 'EX', ARGV[2])" 1 name czq 5
📌 解释:
"return redis.call(...)"
→ 直接写 Lua 代码
1
表示有 1 个 KEYS 参数
name
→ KEYS[1]
czq
→ ARGV[1]
5
→ ARGV[2]结果:
OK
二者区别
redis-cli --eval ...
是在 Linux shell 里执行,不用手动进入交互模式。(方式一)
redis-cli --eval lua文件的路径/lua的名称.lua....
eval "..."
必须进入 redis-cli 交互模式才能用。(方式二)
三、KEYS 和 ARGV 的作用
在 Redis 的 Lua 脚本里:
KEYS → 存放 key(可以有多个,KEYS[1]、KEYS[2]...)
ARGV → 存放参数(value、过期时间等)
📌 示例:
-- 假设脚本里是这样: return "KEY=" .. KEYS[1] .. ", VALUE=" .. ARGV[1] .. ", TTL=" .. ARGV[2]
执行:
eval "return 'KEY='..KEYS[1]..', VALUE='..ARGV[1]..', TTL='..ARGV[2]" 1 name czq 5
输出结果:
"KEY=name, VALUE=czq, TTL=5"
👉 总结:
KEYS 专门用来传 key(好处是 Redis 会自动进行 key hash 定位,支持集群)
ARGV 专门用来传其他参数(value、过期时间等)
四、Spring Boot 使用 Lua 脚本
这个例子是SETNX + 过期时间结合成的原子性,通常用于 分布式锁 或 一人一单 之类的业务。
在 Spring Boot 里也可以执行这个 Lua 脚本。
📌 示例代码:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.stereotype.Service;import java.util.Collections;@Service public class RedisLuaService {@Autowiredprivate RedisTemplate<String, Object> redisTemplate;/*** 使用 SETNX + EX 实现键值设置(例如分布式锁)*/public Object setNxWithExpire() {// 1️⃣ Lua 脚本// 先尝试 SETNX,如果成功,再设置过期时间String lua ="if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then " +" redis.call('expire', KEYS[1], ARGV[2]) " +" return 1 " +"else " +" return 0 " +"end";// 2️⃣ 封装脚本DefaultRedisScript<Long> script = new DefaultRedisScript<>();script.setScriptText(lua);script.setResultType(Long.class);// 3️⃣ 执行脚本return redisTemplate.execute(script, Arrays.asList("lock_key"), // KEYS[1] 例如分布式锁的 key,keys 参数要求传的是一个 List<K>,定义成 List<String>,方便支持多个 key。"czq", "5" // ARGV[1] = value (锁的标识),ARGV[2] = 过期时间秒);} }
✅ 使用说明
Object result = redisLuaService.setNameWithExpireIfAbsent(); System.out.println(result);
返回 1:设置成功(key 原本不存在)。
返回 0:设置失败(key 已经存在)。
Redis 里结果:
127.0.0.1:6379> get name "czq" 127.0.0.1:6379> ttl name (integer) 5
📌 改造后的好处
保证原子性
用 Lua 保证
SETNX
和EXPIRE
是在 Redis 内部一次性执行,避免SETNX
成功但服务挂掉导致没有设置过期时间,从而出现“死锁”。分布式锁场景
SETNX
确保只有一个客户端能拿到锁。
EXPIRE
确保即使客户端崩溃,锁也会在过期时间后自动释放。一人一单 / 防重提交
SETNX
用来保证某个 key(比如订单 ID 或用户 ID)只能被设置一次。
EXPIRE
防止 key 永久占用,给系统自动恢复的能力。返回值可控
返回
1
表示设置成功(抢到锁 / 成功下单)。返回
0
表示设置失败(别人已经抢到锁 / 已经下过单📌小贴士:
Redis 本身就支持
SET key value EX seconds NX
命令(SETNX + EXPIRE的原子性组合版),它是原子性的,不需要 Lua。Spring 的RedisTemplate
里也可以直接调用,避免自己写 Lua。@Service public class RedisService {@Autowiredprivate RedisTemplate<String, Object> redisTemplate;public Boolean setIfAbsentWithExpire(String key, String value, long seconds) {return redisTemplate.opsForValue().setIfAbsent(key, value, seconds, TimeUnit.SECONDS);} }
优点
✅ 原子性保证(内部就是单条 Redis 命令)。
✅ 使用简单,无需写 Lua。
✅ 代码更可读,Spring 已经封装好了。