何为恳求限流?

恳求限流是一种操控API或其他Web服务的流量的技术。它的意图是约束客户端对服务器宣布的恳求的数量或速率,以防止服务器过载或呼应时刻变慢,然后提高体系的可用性和稳定性。

中小型项目恳求限流的需求

  1. 按IP、用户、大局限流
  2. 根据不同完成的限流规划(根据Redis或许LRU缓存)
  3. 根据注解标注哪些接口限流

完好限流规划完成在开源项目中:github.com/valarchie/A…

注解规划

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

  • 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