自定义注解+Lua脚本实现限流

1. 固定窗口限流(推荐首选)

适用场景:接口每秒 / 每分钟最多允许 N 次请求

优点:简单、性能极高、内存占用小

缺点:窗口边界可能有瞬间突刺(大多数业务可忽略)

  • Lua脚本
java 复制代码
        // 固定窗口限流
        String luaScript =
                "local key = KEYS[1]\n" +        // 限流key
                        "local limit = tonumber(ARGV[2])\n" +  // 最大请求数
                        "local window = tonumber(ARGV[3])\n" + // 窗口大小(秒)
                        "local count = redis.call('get', key)\n" + // 获取当前计数
                        "if count and tonumber(count) >= limit then\n" +
                        "    return 0\n" +              // 超过限流,返回0
                        "end\n" +
                        "count = redis.call('incr', key)\n" +  // 计数+1
                        "if count == 1 then\n" +
                        "    redis.call('expire', key, window)\n" + // 第一次设置过期时间
                        "end\n" +
                        "return 1\n";  // 允许通过,返回1

2. 滑动窗口限流(更精准)

适用场景 :金融、支付等绝对不能超限流的场景

优点:无窗口边界突刺,限流最精准

缺点:占用稍多内存,复杂度略高

  • Lua脚本
java 复制代码
        // 滑动窗口限流
        String luaScript =
                "local key = KEYS[1]\n" +
                        "local now = tonumber(ARGV[1])\n" +
                        "local limit = tonumber(ARGV[2])\n" +
                        "local window = tonumber(ARGV[3])\n" +
                        "local window_start = now - window * 1000\n" +
                        "redis.call('ZREMRANGEBYSCORE', key, '-inf', window_start)\n" +  // 删除窗口外的旧请求
                        "local current = redis.call('ZCARD', key)\n" +  // 统计当前窗口内有多少请求
                        "if current < limit then\n" +    // 判断是否超过限流
                        "    local unique_id = ARGV[4]\n" +
                        "    redis.call('ZADD', key, now, unique_id)\n" +
                        "    redis.call('EXPIRE', key, window + 10)\n" +  //-- 自动过期,防残留
                        "    return 1\n" +  // 允许通过
                        "else\n" +
                        "    return 0\n" +  // 被限流
                        "end";
类型 Redis 存什么 数据结构 内存占用 性能 精准度
固定窗口 一个数字(计数器) String 极小 极快 够用(99% 场景)
滑动窗口 一堆时间戳 ZSet 较大 较快 极高(金融 / 支付)
  • 自定义注解
java 复制代码
/**
 * 限流注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
    String limitConfig(); // 限制次数
    String windowConfig(); // 时间窗口(单位秒)
    String key() default ""; // 限制key
}
  • 实现限流逻辑
java 复制代码
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.env.Environment;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Collections;
import java.util.UUID;

/**
 * 限流切面
 */
@Aspect
@Component
@Slf4j
public class RateLimitAspect {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private Environment environment;

    @Around("@annotation(rateLimit)")
    public Object around(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
        String key = rateLimit.key();
        if (key.isEmpty()) {
            key = joinPoint.getSignature().getName();
            log.info("限流key为空, 使用方法名作为限流key: {}", key);
        }

        // 从注解参数动态读取配置值(实际项目中应该从配置中心读取)
        int limit = environment.getProperty(rateLimit.limitConfig(), Integer.class, 3);
        int window = environment.getProperty(rateLimit.windowConfig(), Integer.class, 60);
        //int limit = ConfigService.getConfig("demo-gateway.properties")
                //.getIntProperty(rateLimit.limitConfig(), 100);
        //int window = ConfigService.getConfig("demo-gateway.properties")
                //.getIntProperty(rateLimit.windowConfig(), 60);
        
        // 固定/滑动窗口限流
        String luaScript = "lua脚本"; 

        // 生成唯一标识符
        String uniqueId = UUID.randomUUID().toString();
        long currentTime = System.currentTimeMillis();

        // 执行Lua脚本
        Long result = stringRedisTemplate.execute(
                new DefaultRedisScript<>(luaScript, Long.class),
                Collections.singletonList(key),  // KEYS[1] - 限流的key
                String.valueOf(currentTime),     // ARGV[1] - 当前时间戳(毫秒)
                String.valueOf(limit), // ARGV[2] - 最大允许的请求数
                String.valueOf(window), // ARGV[3] - 时间窗口大小(秒)
                uniqueId                         // ARGV[4] - 唯一标识
        );

        if (result == 0) {
            log.warn("接口: {} 被限流", joinPoint.getSignature().getName());
            Object[] args = joinPoint.getArgs();
            for (Object arg : args) {
                // 根据实际业务需要,触发限流修改入参
                if (arg instanceof KnowledgeBody) {
                    ((KnowledgeBody) arg).setRerank(true);
                }
            }
            // 修改参数后继续执行原方法(Controller方法)
            return joinPoint.proceed(args);
        }

        return joinPoint.proceed();
    }
}
相关推荐
弹简特2 分钟前
【Java项目-轻聊】01-项目演示+项目介绍+准备工作+项目源码
java
luck_bor18 分钟前
File类&递归作业
java·开发语言
武子康37 分钟前
Java-07 深入浅出 MyBatis数据库一对多关系模型实战:表结构设计与查询实现
java·后端
花椒技术1 小时前
企业内部 Agent 落地复盘:Gateway、Skill 和二次确认如何串起受控业务执行
后端·agent·ai编程
REDcker3 小时前
Linux OverlayFS详解
java·linux·运维
Royzst3 小时前
xml知识点
java·服务器·前端
我是一颗柠檬3 小时前
【MySQL全面教学】MySQL事务与ACID Day9(2026年)
数据库·后端·mysql
枕星而眠3 小时前
数据结构八大排序详解(一):四大简单排序
c语言·数据结构·c++·后端
IT_陈寒3 小时前
React useEffect闭包陷阱差点把我整失业了
前端·人工智能·后端
鱼鳞_3 小时前
苍穹外卖-Day08(缓存套餐)
java·redis·缓存