服务公告

服务公告 > Linux命令 > 在Springboot项目中使用Redis提供给Lua的脚本

在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 组织成一套可版本化、可灰度、可回滚的脚本体系,真正做到“平台化交付”。

已经是第一篇啦!

下一篇: 服务器路由命令有哪些常用技巧?