1.流行的两种下流方式
常见的限流算法有:计数器、令牌桶、漏桶
1.1 计数器算法
采用计数器实现限流有点简单粗暴,一般我们会限制一秒钟的能够通过的请求数,比如限流qps为100,算法的实现思路就是从第一个请求进来开始计时,在接下去的1s内,每来一个请求,就把计数加1,如果累加的数字达到了100,那么后续的请求就会被全部拒绝。等到1s结束后,把计数恢复成0,重新开始计数。
具体的实现可以是这样的:对于每次服务调用,可以通过 AtomicLong#incrementAndGet()方法来给计数器加1并返回最新值,通过这个最新值和阈值进行比较。
这种实现方式,相信大家都知道有一个弊端:如果我在单位时间1s内的前10ms,已经通过了100个请求,那后面的990ms,只能眼巴巴的把请求拒绝,我们把这种现象称为“突刺现象”。
1.2 漏桶算法
为了消除"突刺现象",可以采用漏桶算法实现限流,漏桶算法这个名字就很形象,算法内部有一个容器,类似生活用到的漏斗,当请求进来时,相当于水倒入漏斗,然后从下端小口慢慢匀速的流出。不管上面流量多大,下面流出的速度始终保持不变。
不管服务调用方多么不稳定,通过漏桶算法进行限流,每10毫秒处理一次请求。因为处理的速度是固定的,请求进来的速度是未知的,可能突然进来很多请求,没来得及处理的请求就先放在桶里,既然是个桶,肯定是有容量上限,如果桶满了,那么新进来的请求就丢弃。
在算法实现方面,可以准备一个队列,用来保存请求,另外通过一个线程池定期从队列中获取请求并执行,可以一次性获取多个并发执行。
这种算法,在使用过后也存在弊端:无法应对短时间的突发流量。
1.3 令牌桶算法
从某种意义上讲,令牌桶算法是对漏桶算法的一种改进,桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。
在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝。
放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌,所以就存在这种情况,桶中一直有大量的可用令牌,这时进来的请求就可以直接拿到令牌执行,比如设置qps为100,那么限流器初始化完成一秒后,桶中就已经有100个令牌了,这时服务还没完全启动好,等启动完成对外提供服务时,该限流器可以抵挡瞬时的100个请求。所以,只有桶中没有令牌时,请求才会进行等待,最后相当于以一定的速率执行。
实现思路:可以准备一个队列,用来保存令牌,另外通过一个线程池定期生成令牌放到队列中,每来一个请求,就从队列中获取一个令牌,并继续执行。
2 . Guava RateLimiter 限流实现
Guava类库中提供了令牌痛限流器的工具类RateLimiter,下面,我们具体看下Guava中令牌桶的具体实现,RateLimiter的使用:
首先通过RateLimiter.create(1);创建一个限流器,参数代表每秒生成的令牌数,通过limiter.acquire(i);来以阻塞的方式获取令牌,也可以通过tryAcquire(int permits, long timeout, TimeUnit unit)来设置等待超时时间的方式获取令牌,如果超timeout为0,则代表非阻塞,获取不到立即返回。
Guava中的RateLimiter有两种实现WarmingUp(令牌生成速度缓慢提升到稳定值)和Bursty(令牌生成速度恒定),当服务一段时间没有被使用时,RateLimiter会积累一定数目的令牌,从服务的角度来看,这时有两种情况:
(1)服务有大量的空闲资源,可供调用者使用;—— BurstyRateLimiter
(2)由于长时间没有使用,缓存等功能失效,启动时需要预热操作;—— WarmingUpRateLimiter
预热版本WarmingUpRateLimiter的特别之处在于:
(1)桶中令牌数目的算法
(2)下次新的可用令牌产生的时间的算法
3.基于注解的guava-ratelimiter-annotation
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MethodRateLimiter {
/**
* QPS
*
* @return
*/
double limit() default 100;
/**
* 每次获取令牌的个数
*
* @return
*/
int number() default 1;
/**
* 超时时长
*
* @return
*/
int timeout() default 1000;
/**
* 超时单位 毫秒
*
* @return
*/
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
}
@Aspect
@Component
@Slf4j
public class RateLimiterAspect {
/**
* 保存不同方法的限流器
*/
private static final ConcurrentHashMap<String, RateLimiter> RATE_LIMITER_MAP = new ConcurrentHashMap<>();
@Pointcut("@annotation(com.shein.sns.annotation.MethodRateLimiter)")
public void rateLimiter(){}
@Around("rateLimiter()")
public Object doRateLimit(ProceedingJoinPoint pjp) throws Throwable {
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
if (Objects.isNull(RATE_LIMITER_MAP.get(method.getName()))) {
MethodRateLimiter methodRateLimiter = AnnotationUtils.findAnnotation(method, MethodRateLimiter.class);
RATE_LIMITER_MAP.put(method.getName(), RateLimiter.create(Objects.nonNull(methodRateLimiter) ? methodRateLimiter.limit() : 1000));
}
//Only acquire 1 here now
double sleepTime = RATE_LIMITER_MAP.get(method.getName()).acquire();
if (sleepTime > 0.0) {
log.info("等待令牌耗时:methodName={},耗时{} 秒", method.getName(),sleepTime);
}
return pjp.proceed();
}
}