接口限流实现

限流

限流是什么?韩国首尔梨泰院踩踏事件,一时刻大量人聚集在一个狭窄路口,最后导致事故的发生。假如果,进去的时候限流,出去的时候限流,严格管理,那么悲剧发生的概率是不是会小一点。

先问俩件事:
你的接口能支持多少qps?
假如100000个请求同时打在你的接口上,你的服务会发生什么事?

接口限流就是做力所能及的事,保证接口不被冲烂,超过阈值的请求,存在队列或者丢弃返回。
image
上图水龙头可以自己轻松设置水流的速度。

限流算法有哪些?

固定窗口

就是将时间划分为窗口模式,在一个窗口范围内,请求的次数的上限是固定的,超过窗口范围的请求便被丢弃。

当请求在窗口后半段,与下一个窗口的前半段发生时,qps可能会翻倍

滑动窗口

滑动窗口按时间再细分,每次都计算以当前时间为终止时间,然后时间精度(1s等精度)内的请求总数。解决了,qps翻倍的问题。

漏桶

就是使用一个桶存储所有请求,然后桶会有一定流速流出消耗桶内任务,当桶内任务满的时候,选择抛弃任务。

令牌桶

就是使用一个桶存储所有令牌,然后桶会有一定流速生成令牌,当桶内任务满的时候,选择抛弃生成的令牌。请求接口时需要到令牌桶获取令牌。

限流算法实战

单机限流

Google Guava 利用令牌桶算法实现了限流工具类 RateLimiter。

  1. 引入依赖
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>31.1-jre</version>
        </dependency>
  1. 创建限流注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RateLimit {
    @AliasFor("qps")
    double value() default 0;

    @AliasFor("value")
    double qps() default 0;

    int timeout() default 0;

    TimeUnit timeUnit() default TimeUnit.MICROSECONDS;

}
  1. 进行拦截
/**
 * @author tao
 * @Date 2023-04-02
 */
@Aspect
@Component
public class RateLimitAspect {

    /**
     * RateLimiter Cache
     * different interfaces have different rate limiters
     */
    private ConcurrentHashMap<String, RateLimiter> RATE_LIMITER_CACHE = new ConcurrentHashMap<>();

    @Pointcut("@annotation(com.tao.anno.RateLimit)")
    public void rateLimit() {
    }

    @Around("rateLimit()")
    public Object pointcut(ProceedingJoinPoint point) throws Throwable {
        MethodSignature methodSignature = (MethodSignature) point.getSignature();
        Method method = methodSignature.getMethod();
        // rate limiter cache key -> package name + method name (eg:"com.alibaba.comtroller.getUser")
        String key = methodSignature.getDeclaringTypeName() + "." + method.getName();
        RateLimit rateLimit = AnnotationUtils.findAnnotation(method, RateLimit.class);
        if (rateLimit != null) {
            double value = rateLimit.value();
            int timeout = rateLimit.timeout();
            TimeUnit timeUnit = rateLimit.timeUnit();
            if (RATE_LIMITER_CACHE.get(key) == null) {
                synchronized (key) {
                    if (RATE_LIMITER_CACHE.get(key) == null) {
                        RATE_LIMITER_CACHE.put(key, RateLimiter.create(value));
                    }
                }
            }
            RateLimiter rateLimiter = RATE_LIMITER_CACHE.get(key);
            if (rateLimiter == null || !rateLimiter.tryAcquire(timeout, timeUnit)) {
                // Custom processing logic, such as throwing exceptions
                System.out.println(method.getName() + "interface rate limit");
            }
        }
        return point.proceed();
    }
}

  1. 使用
@RestController
@RequestMapping("/user")
public class RateLimitController {

    @RateLimit(value = 1, timeout = 10, timeUnit = TimeUnit.MILLISECONDS)
    @GetMapping("/limit1")
    public String test1() {
        return "Hello,world!";
    }

