后端_基于注解实现的请求限流
前言
在高并发场景下,恶意请求或突发流量可能导致系统过载,请求限流(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设置过期时间(与注解的时间窗口一致),自动清理过期计数。
核心原理图示
请求 → AOP切面拦截 → 生成限流Key → Redis计数检查├─ 未超限 → 计数+1 → 执行方法└─ 已超限 → 返回429错误
3、实战案例
3.1 基础使用:全局限流
限制 /user/create
接口每分钟最多10次请求(所有用户共享):
@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次修改个人信息:
@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次登录尝试(防暴力破解):
@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次:
@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 限流效果
当请求超限后,接口返回标准化错误:
{"code": 429,"data": null,"msg": "请求过于频繁,请稍后重试"
}
4、核心代码解析
4.1 AOP切面实现(RateLimiterAspect)
/*** 拦截声明了 {@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");// 解析 KeyString 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)
/*** 限流 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/lcR0WrateLimiter.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/lcR0WrateLimiter.expire(duration);return rateLimiter;}}
5、总结
基于 @RateLimiter
注解的限流方案具有以下优势:
- 易用性:通过注解声明限流规则,无需编写复杂逻辑。
- 灵活性:支持全局、用户、IP等多维度限流,满足不同场景。
- 高性能:基于Redis原子操作,性能损耗低,支持分布式系统。
实际使用时,需根据业务场景合理设置 count
和 timeUnit
(如高频接口设为秒级,低频接口设为分钟级),避免过度限流影响用户体验。