最新要闻

广告

手机

iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?

iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?

警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案

警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案

家电

全球速递!中小型项目请求限流设计

来源:博客园

何为请求限流?

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

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

  1. 按IP、用户、全局限流
  2. 基于不同实现的限流设计(基于Redis或者LRU缓存)
  3. 基于注解标注哪些接口限流

完整限流设计实现在开源项目中:https://github.com/valarchie/AgileBoot-Back-End


(资料图)

注解设计

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

  • key(缓存的key)
  • time(时间范围)
  • maxCount(时间范围内最大的请求次数)
  • limitType(按IP/用户/全局进行限流)
  • cacheType(基于Redis或者Map来实现限流)
/** * 限流注解 * * @author valarchie */@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @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@Slf4jpublic class RedisRateLimitChecker extends AbstractRateLimitChecker{    @NonNull    private RedisTemplate redisTemplate;    private final RedisScript 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@Slf4jpublic class MapRateLimitChecker extends AbstractRateLimitChecker{    /**     * 最大仅支持4096个key   超出这个key  限流将可能失效     */    private final LRUCache 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"")@RequiredArgsConstructorpublic 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 getCaptchaImg() {    CaptchaDTO captchaImg = loginService.generateCaptchaImg();    return ResponseDTO.ok(captchaImg);}

这是笔者关于中小型项目关于请求限流的实现,如有不足欢迎大家评论指正。

全栈技术交流群:1398880

关键词: