常用接口限流功能实现基于Guava设计

版权声明: https://blog.csdn.net/qq_39211866/article/details/84312116

Guava是一组核心库,包括新的集合类型(例如multimap和multiset),不可变集合,图形库,函数类型,内存缓存以及用于并发,I / O,散列,基元的API /实用程序,反射,字符串处理等等!

本示例只使用了Guava工具包的RateLimiter类,进行API的限流。
限流简介:
限流中的“流”字该如何解读呢?要限制的指标到底是什么?不同的场景对“流”的定义也是不同的,可以是网络流量,带宽,每秒处理的事务数 (TPS),每秒请求数 (hits per second),并发请求数,甚至还可能是业务上的某个指标,比如用户在某段时间内允许的最多请求短信验证码次数。

从保证系统稳定可用的角度考量,对于微服务系统来说,最好的一个限流指标是:并发请求数。通过限制并发处理的请求数目,可以限制任何时刻都不会有过多的请求在消耗资源,比如:我们通过配置 web 容器中 servlet worker 线程数目为 200,则任何时刻最多都只有 200 个请求在处理,超过的请求都会被阻塞排队。

假如一个接口请求量因为某些原因突然涨到之前的10倍,没多久该接口几乎不可使用,并引发连锁反应导致整个系统崩溃。如何应对这种情况呢?生活给了我们答案:比如老式电闸都安装了保险丝,一旦有人使用超大功率的设备,保险丝就会烧断以保护各个电器不被强电流给烧坏。同理我们的接口也需要安装上“保险丝”,以防止非预期的请求对系统压力过大而引起的系统瘫痪,当流量过大时,可以采取拒绝或者引流等机制。

常见的限流算法有:

  • 漏桶算法
  • 令牌桶算法
  1. 漏桶算法
    漏桶算法大概意思是请求进入到漏桶中,漏桶以一定的速率漏水。当请求过多时,水直接溢出。可以看出,漏桶算法可以强制限制数据的传输速度。
  2. 令牌桶算法
    令牌桶算法的原理是系统以一定速率向桶中放入令牌,如果有请求时,请求会从桶中取出令牌,如果能取到令牌,则可以继续完成请求,否则等待或者拒绝服务。这种算法可以应对突发程序的请求,因此比漏桶算法好。

在Wikipedia上,令牌桶算法是这么描述的:

  • 每秒会有r个令牌放入桶中,或者说,每过1/r 秒桶中增加一个令牌
  • 桶中最多存放b个令牌,如果桶满了,新放入的令牌会被丢弃
  • 当一个n字节的数据包到达时,消耗n个令牌,然后发送该数据包
  • 如果桶中可用令牌小于n,则该数据包将被缓存或丢弃

RateLimiter

Guava中开源出来一个令牌桶算法的工具类RateLimiter,可以轻松实现限流的工作。RateLimiter对简单的令牌桶算法做了一些工程上的优化,具体的实现是SmoothBursty。需要注意的是,RateLimiter的另一个实现SmoothWarmingUp,就不是令牌桶了,而是漏桶算法。也许是出于简单起见,RateLimiter中的时间窗口能且仅能为1S,如果想搞其他时间单位的限流,只能另外造轮子。

RateLimiter有一个有趣的特性是[前人挖坑后人跳],也就是说RateLimiter允许某次请求拿走了超出剩余令牌数的令牌,但是下一次请求将为此付出代价,一直等到令牌亏空补上,并且桶中有足够本次请求使用的令牌为止。这里面就涉及到一个权衡,是让前一次请求干等到令牌够用才走掉呢,还是让它走掉后面的请求等一等呢?Guava的设计者选择的是后者,先把眼前的活干了,后面的事后面再说。
并且构建了一个自定义注解,方便松耦合,灵活的对服务进行限流。

    /**
     * The entry point of application.
     * <p>
     * RateLimiter有一个有趣的特性是[前人挖坑后人跳],也就是说RateLimiter允许某次请求拿走了超出剩余令牌数的令牌,
     * 但是下一次请求将为此付出代价,一直等到令牌亏空补上,并且桶中有足够本次请求使用的令牌为止。
     * 这里面就涉及到一个权衡,是让前一次请求干等到令牌够用才走掉呢,还是让它走掉后面的请求等一等呢?
     * Guava的设计者选择的是后者,先把眼前的活干了,后面的事后面再说。
     */
    @Test
    public void advanceConsumerTest() {
        //每秒产生2个令牌
        RateLimiter rateLimiter = RateLimiter.create(2);
        //获取令牌,返回获取令牌所需等待的时间,获取太多,导致后面得等亏损的令牌补上才能获取到。
        System.out.println(rateLimiter.acquire(10));
        System.out.println(rateLimiter.tryAcquire(2, 2, TimeUnit.SECONDS));
        System.out.println(rateLimiter.acquire(2));
        System.out.println(rateLimiter.acquire(1));
    }

