# 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 示例
* 生产最佳实践与性能调优案例