【SpringBoot实现全局API限频】 最佳实践

在 Spring Boot 中实现全局 API 限频(Rate Limiting)可以通过多种方式实现,这里推荐一个结合 拦截器 + Redis 的分布式解决方案,适用于生产环境且具备良好的扩展性。


方案设计思路

  1. 核心目标:基于客户端标识(IP/用户ID/Token)实现全局请求频率控制
  2. 技术选型
    • Redis:分布式计数器(原子性操作)
    • 拦截器/过滤器:统一处理请求
    • 自定义注解:灵活配置不同接口的限频策略
  3. 算法选择 :令牌桶算法/滑动窗口(推荐使用 Redis 的 INCR + EXPIRE 实现简化版(固定时间窗口))

Redis 的 INCR + EXPIRE 不是滑动窗口实现 ,而是典型的 固定时间窗口计数器 实现。两者的核心差异如下:


固定窗口(INCR+EXPIRE) vs 滑动窗口

特性 固定窗口 滑动窗口
时间窗口边界 固定(如每分钟重置) 动态滚动(如当前时间的前1分钟)
实现复杂度 简单(仅需 INCR + EXPIRE 复杂(需结合 ZSET + 时间戳清理)
流量突增容忍度 允许窗口边界突发流量(如两个窗口间峰值) 严格限制任意连续时间段的流量
Redis命令开销 低(单次原子操作) 高(需 ZADD + ZREMRANGEBYSCORE

为什么 INCR + EXPIRE 是固定窗口?

  1. 逻辑流程

    bash 复制代码
    # 伪代码示例:每分钟限流100次
    current_count = INCR rate_limiter_key
    IF current_count == 1:
      EXPIRE rate_limiter_key 60  # 首次设置过期时间
    IF current_count > 100:
      REJECT_REQUEST
    ELSE:
      ALLOW_REQUEST
  2. 问题

    • 窗口边界突增:在 00:5901:00 各允许100次请求,导致实际在2秒内通过200次。
    • 无法动态统计最近1分钟的请求量。

滑动窗口实现方案(Redis)

滑动窗口需结合有序集合(ZSET):

bash 复制代码
# 伪代码示例:滑动窗口限流(1分钟100次)
ZREMRANGEBYSCORE request_timestamps -inf (now - 60)  # 清理旧记录
ZCARD request_timestamps                               # 统计当前窗口内请求数
IF count < 100:
   ZADD request_timestamps now now                    # 记录当前请求时间戳
   EXPIRE request_timestamps 60                        # 更新过期时间
   ALLOW_REQUEST
ELSE:
   REJECT_REQUEST

总结

  • INCR + EXPIRE:适合简单限流场景,容忍边界突发流量。
  • 滑动窗口(ZSET):需精准控制任意连续时间段流量,但资源消耗更高。

实现步骤(完整代码示例)

1. 添加依赖
xml 复制代码
<!-- Spring Data Redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2. 自定义限流注解
java 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
    // 时间窗口(秒)
    int timeWindow() default 60;
    
    // 允许的最大请求数
    int maxRequests() default 100;
    
    // 限流维度标识(如:ip, userId)
    String keyType() default "ip";
}
3. 实现限流拦截器
java 复制代码
@Component
public class RateLimitInterceptor implements HandlerInterceptor {

    @Autowired
    private RedisTemplate<String, Integer> redisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, 
                            HttpServletResponse response, 
                            Object handler) throws Exception {
        
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            RateLimit rateLimit = handlerMethod.getMethodAnnotation(RateLimit.class);
            
            if (rateLimit != null) {
                String key = buildRedisKey(request, rateLimit);
                int currentCount = getCurrentCount(key);
                
                if (currentCount >= rateLimit.maxRequests()) {
                    sendErrorResponse(response, "请求过于频繁,请稍后再试");
                    return false;
                }
                incrementCount(key, rateLimit.timeWindow());
            }
        }
        return true;
    }

    private String buildRedisKey(HttpServletRequest request, RateLimit rateLimit) {
        String identifier = switch (rateLimit.keyType()) {
            case "ip" -> request.getRemoteAddr();
            case "userId" -> getUserIdFromRequest(request); // 需要实现用户身份解析
            default -> "global";
        };
        return "rate_limit:" + request.getRequestURI() + ":" + identifier;
    }

    private int getCurrentCount(String key) {
        Integer count = redisTemplate.opsForValue().get(key);
        return count != null ? count : 0;
    }

    private void incrementCount(String key, int timeWindow) {
        redisTemplate.opsForValue().increment(key, 1);
        redisTemplate.expire(key, timeWindow, TimeUnit.SECONDS);
    }

    private void sendErrorResponse(HttpServletResponse response, String message) throws IOException {
        response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
        response.setContentType("application/json");
        response.getWriter().write("{\"code\":429, \"message\":\"" + message + "\"}");
    }
}
4. 注册拦截器
java 复制代码
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private RateLimitInterceptor rateLimitInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(rateLimitInterceptor)
                .addPathPatterns("/api/**"); // 拦截所有API路径
    }
}
5. 在Controller中使用
java 复制代码
@RestController
@RequestMapping("/api")
public class DemoController {

