何为请求限流?
请求限流是一种控制API或其他Web服务的流量的技术。它的目的是限制客户端对服务器发出的请求的数量或速率,以防止服务器过载或响应时间变慢,从而提高系统的可用性和稳定性。
中小型项目请求限流的需求
- 按IP、用户、全局限流
- 基于不同实现的限流设计(基于Redis或者LRU缓存)
- 基于注解标注哪些接口限流
完整限流设计实现在开源项目中:https://github.com/valarchie/AgileBoot-Back-End
注解设计
声明一个注解类,主要有以下几个属性
- key(缓存的key)
- time(时间范围)
- maxCount(时间范围内最大的请求次数)
- limitType(按IP/用户/全局进行限流)
- cacheType(基于Redis或者Map来实现限流)
- /**
- * 限流注解
- *
- * @author valarchie
- */
- @Target(ElementType.METHOD)
- @Retention(RetentionPolicy.RUNTIME)
- @Documented
- public @interface RateLimit {
- /**
- * 限流key
- */
- String key() default "None";
- /**
- * 限流时间,单位秒
- */
- int time() default 60;
- /**
- * 限流次数
- */
- int maxCount() default 100;
- /**
- * 限流条件类型
- */
- LimitType limitType() default LimitType.GLOBAL;
- /**
- * 限流使用的缓存类型
- */
- CacheType cacheType() default CacheType.REDIS;
- }
复制代码 LimitType枚举,我们可以将不同限制类型的逻辑直接放在枚举类当中。推荐将逻辑直接放置在枚举类中,代码的组织形式会更好。- enum LimitType {
- /**
- * 默认策略全局限流 不区分IP和用户
- */
- GLOBAL{
- @Override
- public String generateCombinedKey(RateLimit rateLimiter) {
- return rateLimiter.key() + this.name();
- }
- },
- /**
- * 根据请求者IP进行限流
- */
- IP {
- @Override
- public String generateCombinedKey(RateLimit rateLimiter) {
- String clientIP = ServletUtil.getClientIP(ServletHolderUtil.getRequest());
- return rateLimiter.key() + clientIP;
- }
- },
- /**
- * 按用户限流
- */
- USER {
- @Override
- public String generateCombinedKey(RateLimit rateLimiter) {
- LoginUser loginUser = AuthenticationUtils.getLoginUser();
- if (loginUser == null) {
- throw new ApiException(ErrorCode.Client.COMMON_NO_AUTHORIZATION);
- }
- return rateLimiter.key() + loginUser.getUsername();
- }
- };
- public abstract String generateCombinedKey(RateLimit rateLimiter);
- }
-
复制代码 CacheType, 主要分为Redis和Map, 后续有新的类型可以新增。- enum CacheType {
- /**
- * 使用redis做缓存
- */
- REDIS,
- /**
- * 使用map做缓存
- */
- Map
- }
复制代码 RateLimitChecker设计
声明一个抽象类,然后将具体实现放在实现类中,便于扩展- /**
- * @author valarchie
- */
- public abstract class AbstractRateLimitChecker {
- /**
- * 检查是否超出限流
- * @param rateLimiter
- */
- public abstract void check(RateLimit rateLimiter);
- }
复制代码 Redis限流实现
- /**
- * @author valarchie
- */
- @Component
- @RequiredArgsConstructor
- @Slf4j
- public class RedisRateLimitChecker extends AbstractRateLimitChecker{
- @NonNull
- private RedisTemplate<Object, Object> redisTemplate;
- private final RedisScript<Long> limitScript = new DefaultRedisScript<>(limitScriptText(), Long.class);
- @Override
- public void check(RateLimit rateLimiter) {
- int maxCount = rateLimiter.maxCount();
- String combineKey = rateLimiter.limitType().generateCombinedKey(rateLimiter);
- Long currentCount;
- try {
- currentCount = redisTemplate.execute(limitScript, ListUtil.of(combineKey), maxCount, rateLimiter.time());
- log.info("限制请求:{}, 当前请求次数:{}, 缓存key:{}", combineKey, currentCount, rateLimiter.key());
- } catch (Exception e) {
- throw new RuntimeException("redis限流器异常,请确保redis启动正常");
- }
- if (currentCount == null) {
- throw new RuntimeException("redis限流器异常,请稍后再试");
- }
- if (currentCount.intValue() > maxCount) {
- throw new ApiException(ErrorCode.Client.COMMON_REQUEST_TOO_OFTEN);
- }
- }
- /**
- * 限流脚本
- */
- private static String limitScriptText() {
- return "local key = KEYS[1]\n" +
- "local count = tonumber(ARGV[1])\n" +
- "local time = tonumber(ARGV[2])\n" +
- "local current = redis.call('get', key);\n" +
- "if current and tonumber(current) > count then\n" +
- " return tonumber(current);\n" +
- "end\n" +
- "current = redis.call('incr', key)\n" +
- "if tonumber(current) == 1 then\n" +
- " redis.call('expire', key, time)\n" +
- "end\n" +
- "return tonumber(current);";
- }
- }
复制代码 Map + Guava RateLimiter实现
- /**
- * @author valarchie
- */
- @SuppressWarnings("UnstableApiUsage")
- @Component
- @RequiredArgsConstructor
- @Slf4j
- public class MapRateLimitChecker extends AbstractRateLimitChecker{
- /**
- * 最大仅支持4096个key 超出这个key 限流将可能失效
- */
- private final LRUCache<String, RateLimiter> cache = new LRUCache<>(4096);
- @Override
- public void check(RateLimit rateLimit) {
- String combinedKey = rateLimit.limitType().generateCombinedKey(rateLimit);
- RateLimiter rateLimiter = cache.get(combinedKey,
- () -> RateLimiter.create((double) rateLimit.maxCount() / rateLimit.time())
- );
- if (!rateLimiter.tryAcquire()) {
- throw new ApiException(ErrorCode.Client.COMMON_REQUEST_TOO_OFTEN);
- }
- log.info("限制请求key:{}, combined key:{}", rateLimit.key(), combinedKey);
- }
- }
复制代码 限流切面
我们需要在切面中,读取限流注解标注的信息,然后选择不同的限流实现来进行限流。- /**
- * 限流切面处理
- *
- * @author valarchie
- */
- @Aspect
- @Component
- @Slf4j
- @ConditionalOnExpression("'${agileboot.embedded.redis}' != 'true'")
- @RequiredArgsConstructor
- public class RateLimiterAspect {
- @NonNull
- private RedisRateLimitChecker redisRateLimitChecker;
- @NonNull
- private MapRateLimitChecker mapRateLimitChecker;
- @Before("@annotation(rateLimiter)")
- public void doBefore(JoinPoint point, RateLimit rateLimiter) {
- MethodSignature signature = (MethodSignature) point.getSignature();
- Method method = signature.getMethod();
- log.info("当前限流方法:" + method.toGenericString());
- switch (rateLimiter.cacheType()) {
- case REDIS:
- redisRateLimitChecker.check(rateLimiter);
- break;
- case Map:
- mapRateLimitChecker.check(rateLimiter);
- return;
- default:
- redisRateLimitChecker.check(rateLimiter);
- }
- }
- }
复制代码 注解使用
以下是我们标注的注解例子。
time=10,maxCount=10表明10秒内最多10次请求。
cacheType=Redis表明使用Redis来实现。
limitType=IP表明基于IP来限流。- /**
- * 生成验证码
- */
- @Operation(summary = "验证码")
- @RateLimit(key = RateLimitKey.LOGIN_CAPTCHA_KEY, time = 10, maxCount = 10, cacheType = CacheType.REDIS,
- limitType = LimitType.IP)
- @GetMapping("/captchaImage")
- public ResponseDTO<CaptchaDTO> getCaptchaImg() {
- CaptchaDTO captchaImg = loginService.generateCaptchaImg();
- return ResponseDTO.ok(captchaImg);
- }
复制代码 这是笔者关于中小型项目关于请求限流的实现,如有不足欢迎大家评论指正。
全栈技术交流群:1398880
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |