在 Redis 中,多条命令之间可能被其他客户端插入执行。如果需要把一组操作作为一个整体原子执行,Lua 脚本是最常用的方案。Redis 会单线程地执行整段脚本,期间不会插入其他命令。
为什么用 Lua 脚本
- 保证原子性:脚本执行期间相当于一个不可分割的事务
- 减少网络往返:多条命令一次请求完成
- 可以用命令结果做条件判断,实现"先读后写"的逻辑
EVAL 命令基本用法
redis-cli EVAL "return 1" 0
EVAL 的第一个参数是脚本内容,第二个参数是脚本涉及的 key 数量,之后依次是 KEYS 和 ARGV。
redis-cli EVAL "return {KEYS[1], ARGV[1]}" 1 mykey hello
在脚本中通过 redis.call() 调用 Redis 命令:
redis-cli EVAL "redis.call('SET', KEYS[1], ARGV[1]); return redis.call('GET', KEYS[1])" 1 greeting "你好"
redis.call() 出错会中断脚本,如果想忽略错误可以使用 redis.pcall()。
实战示例
1. 简单限流器
-- KEYS[1]: 计数器 key
-- ARGV[1]: 限流次数
-- ARGV[2]: 过期时间(秒)
local current = tonumber(redis.call('INCR', KEYS[1]))
if current == 1 then
redis.call('EXPIRE', KEYS[1], ARGV[2])
end
if current > tonumber(ARGV[1]) then
return 0
end
return 1
调用方式:
redis-cli EVAL "$(cat ratelimit.lua)" 1 rate:user:1001 100 60
2. 释放分布式锁(校验持有者)
-- KEYS[1]: 锁 key
-- ARGV[1]: 锁的唯一标识
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
else
return 0
end
先用 SET lock:order1 uuid NX EX 10 加锁,释放时把 uuid 作为 ARGV[1] 传入,避免误删别人的锁。
EVALSHA 与脚本缓存
每次 EVAL 都要传输整段脚本,脚本较长时开销大。Redis 提供 SCRIPT LOAD 把脚本缓存到服务端,返回一个 SHA1 值,之后用 EVALSHA 只传哈希即可。
redis-cli SCRIPT LOAD "return redis.call('GET', KEYS[1])"
# 输出类似 "a5260dd66ce02462c5b5231c977b769b48d1b7d9"
redis-cli EVALSHA a5260dd66ce02462c5b5231c977b769b48d1b7d9 1 mykey
注意:Redis 重启或执行 SCRIPT FLUSH 后缓存会丢失,客户端一般要实现"先 EVALSHA,返回 NOSCRIPT 错误再降级 EVAL"的逻辑。
Java 客户端示例
以 Jedis 为例:
try (Jedis jedis = new Jedis("127.0.0.1", 6379)) {
String script =
"local current = redis.call('INCR', KEYS[1]) " +
"if current == 1 then redis.call('EXPIRE', KEYS[1], ARGV[1]) end " +
"return current";
Object result = jedis.eval(
script,
1,
"rate:user:1001",
String.valueOf(60)
);
System.out.println("当前计数:" + result);
}
Spring Data Redis 中可以用 DefaultRedisScript:
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(script);
redisScript.setResultType(Long.class);
Long count = stringRedisTemplate.execute(redisScript, List.of("rate:user:1001"), "60");
注意事项
- Lua 脚本执行期间会阻塞其他命令,脚本逻辑要尽量短,避免慢脚本拖垮 Redis
- 脚本只能操作在
KEYS中声明的 key,这是为了保证在 Redis Cluster 下能正确定位到槽位 - 脚本中的 key 与参数一定要通过
KEYS和ARGV传入,不要硬编码 - 部分命令(如
SUBSCRIBE)不能在脚本中使用,具体以官方文档为准