# rate-limiter-spring-boot-starter **Repository Path**: library-components/rate-limiter-spring-boot-starter ## Basic Information - **Project Name**: rate-limiter-spring-boot-starter - **Description**: No description available - **Primary Language**: Java - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-09-07 - **Last Updated**: 2025-09-07 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # rate-limiter-spring-boot-starter 一个开箱即用的 **Redis 固定窗口计数** 限流 Starter,适配 **Spring Boot 2.7.x(Framework 5.3.x,`javax.servlet.*` 命名空间)**。 支持 **SpEL Key**、全局/注解双开关、Lua 原子计数、SPI 扩展拒绝回调;**不强制传递 Redis/Web 依赖**,由业务方自行控制依赖粒度。 --- ## ✨ 特性 * `@RateLimiter` 注解式接入,**支持 SpEL**(`#userId`、`#p0`、`#request.remoteAddr` 等) * **双开关**:全局开关(yml)+ 注解局部开关(`enabled`) * **Redis + Lua** 原子计数(首调用设置过期) * **yml 可配** 默认窗口与阈值,注解可覆盖 * **SPI**:`RateLimitDeniedHandler`,自定义拒绝行为(报警/埋点/metrics) * 统一异常 `TooManyRequestsException`,便于全局返回 **HTTP 429** * **依赖可选**:Starter 不强推 `spring-boot-starter-data-redis` / `spring-boot-starter-web` --- ## 适配环境 * Spring Boot **2.7.3** * Spring Framework **5.3.22**(由 BOM 管理) * JDK **8+** * Redis **5+**(单机/哨兵/Cluster) > 若你的工程是 Spring Boot 3.x(`jakarta.*`),请使用相应分支/版本。 --- ## 1) 引入依赖(业务应用) > 由于 Starter 将 Redis/Web 依赖标记为 **optional/provided**,**不会传递**到你的应用。请在业务应用中显式添加如下依赖。 ### Maven ```xml io.limiter rate-limiter-spring-boot-starter 1.0.0 org.springframework.boot spring-boot-starter-data-redis org.springframework spring-web javax.servlet javax.servlet-api 4.0.1 provided ``` ### Gradle ```gradle implementation "io.limiter:rate-limiter-spring-boot-starter:1.0.0" implementation "org.springframework.boot:spring-boot-starter-data-redis" implementation "org.springframework:spring-web" compileOnly "javax.servlet:javax.servlet-api:4.0.1" ``` --- ## 2) 基础配置 `application.yml` ```yaml stall: limiter: enabled: true # 全局开关(总闸) default-window-seconds: 60 # 全局默认窗口(秒) default-count: 5 # 全局默认阈值 key-prefix: rl # Redis Key 前缀 spring: redis: host: 127.0.0.1 port: 6379 ``` --- ## 3) 注解使用示例 ```java @RestController @RequestMapping("/api/demo") public class DemoController { // 使用全局默认值:窗口60s、阈值5;Key=类.方法:clientIp @GetMapping("/hello") @RateLimiter public String hello() { return "ok"; } // 使用 SpEL:按用户ID维度;窗口10s,阈值3 @GetMapping("/user/{userId}") @RateLimiter(key = "#userId", time = 10, count = 3) public String byUser(@PathVariable Long userId) { return "ok-" + userId; } // 注解局部开关:临时关闭该接口限流(全局开着也不会限) @GetMapping("/free") @RateLimiter(enabled = false) public String freePass() { return "no-limit"; } } ``` ### 注解签名 ```java @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RateLimiter { /** SpEL(#userId/#p0/#a0/#request.remoteAddr);空则=类.方法:clientIp */ String key() default ""; /** 窗口秒数(<=0 使用全局默认) */ int time() default -1; /** 允许请求数(<=0 使用全局默认) */ int count() default -1; /** 注解局部开关(默认 true) */ boolean enabled() default true; } ``` --- ## 4) 统一返回 HTTP 429(可选) ```java @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(TooManyRequestsException.class) @ResponseStatus(HttpStatus.TOO_MANY_REQUESTS) public Map handle429(TooManyRequestsException ex) { Map body = new HashMap<>(); body.put("code", 429); body.put("msg", ex.getMessage()); body.put("timestamp", System.currentTimeMillis()); return body; } } ``` --- ## 5) SPI:拒绝回调(埋点/报警) ```java @Configuration public class LimiterExtensions { @Bean public RateLimitDeniedHandler myDeniedHandler() { return (key, window, limit) -> { // 在这里做日志/指标/报警,比如: // meterRegistry.counter("rate_limit.denied", "key", key).increment(); }; } } ``` 接口定义: ```java @FunctionalInterface public interface RateLimitDeniedHandler { void onDenied(String key, int windowSeconds, int limit); } ``` --- ## 6) 工作原理 * **固定窗口计数**:在窗口内请求数超过阈值则拒绝 * **Lua 原子脚本**:`GET → 判断 → INCR → 首次 EXPIRE` * **切面优先级**:`@Order(0)` 尽早在链路前端拒绝,降低系统开销 * **Key 设计**:默认 `keyPrefix:类.方法:clientIp`;支持 SpEL 自定义 --- ## 7) 配置项一览 | 配置项 | 类型 | 默认值 | 说明 | | -------------------------------------- | ------- | -----: | ------------ | | `stall.limiter.enabled` | boolean | `true` | **全局开关**(总闸) | | `stall.limiter.default-window-seconds` | int | `60` | 全局默认窗口 | | `stall.limiter.default-count` | int | `5` | 全局默认阈值 | | `stall.limiter.key-prefix` | string | `rl` | Redis Key 前缀 | > 优先级:**注解值 > 全局默认**。当 `enabled=false` 时该方法**不参与限流**。 --- ## 8) 常见问题(FAQ) **Q1:`Cannot resolve symbol 'HttpServletRequest'`?** A:你没有显式添加 `javax.servlet-api:4.0.1`(`provided`/`compileOnly`)。同时确认包名是 `javax.servlet.*`(Boot 2.x),不是 `jakarta.*`。 **Q2:引入 Starter 后仍不生效?** * 确认 `stall.limiter.enabled=true`(默认已启用)。 * 业务应用是否显式引入了 `spring-boot-starter-data-redis`。 * Redis 是否可达,`spring.redis.*` 已配置。 * 目标方法是否标注了 `@RateLimiter`。 * 没有过早在网关/Nginx 层被拦截。 **Q3:不用 `spring-boot-starter-web` 可以吗?** 可以。Starter 仅用到 `spring-web`(工具类)与 `javax.servlet-api`(编译期)。 如果你希望 **零 Servlet 依赖**,可自定义 Key 解析(见下一条)。 **Q4:如何做到“零 Servlet 依赖”?** 把 Key 解析抽成自定义 Resolver(业务侧提供 Bean),比如基于 ThreadLocal、JWT Claim 或 RPC 元数据;Starter 仅做限流计数,不读取 `HttpServletRequest`。 **Q5:支持 WebFlux 吗?** 计数核心无关 Web 技术栈;默认 `IpResolver` 基于 Servlet。WebFlux 下建议在网关层限流或自定义 Key 解析。 --- ## 9) 性能与实践建议 * 热点接口压测后**适当放大窗口与阈值**,减少每秒脚本执行次数 * Key 尽量**短且稳定**,避免包含大对象字符串 * 结合 Nginx/Ingress **前置粗限流**,应用内细化限流 * 关注 Redis 连接池/超时,生产建议使用哨兵或 Cluster --- ## 10) 版本矩阵 | 组件 | 版本 | | ---------------- | ----------------------------------- | | Spring Boot | 2.7.3 | | Spring Framework | 5.3.22(BOM 管理) | | Servlet API | `javax.servlet-api:4.0.1`(provided) | | JDK | 8+ | | Redis | 5+ | --- ## 11) 变更日志 * **1.0.0** * 初始版本:固定窗口(Redis+Lua) * SpEL Key、全局/注解双开关、SPI 回调、统一 429 异常 * **依赖可选化**:Redis/Web 由业务应用显式控制 --- ## 12) 许可证 (根据你的组织策略选择并声明:MIT / Apache-2.0 / …) --- ## 13) 贡献 欢迎 PR / Issue: * 滑动窗口/令牌桶策略 * Micrometer 指标 * WebFlux KeyResolver 示例 * 生产最佳实践与性能调优案例