服务公告
在Springboot项目中使用Redis提供给Lua的脚本
发布时间:2025-12-30 00:12
在 Spring Boot 项目中使用 Redis 给 Lua 的脚本(企业级落地版)✅
在业务链路里,只要你遇到“先查再改、先判断再写、需要保证并发一致性”的场景,Redis Lua 脚本基本就是性价比最高的解法:把多条 Redis 命令合并成“一次执行”,天然具备原子性,还能减少网络往返(RTT)。简单说:让 Redis 做“就地决策”,别让应用层来回跑腿。🙂

1)Lua 脚本给 Redis 的“入参规则”:KEYS 与 ARGV
这是踩坑率最高但也最关键的一点:所有 Key 放在 KEYS,非 Key 参数放在 ARGV。
-- KEYS[1]:业务Key(例如限流Key、库存Key、锁Key)
-- ARGV[1..n]:阈值、窗口时间、增量等非Key参数
解释(为什么要这么做)
- KEYS:Redis 用它做路由(尤其是 Cluster),也用于权限与安全审计语义。
- ARGV:纯参数,不参与路由。
- 如果你把 Key 塞进 ARGV:在集群下更容易出现“执行节点不对、跨槽失败”等隐性问题。⚠️
2)Spring Boot 调用 Lua 的标准姿势:DefaultRedisScript + RedisTemplate
下面这段是“可复制进项目”的主流写法:Lua 脚本放在 resources/lua/,由 Spring 管理并执行。
@Configuration
public class RedisLuaConfig {
@Bean
public DefaultRedisScript<Long> rateLimitScript() {
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setLocation(new ClassPathResource("lua/rate_limit.lua"));
script.setResultType(Long.class);
return script;
}
}
逐行解释(别跳过,这里决定你线上稳不稳)
DefaultRedisScript<Long>:声明脚本返回值类型,避免返回值反序列化“玄学报错”。setLocation(...):从 classpath 加载脚本文件,便于版本管理与灰度发布。setResultType(Long.class):Lua 返回数字时,强烈建议用 Long 接;否则你可能拿到 Integer/Long 混用导致类型转换异常。
3)示例:用 Lua 做固定窗口限流(高并发下很顶)
3.1 Lua 脚本:rate_limit.lua
-- KEYS[1] = 限流key
-- ARGV[1] = 最大次数(limit)
-- ARGV[2] = 窗口秒数(windowSeconds)
local current = redis.call('INCR', KEYS[1])
if current == 1 then
redis.call('EXPIRE', KEYS[1], tonumber(ARGV[2]))
end
if current > tonumber(ARGV[1]) then
return 0
else
return 1
end
逐段解释(脚本逻辑拆开讲清楚)
INCR:自增计数器;Redis 侧执行,避免并发下“读-改-写”撕裂。current == 1:只有第一次计数时才设置过期,保证窗口一致。tonumber(ARGV[x]):Lua 默认字符串,强转数值是硬要求,否则比较会走偏。return 1/0:返回“放行/拦截”结果,应用层只做策略落地,不做并发判断。
3.2 Spring Service 调用脚本
@Service
public class RateLimitService {
private final StringRedisTemplate stringRedisTemplate;
private final DefaultRedisScript<Long> rateLimitScript;
public RateLimitService(StringRedisTemplate stringRedisTemplate,
DefaultRedisScript<Long> rateLimitScript) {
this.stringRedisTemplate = stringRedisTemplate;
this.rateLimitScript = rateLimitScript;
}
public boolean allow(String key, long limit, long windowSeconds) {
List<String> keys = Collections.singletonList(key);
Long result = stringRedisTemplate.execute(
rateLimitScript,
keys,
String.valueOf(limit),
String.valueOf(windowSeconds)
);
return result != null && result == 1L;
}
}
逐行解释(执行链路你得完全掌控)
StringRedisTemplate:字符串序列化最稳,避免“对象序列化版本变更”带来的线上事故。keys = singletonList(key):Lua 的 KEYS 必须是 List 传入。execute(script, keys, args...):args 会进入 Lua 的 ARGV。result == 1L:把 Redis 的判断结果转成业务布尔值,调用方体验更像“产品能力”。
4)生产级注意事项(这部分决定你是“能用”还是“好用”)🧠
| 关注点 | 建议做法 | 典型收益 |
|---|---|---|
| 原子性 | 把“判断+写入”放进 Lua | 并发一致性直接拉满 |
| 序列化 | 优先 StringRedisTemplate |
少踩坑、少返工 |
| Cluster | 多 Key 需同槽:使用 {tag}让 key 同 hash tag |
避免跨槽执行失败 |
| 脚本复杂度 | 不写大循环、不做重计算 | 防止阻塞 Redis 主线程 |
| 容错 | 可用 redis.pcall包裹易失败命令 |
降低异常扩散面 |
补充一句带点“现实主义幽默”的:Lua 脚本不是让你在 Redis 里写一套新系统的,Redis 主线程也不想加班。🙂
结论:什么时候你该果断上 Lua?
- 限流、配额、抢购、库存扣减、幂等校验、分布式锁这类“并发一致性敏感”场景:直接上 Lua。
- 你希望把业务规则“下沉为平台能力”,让应用层更轻:Lua 会让链路更像一个可复用的产品模块。✅
如果你愿意再往前走一步,我可以按你现有的业务(例如“接口级限流 + 黑白名单 + 风控分值”)把 Lua 组织成一套可版本化、可灰度、可回滚的脚本体系,真正做到“平台化交付”。
已经是第一篇啦!
下一篇: 服务器路由命令有哪些常用技巧?