本次任务的目标是:实现一套方法级的接口限流,支持全局,IP,用户多个维度,防止知识库问答,文件上传等高成本被频繁调用。

技术栈:

Spring Boot4.0

Spring AOP(AspectJ)

Redisson + Redis Lua脚本

一.整体思路

限流分成三部分:注解负责声明规则,切面负责拦截执行,Lua脚本负责计算判断。

注解:贴在接口方法上,写明限多少次,按什么维度限,本身不含逻辑。

切面:拦截所有带注解的方法,取出规则去Redis判断,通过就放行,不通过就拒绝或降级。

Lua脚本:把计数的检查和扣减放在一起原子执行。

二.核心代码实现

1.自定义@RateLimit注解,用@Repeatable支持在一个方法上声明多条规则:

@Target(ElementType.METHOD)
@Rentention(RetentionPolicy.RUNTIME)
@Repeatable(RateLimit.Container.class)
public @interface RateLimit {
    enum Dimension { GLOBAL, IP, USER }
    Dimension dimension() default Dimension.GLOBAL;
    double count();                            // 窗口内允许的最大次数
    long interval() default 1;                 // 时间窗口大小
    TimeUnit timeUnit() default TimeUnit.SECONDS;
    String fallback() default "";              // 降级方法
}

使用时一个方法可以挂多条,全部通过才放行:

@RateLimit(dimension = Dimension.GLOBAL, count = 10)
@RateLimit(dimension = Dimension.IP, count = 5)
public Result<QueryResponse> queryKnowledgeBase(...) { ... }

2.AOP切面拦截带注解的方法,逐条规则校验,任意一条不通过就拒绝

@Around("@annotation(...RateLimit) || @annotation(...RateLimit.Container)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    RateLimit[] rules = method.getAnnotationsByType(RateLimit.class);
    for (RateLimit rule : rules) {
        String key = generateKey(className, methodName, rule.dimension());
        Long ok = executeRateLimitScript(key, ...);
        if (ok == null || ok == 0) {
            return handleRateLimitExceeded(joinPoint, rule, key);
        }
    }
    return joinPoint.proceed();
}

3.用Redis Lua脚本做计数判断,基于滑动窗口

-- 当前剩余次数(不存在则取最大值)
local current_val = tonumber(redis.call("get", value_key)) or max_tokens
-- 回收已经超过时间窗口的次数
local expired = redis.call("zrangebyscore", permits_key, 0, now_ms - interval)
-- 累加回收次数、删除过期记录、current_val 加回
if current_val < permits then return 0 end
redis.call("zadd", permits_key, now_ms, request_id .. ":" .. permits)
redis.call("set", value_key, current_val - permits)
return 1

三.几个关键点

为什么用Lua脚本:如果先查剩余次数,再扣减,是两步操作,高并发下两个请求可能同时查到“还剩一次”都放行,导致超限。Lua脚本再Redis里是原子执行的,把检查和扣减放在一个脚本就不会有这个问题。

为什么用滑动窗口:固定窗口(整秒清零)在临界点会翻倍,比如上一秒末尾和下一秒开头各来一批,短时间内实际放行可能是阈值的两倍。滑动窗口按时间戳记录每次请求,下次回收过期的,窗口随时间平滑移动,避免这个问题。

Key 用 Hash Tag:限流 Key 形如 ratelimit:{类名:方法名}:ip:1.2.3.4,用大括号把类名方法名包起来,保证同一方法的多个维度 Key 在 Redis 集群下落到同一槽位,否则 Lua 操作多个 Key 会报 CROSSSLOT。

NOSCRIPT 处理:脚本用 sha 调用,Redis 重启后缓存会丢,切面里捕获 NOSCRIPT 异常后重新加载脚本再执行一次。

四.总结

步骤 实现方式
声明规则 @RateLimit注解
多维度限流 可重复注解,GLOBAL / IP / USER
拦截执行 AOP 切面
原子计数 Redis Lua 脚本
避免临界翻倍 滑动窗口(按时间戳记录 + 回收)
集群兼容 Key 使用 Hash Tag

整套限流做成了贴注解即生效,对业务代码无侵入的通用组件。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