结果如下,可以看到,RateLimiter每秒只能产生2个令牌,而第一获取10个的话,后面的就需要用5秒的时间补上空缺。

0.0
false
4.994628
0.995124

下面通过一个例子,测试100个并发下,限流起到的效果

    @Test
    public void rateLimitTest() throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(1);
        for (int i = 0; i <= 100; i++) {
            Business business = new Business(countDownLatch);
            business.start();
        }
        countDownLatch.countDown();
        //等待结果处理,有只设了10个令牌,所以,只有10个请求有效。
        TimeUnit.SECONDS.sleep(10);
        System.out.println("所有模拟请求结束  at " + new Date());
    }

    class Business extends Thread {
        CountDownLatch countDownLatch;

        public Business(CountDownLatch latch) {
            this.countDownLatch = latch;
        }

        @Override
        public void run() {
            try {
                countDownLatch.await();
                if (rateLimiterService.tryAcquire()) {
                    //模拟业务
                    TimeUnit.SECONDS.sleep(3);
                    System.out.println("成功处理业务" + new Date());
                } else {
                    System.out.println("系统繁忙!请稍后再试!");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

运行结果,只有10个请求获取到令牌,成功执行,其他的都直接返回

.....
系统繁忙!请稍后再试!
系统繁忙!请稍后再试!
系统繁忙!请稍后再试!
系统繁忙!请稍后再试!
系统繁忙!请稍后再试!
系统繁忙!请稍后再试!
系统繁忙!请稍后再试!
系统繁忙!请稍后再试!
成功处理业务Tue Nov 20 23:45:23 CST 2018
成功处理业务Tue Nov 20 23:45:23 CST 2018
成功处理业务Tue Nov 20 23:45:23 CST 2018
成功处理业务Tue Nov 20 23:45:23 CST 2018
成功处理业务Tue Nov 20 23:45:23 CST 2018
成功处理业务Tue Nov 20 23:45:23 CST 2018
成功处理业务Tue Nov 20 23:45:23 CST 2018
成功处理业务Tue Nov 20 23:45:23 CST 2018
成功处理业务Tue Nov 20 23:45:23 CST 2018
成功处理业务Tue Nov 20 23:45:23 CST 2018
成功处理业务Tue Nov 20 23:45:23 CST 2018
所有模拟请求结束  at Tue Nov 20 23:45:30 CST 2018

最后,为了方便日常使用,我还特定的设计了一个自定义注解,返回简单定义达到效果,正所谓偷懒使人进步。
这里贴出基于注解的设计:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RateLimiterAnnotation {
    /**
     * 限流服务名
     *
     * @return
     */
    String name();

    /**
     * 每秒限流次数
     *
     * @return
     */
    double count();
}

切面实现类

@Aspect
@Component
public class RateLimiterAnnotationAspect {

    private ConcurrentMap<String, RateLimiter> rateLimiterMap = new ConcurrentHashMap<>();

    /**
     * Before.
     *
     * @param point the point
     */
    @Before("@annotation(com.dashuai.learning.ratelimiter.annotation.RateLimiterAnnotation)")
    public void before(JoinPoint point) {
        RateLimiterAnnotation rateLimiterAnnotation = this.getAnnotation(point, RateLimiterAnnotation.class);
        double rateLimitCount = rateLimiterAnnotation.count();
        String rateLimitName = rateLimiterAnnotation.name();
        if (rateLimiterMap.get(rateLimitName) == null) {
            rateLimiterMap.put(rateLimitName, RateLimiter.create(rateLimitCount));
        }
        rateLimiterMap.get(rateLimitName).acquire();
    }

    private <T extends Annotation> T getAnnotation(JoinPoint pjp, Class<T> clazz) {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        return method.getAnnotation(clazz);
    }
}

使用:

@Service
public class AopTestServiceImpl implements AopTestService {
    @Override
    @RateLimiterAnnotation(name = "v1", count = 5.0)
    public String testRateLimiter(Double count, String context) {
        System.out.println(count + "   " + context);
        return "测试";
    }

    @Override
    @RateLimiterAnnotation(name = "v2", count = 7.0)
    public String testRateLimiterv2(Double count, String context) {
        System.out.println("V2版本发出:" + count + "   " + context);
        return "测试第二个";
    }
}

设计思路较简单,通过一个map存储各个服务的限流数,在通过AOP切面前置判断,达到一个限流效果。

本文源码以上传github:
https://github.com/liaozihong/SpringBoot-Learning/tree/master/SpringBoot-RateLimiter

另外,一开源限流框架值得研究:
https://github.com/wangzheng0822/ratelimiter4j
参考链接:
https://wizardforcel.gitbooks.io/guava-tutorial/

猜你喜欢

转载自blog.csdn.net/qq_39211866/article/details/84312116