中小型项目请求限流设计

打印 上一主题 下一主题

主题 914|帖子 914|积分 2752

何为请求限流?

请求限流是一种控制API或其他Web服务的流量的技术。它的目的是限制客户端对服务器发出的请求的数量或速率,以防止服务器过载或响应时间变慢,从而提高系统的可用性和稳定性。
中小型项目请求限流的需求


  • 按IP、用户、全局限流
  • 基于不同实现的限流设计(基于Redis或者LRU缓存)
  • 基于注解标注哪些接口限流
完整限流设计实现在开源项目中:https://github.com/valarchie/AgileBoot-Back-End
注解设计

声明一个注解类,主要有以下几个属性

  • key(缓存的key)
  • time(时间范围)
  • maxCount(时间范围内最大的请求次数)
  • limitType(按IP/用户/全局进行限流)
  • cacheType(基于Redis或者Map来实现限流)
  1. /**
  2. * 限流注解
  3. *
  4. * @author valarchie
  5. */
  6. @Target(ElementType.METHOD)
  7. @Retention(RetentionPolicy.RUNTIME)
  8. @Documented
  9. public @interface RateLimit {
  10.     /**
  11.      * 限流key
  12.      */
  13.     String key() default "None";
  14.     /**
  15.      * 限流时间,单位秒
  16.      */
  17.     int time() default 60;
  18.     /**
  19.      * 限流次数
  20.      */
  21.     int maxCount() default 100;
  22.     /**
  23.      * 限流条件类型
  24.      */
  25.     LimitType limitType() default LimitType.GLOBAL;
  26.     /**
  27.      * 限流使用的缓存类型
  28.      */
  29.     CacheType cacheType() default CacheType.REDIS;
  30. }
复制代码
LimitType枚举,我们可以将不同限制类型的逻辑直接放在枚举类当中。推荐将逻辑直接放置在枚举类中,代码的组织形式会更好。
  1. enum LimitType {
  2.         /**
  3.          * 默认策略全局限流  不区分IP和用户
  4.          */
  5.         GLOBAL{
  6.             @Override
  7.             public String generateCombinedKey(RateLimit rateLimiter) {
  8.                 return rateLimiter.key() + this.name();
  9.             }
  10.         },
  11.         /**
  12.          * 根据请求者IP进行限流
  13.          */
  14.         IP {
  15.             @Override
  16.             public String generateCombinedKey(RateLimit rateLimiter) {
  17.                 String clientIP = ServletUtil.getClientIP(ServletHolderUtil.getRequest());
  18.                 return rateLimiter.key() + clientIP;
  19.             }
  20.         },
  21.         /**
  22.          * 按用户限流
  23.          */
  24.         USER {
  25.             @Override
  26.             public String generateCombinedKey(RateLimit rateLimiter) {
  27.                 LoginUser loginUser = AuthenticationUtils.getLoginUser();
  28.                 if (loginUser == null) {
  29.                     throw new ApiException(ErrorCode.Client.COMMON_NO_AUTHORIZATION);
  30.                 }
  31.                 return rateLimiter.key() + loginUser.getUsername();
  32.             }
  33.         };
  34.         public abstract String generateCombinedKey(RateLimit rateLimiter);
  35.     }
  36.    
复制代码
CacheType, 主要分为Redis和Map, 后续有新的类型可以新增。
  1.   enum CacheType {
  2.       /**
  3.        * 使用redis做缓存
  4.        */
  5.       REDIS,
  6.       /**
  7.        * 使用map做缓存
  8.        */
  9.       Map
  10.   }
复制代码
RateLimitChecker设计

声明一个抽象类,然后将具体实现放在实现类中,便于扩展
  1. /**
  2. * @author valarchie
  3. */
  4. public abstract class AbstractRateLimitChecker {
  5.     /**
  6.      * 检查是否超出限流
  7.      * @param rateLimiter
  8.      */
  9.     public abstract void check(RateLimit rateLimiter);
  10. }