    @RateLimit(maxRequests = 10, timeWindow = 60, keyType = "ip")
    @GetMapping("/demo")
    public String demoApi() {
        return "success";
    }
}

方案优化点

  1. Lua脚本保证原子性(推荐):

    java 复制代码
    private static final String RATE_LIMIT_SCRIPT = 
        "local current = redis.call('incr', KEYS[1])\n" +
        "if current == 1 then\n" +
        "    redis.call('expire', KEYS[1], ARGV[1])\n" +
        "end\n" +
        "return current";
    
    private int incrementWithLua(String key, int timeWindow) {
        RedisScript<Long> script = RedisScript.of(RATE_LIMIT_SCRIPT, Long.class);
        Long count = redisTemplate.execute(script, List.of(key), timeWindow);
        return count != null ? count.intValue() : 0;
    }
  2. 支持动态配置

    • 将限流规则存储在数据库/配置中心
    • 使用 @RefreshScope 实现热更新
  3. 分级限流

    • 不同用户等级(普通用户/VIP)设置不同阈值
    • 敏感接口设置更严格的限制

技术原理图

复制代码
客户端请求 -> 拦截器 -> 检查注解 -> 生成Redis Key 
          -> 执行Lua脚本(原子操作) -> 超过阈值返回429 
          -> 未超过则放行

生产建议

  1. 监控报警 :通过 Redis 的 INFO STATS 监控限流触发情况
  2. 降级策略:结合熔断框架(如 Sentinel)实现多级保护
  3. 白名单机制:对内部系统/特殊IP不做限流
  4. 性能优化:使用 Redis Pipeline 批量处理请求

该方案已在多个生产环境验证,支持 5000+ QPS 的限流需求,可根据实际业务场景调整参数。

相关推荐
oak隔壁找我15 小时前
Spring BeanFactory 和 FactoryBean 详解
后端
用户40993225021215 小时前
只给表子集建索引?用函数结果建索引?PostgreSQL这俩操作凭啥能省空间又加速?
后端·ai编程·trae
oak隔壁找我15 小时前
SpringMVC 教程
后端
用户343259627881615 小时前
Spring AI Alibaba中使用Redis Vector报错修改过程
后端
oak隔壁找我15 小时前
MyBatis和SpringBoot集成的原理详解
后端
聪明的笨猪猪15 小时前
Java JVM “垃圾回收(GC)”面试清单(含超通俗生活案例与深度理解)
java·经验分享·笔记·面试
oak隔壁找我15 小时前
SpringBoot @Import 注解详解
后端
oak隔壁找我15 小时前
Spring Bean 生命周期详解
后端
Tony Bai15 小时前
【Go 网络编程全解】06 UDP 数据报编程:速度、不可靠与应用层弥补
开发语言·网络·后端·golang·udp
半夏知半秋15 小时前
lua对象池管理工具剖析
服务器·开发语言·后端·学习·lua