    @RateLimit(qps = 10, timeout = 10, timeUnit = TimeUnit.MILLISECONDS)
    @GetMapping("/limit2")
    public String test2() {
        return "Hello,world!";
    }
}
  1. 测试
    接口请求速度过快,便会出现提示
    image

分布式限流

  1. 限流注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RedissonRateLimit {

    //限流时存放在redis中key值
    String redisKey();

    //限流接口名
    String interfaceName();

    //在限流的时间内,允许通过的次数
    long rate();

    //限流的时间,单位毫秒
    long rateInterval();

    boolean isEnable() default false;
    
}
  1. AOP拦截
/**
 * RRateLimiter API: https://www.javadoc.io/doc/org.redisson/redisson/3.10.6/org/redisson/api/RRateLimiter.html
 */
@Slf4j
@Aspect
public class RedissonRateLimitAspect {

    @Autowired
    private RedissonClient redissonClient;


    @Before("@annotation(redissonRateLimit)")
    public void redissonRateLimitHandle(RedissonRateLimit redissonRateLimit) {

        if (!redissonRateLimit.isEnable() || ObjectUtil.isEmpty(redissonRateLimit) || ObjectUtil.isEmpty(redissonClient)) {
            return;
        }

        //获取限流次数/限流时间/限流的key
        String interfaceName = redissonRateLimit.interfaceName();
        long rate = redissonRateLimit.rate();
        long rateInterval = redissonRateLimit.rateInterval();

        String redisKey = redissonRateLimit.redisKey();
        RRateLimiter rateLimiter = redissonClient.getRateLimiter(redisKey);
        if (!rateLimiter.isExists()) {
            rateLimiter.trySetRate(RateType.OVERALL, rate, rateInterval, RateIntervalUnit.MILLISECONDS);
        } else {
            //获取旧限流的配置信息,防止服务重启,限流配置更新
            RateLimiterConfig rateLimiterConfig = rateLimiter.getConfig();
            Long oldRateInterval = rateLimiterConfig.getRateInterval();
            Long oldRate = rateLimiterConfig.getRate();

            if (rateInterval != oldRateInterval && rate != oldRate) {
                //删除之前的限流配置
                rateLimiter.delete();
                rateLimiter.trySetRate(RateType.OVERALL, rate, rateInterval, RateIntervalUnit.MILLISECONDS);
            }
        }
        log.info("获取到的限流接口名:{},限流间隔时间:{},限流次数:{},限流的主键key:{}", interfaceName, rateInterval, rate, redisKey);
        if (rateLimiter.getConfig() != null && !rateLimiter.tryAcquire()) {
            throw new RedissonRateLimitException("当前请求次数过多,请在" + ((rateInterval / 1000 / 60) < 1 ? 1 :
                    (rateInterval / 1000 / 60 + 1)) + "分钟后尝试!");
        }
    }
}
  1. 测试
@RestController
@RequestMapping("/test")
public class TestController {
    // 2000ms 生成1个令牌
    @RedissonRateLimit(redisKey = "test", rateInterval = 2000, rate = 1, isEnable = true, interfaceName = "test")
    @GetMapping("")
    String test() {
        return "success";
    }
    
}

分布式限流器的原理:

  1. 记录限流器的配置:多长时间产生多少令牌
  2. 判断令牌是不是大于所需要的令牌,如果小于返回需要等待时间。

    每次请求时会根据过期时间,清除已分配令牌。

参考文章

分布式服务限流实战,已经为你排好坑了:https://www.infoq.cn/article/Qg2tX8fyw5Vt-f3HH673
Rediss RRateLimiter 原理:https://blog.csdn.net/promisessh/article/details/112767743
Redisson分布式限流RRateLimiter的实现原理:https://blog.csdn.net/qq_41625866/article/details/129501114
Redisson官网中文文档:https://github.com/redisson/redisson/wiki/目录

posted @ 2023-04-02 22:13  帅气的涛啊  阅读(81)  评论(0编辑  收藏  举报