复制代码
Redis限流实现
  1. /**
  2. * @author valarchie
  3. */
  4. @Component
  5. @RequiredArgsConstructor
  6. @Slf4j
  7. public class RedisRateLimitChecker extends AbstractRateLimitChecker{
  8.     @NonNull
  9.     private RedisTemplate<Object, Object> redisTemplate;
  10.     private final RedisScript<Long> limitScript = new DefaultRedisScript<>(limitScriptText(), Long.class);
  11.     @Override
  12.     public void check(RateLimit rateLimiter) {
  13.         int maxCount = rateLimiter.maxCount();
  14.         String combineKey = rateLimiter.limitType().generateCombinedKey(rateLimiter);
  15.         Long currentCount;
  16.         try {
  17.             currentCount = redisTemplate.execute(limitScript, ListUtil.of(combineKey), maxCount, rateLimiter.time());
  18.             log.info("限制请求:{}, 当前请求次数:{}, 缓存key:{}", combineKey, currentCount, rateLimiter.key());
  19.         } catch (Exception e) {
  20.             throw new RuntimeException("redis限流器异常,请确保redis启动正常");
  21.         }
  22.         if (currentCount == null) {
  23.             throw new RuntimeException("redis限流器异常,请稍后再试");
  24.         }
  25.         if (currentCount.intValue() > maxCount) {
  26.             throw new ApiException(ErrorCode.Client.COMMON_REQUEST_TOO_OFTEN);
  27.         }
  28.     }
  29.     /**
  30.      * 限流脚本
  31.      */
  32.     private static String limitScriptText() {
  33.         return "local key = KEYS[1]\n" +
  34.             "local count = tonumber(ARGV[1])\n" +
  35.             "local time = tonumber(ARGV[2])\n" +
  36.             "local current = redis.call('get', key);\n" +
  37.             "if current and tonumber(current) > count then\n" +
  38.             "    return tonumber(current);\n" +
  39.             "end\n" +
  40.             "current = redis.call('incr', key)\n" +
  41.             "if tonumber(current) == 1 then\n" +
  42.             "    redis.call('expire', key, time)\n" +
  43.             "end\n" +
  44.             "return tonumber(current);";
  45.     }
  46. }
复制代码
Map + Guava RateLimiter实现
  1. /**
  2. * @author valarchie
  3. */
  4. @SuppressWarnings("UnstableApiUsage")
  5. @Component
  6. @RequiredArgsConstructor
  7. @Slf4j
  8. public class MapRateLimitChecker extends AbstractRateLimitChecker{
  9.     /**
  10.      * 最大仅支持4096个key   超出这个key  限流将可能失效
  11.      */
  12.     private final LRUCache<String, RateLimiter> cache = new LRUCache<>(4096);
  13.     @Override
  14.     public void check(RateLimit rateLimit) {
  15.         String combinedKey = rateLimit.limitType().generateCombinedKey(rateLimit);
  16.         RateLimiter rateLimiter = cache.get(combinedKey,
  17.             () -> RateLimiter.create((double) rateLimit.maxCount() / rateLimit.time())
  18.         );
  19.         if (!rateLimiter.tryAcquire()) {
  20.             throw new ApiException(ErrorCode.Client.COMMON_REQUEST_TOO_OFTEN);
  21.         }
  22.         log.info("限制请求key:{}, combined key:{}", rateLimit.key(), combinedKey);
  23.     }
  24. }
复制代码
限流切面

我们需要在切面中,读取限流注解标注的信息,然后选择不同的限流实现来进行限流。
  1. /**
  2. * 限流切面处理
  3. *
  4. * @author valarchie
  5. */
  6. @Aspect
  7. @Component
  8. @Slf4j
  9. @ConditionalOnExpression("'${agileboot.embedded.redis}' != 'true'")
  10. @RequiredArgsConstructor
  11. public class RateLimiterAspect {
  12.     @NonNull
  13.     private RedisRateLimitChecker redisRateLimitChecker;
  14.     @NonNull
  15.     private MapRateLimitChecker mapRateLimitChecker;
  16.     @Before("@annotation(rateLimiter)")
  17.     public void doBefore(JoinPoint point, RateLimit rateLimiter) {
  18.         MethodSignature signature = (MethodSignature) point.getSignature();
  19.         Method method = signature.getMethod();
  20.         log.info("当前限流方法:" + method.toGenericString());
  21.         switch (rateLimiter.cacheType()) {
  22.             case REDIS:
  23.                 redisRateLimitChecker.check(rateLimiter);
  24.                 break;
  25.             case Map:
  26.                 mapRateLimitChecker.check(rateLimiter);
  27.                 return;
  28.             default:
  29.                 redisRateLimitChecker.check(rateLimiter);
  30.         }
  31.     }
  32. }
复制代码
注解使用

以下是我们标注的注解例子。
time=10,maxCount=10表明10秒内最多10次请求。
cacheType=Redis表明使用Redis来实现。
limitType=IP表明基于IP来限流。
  1. /**
  2. * 生成验证码
  3. */
  4. @Operation(summary = "验证码")
  5. @RateLimit(key = RateLimitKey.LOGIN_CAPTCHA_KEY, time = 10, maxCount = 10, cacheType = CacheType.REDIS,
  6.     limitType = LimitType.IP)
  7. @GetMapping("/captchaImage")
  8. public ResponseDTO<CaptchaDTO> getCaptchaImg() {
  9.     CaptchaDTO captchaImg = loginService.generateCaptchaImg();
  10.     return ResponseDTO.ok(captchaImg);
  11. }
复制代码
这是笔者关于中小型项目关于请求限流的实现,如有不足欢迎大家评论指正。
全栈技术交流群:1398880


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
回复

举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

篮之新喜

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表