前言
在高并发场景下,恶意请求或突发流量可能导致系统过载,请求限流(Rate Limiting)通过控制单位时间内的请求次数,保护系统稳定运行。本文介绍声明式限流的实现原理与实战案例。
1、核心概念与注解说明
请求限流的核心是限制单位时间内的请求次数 ,通过 @RateLimiter
注解实现声明式限流,无需侵入业务代码。
1.1 @RateLimiter 注解参数
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
count |
int | 单位时间内允许的最大请求次数 | 必传 |
time |
int | 时间窗口大小(单位由 timeUnit 指定) |
1 |
timeUnit |
TimeUnit | 时间单位(如分钟、秒) | TimeUnit.SECONDS |
keyResolver |
Class<? extends KeyResolver> | 限流维度解析器(如全局、用户ID、IP) | DefaultRateLimiterKeyResolver |
keyArg |
String | 自定义限流Key的Spring EL表达式(配合ExpressionIdempotentKeyResolver使用) | 空 |
1.2 限流维度解析器
支持多种限流粒度,通过 keyResolver
指定:
解析器类名 | 限流维度 | 适用场景 |
---|---|---|
DefaultRateLimiterKeyResolver | 全局级别 | 限制接口总请求量(如全局限 100次/分) |
UserRateLimiterKeyResolver | 用户ID级别 | 按用户限制(如单用户 10次/分) |
ClientIpRateLimiterKeyResolver | 客户端IP级别 | 按IP限制(如单IP 5次/分) |
ServerNodeRateLimiterKeyResolver | 服务器节点级别 | 集群中单个节点的请求限制 |
ExpressionIdempotentKeyResolver | 自定义表达式 | 复杂维度(如 #user.id + '-' + #type ) |
2、实现原理
限流功能基于 AOP 切面 + Redis 计数器 实现,核心流程如下:
- 拦截请求 :通过
RateLimiterAspect
切面拦截被@RateLimiter
注解标记的方法。 - 生成限流Key :根据
keyResolver
解析限流维度(如用户ID),结合方法签名生成唯一Redis Key。 - 判断是否超限 :
- 若未超限:Redis计数器+1,允许请求执行。
- 若已超限:直接返回限流错误。
- 自动过期:Redis Key设置过期时间(与注解的时间窗口一致),自动清理过期计数。
核心原理图示
plain
请求 → AOP切面拦截 → 生成限流Key → Redis计数检查
├─ 未超限 → 计数+1 → 执行方法
└─ 已超限 → 返回429错误
3、实战案例
3.1 基础使用:全局限流
限制 /user/create
接口每分钟最多10次请求(所有用户共享):
java
@RestController
public class UserController {
@PostMapping("/user/create")
// 每分钟最多10次请求(全局维度)
@RateLimiter(count = 10, timeUnit = TimeUnit.MINUTES)
public String createUser(@RequestBody User user) {
userService.createUser(user); // 业务逻辑
return "用户创建成功";
}
}
3.2 进阶使用:按用户/IP限流
案例1:按用户ID限流
限制单个用户每小时最多5次修改个人信息:
java
@PostMapping("/user/update")
// 单用户每小时最多5次请求(用户ID维度)
@RateLimiter(
count = 5,
time = 1,
timeUnit = TimeUnit.HOURS,
keyResolver = UserRateLimiterKeyResolver.class
)
public String updateUser(@RequestBody User user) {
userService.updateUser(user);
return "用户更新成功";
}
案例2:按IP限流
限制单个IP每分钟最多3次登录尝试(防暴力破解):
java
@PostMapping("/login")
// 单IP每分钟最多3次请求(IP维度)
@RateLimiter(
count = 3,
timeUnit = TimeUnit.MINUTES,
keyResolver = ClientIpRateLimiterKeyResolver.class
)
public String login(@RequestParam String username, @RequestParam String password) {
return userService.login(username, password);
}
案例3:自定义维度限流
限制同一用户对同一商品的查询,每秒最多2次:
java
@GetMapping("/product/query")
// 自定义Key:用户ID+商品ID,每秒最多2次
@RateLimiter(
count = 2,
timeUnit = TimeUnit.SECONDS,
keyResolver = ExpressionIdempotentKeyResolver.class,
keyArg = "#userId + '-' + #productId" // Spring EL表达式
)
public Product queryProduct(Long userId, Long productId) {
return productService.getById(productId);
}
3.3 限流效果
当请求超限后,接口返回标准化错误:
json
{
"code": 429,
"data": null,
"msg": "请求过于频繁,请稍后重试"
}
4、核心代码解析
4.1 AOP切面实现(RateLimiterAspect)
java
/**
* 拦截声明了 {@link RateLimiter} 注解的方法,实现限流操作
*
*/
@Aspect
@Slf4j
public class RateLimiterAspect {
/**
* RateLimiterKeyResolver 集合
*/
private final Map<Class<? extends RateLimiterKeyResolver>, RateLimiterKeyResolver> keyResolvers;
private final RateLimiterRedisDAO rateLimiterRedisDAO;
public RateLimiterAspect(List<RateLimiterKeyResolver> keyResolvers, RateLimiterRedisDAO rateLimiterRedisDAO) {
this.keyResolvers = CollectionUtils.convertMap(keyResolvers, RateLimiterKeyResolver::getClass);
this.rateLimiterRedisDAO = rateLimiterRedisDAO;
}
@Before("@annotation(rateLimiter)")
public void beforePointCut(JoinPoint joinPoint, RateLimiter rateLimiter) {
// 获得 IdempotentKeyResolver 对象
RateLimiterKeyResolver keyResolver = keyResolvers.get(rateLimiter.keyResolver());
Assert.notNull(keyResolver, "找不到对应的 RateLimiterKeyResolver");
// 解析 Key
String key = keyResolver.resolver(joinPoint, rateLimiter);
// 获取 1 次限流
boolean success = rateLimiterRedisDAO.tryAcquire(key,
rateLimiter.count(), rateLimiter.time(), rateLimiter.timeUnit());
if (!success) {
log.info("[beforePointCut][方法({}) 参数({}) 请求过于频繁]", joinPoint.getSignature().toString(), joinPoint.getArgs());
String message = StrUtil.blankToDefault(rateLimiter.message(),
GlobalErrorCodeConstants.TOO_MANY_REQUESTS.getMsg());
throw new ServiceException(GlobalErrorCodeConstants.TOO_MANY_REQUESTS.getCode(), message);
}
}
}
4.2 Redis计数实现(IdempotentRedisDAO)
java
/**
* 限流 Redis DAO
*/
@AllArgsConstructor
public class RateLimiterRedisDAO {
/**
* 限流操作
*
* KEY 格式:rate_limiter:%s // 参数为 uuid
* VALUE 格式:String
* 过期时间:不固定
*/
private static final String RATE_LIMITER = "rate_limiter:%s";
private final RedissonClient redissonClient;
public Boolean tryAcquire(String key, int count, int time, TimeUnit timeUnit) {
// 1. 获得 RRateLimiter,并设置 rate 速率
RRateLimiter rateLimiter = getRRateLimiter(key, count, time, timeUnit);
// 2. 尝试获取 1 个
return rateLimiter.tryAcquire();
}
private static String formatKey(String key) {
return String.format(RATE_LIMITER, key);
}
private RRateLimiter getRRateLimiter(String key, long count, int time, TimeUnit timeUnit) {
String redisKey = formatKey(key);
RRateLimiter rateLimiter = redissonClient.getRateLimiter(redisKey);
long rateInterval = timeUnit.toSeconds(time);
Duration duration = Duration.ofSeconds(rateInterval);
// 1. 如果不存在,设置 rate 速率
RateLimiterConfig config = rateLimiter.getConfig();
if (config == null) {
rateLimiter.trySetRate(RateType.OVERALL, count, duration);
// 原因参见 https://t.zsxq.com/lcR0W
rateLimiter.expire(duration);
return rateLimiter;
}
// 2. 如果存在,并且配置相同,则直接返回
if (config.getRateType() == RateType.OVERALL
&& Objects.equals(config.getRate(), count)
&& Objects.equals(config.getRateInterval(), TimeUnit.SECONDS.toMillis(rateInterval))) {
return rateLimiter;
}
// 3. 如果存在,并且配置不同,则进行新建
rateLimiter.setRate(RateType.OVERALL, count, duration);
// 原因参见 https://t.zsxq.com/lcR0W
rateLimiter.expire(duration);
return rateLimiter;
}
}
5、总结
基于 @RateLimiter
注解的限流方案具有以下优势:
- 易用性:通过注解声明限流规则,无需编写复杂逻辑。
- 灵活性:支持全局、用户、IP等多维度限流,满足不同场景。
- 高性能:基于Redis原子操作,性能损耗低,支持分布式系统。
实际使用时,需根据业务场景合理设置 count
和 timeUnit
(如高频接口设为秒级,低频接口设为分钟级),避免过度限流影响用户体验。