SpringBoot项目中怎么对接口进行限流,有哪些常见的限流算法,怎么优雅的进行限流。

首先就让咱们来看看为什么需求对接口进行限流?

为什么要进行限流?

因为互联网体系一般都要面临大并发大流量的恳求,在突发情况下(最常见的场景便是秒杀、抢购),瞬时大流量会直接将体系打垮,无法对外提供服务。那为了避免呈现这种情况最常见的解决方案之一便是限流,当恳求到达必定的并发数或速率,就进行等待、排队、降级、拒绝服务等。

例如,12306购票体系,在面临高并发的情况下,便是采用了限流。在流量高峰期间经常会呈现提示语;”当时排队人数较多,请稍后再试!”

什么是限流?有哪些限流算法?

限流是对某一时刻窗口内的恳求数进行约束,坚持体系的可用性和稳定性,避免因流量暴增而导致的体系运行缓慢或宕机。

常见的限流算法有三种:

1. 计数器限流

计数器限流算法是最为简略粗暴的解决方案,首要用来约束总并发数,比如数据库连接池巨细、线程池巨细、接口拜访并发数等都是运用计数器算法。

如:运用 AomicInteger 来进行计算当时正在并发履行的次数,假如超过域值就直接拒绝恳求,提示体系繁忙。

2. 漏桶算法

SpringBoot 中使用Guava实现单机令牌桶限流

漏桶算法思路很简略,咱们把水比作是恳求,漏桶比作是体系处理才能极限,水先进入到漏桶里,漏桶里的水按必定速率流出,当流出的速率小于流入的速率时,因为漏桶容量有限,后续进入的水直接溢出(拒绝恳求),以此完成限流。

3. 令牌桶算法

SpringBoot 中使用Guava实现单机令牌桶限流

令牌桶算法的原理也比较简略,咱们能够了解成医院的挂号看病,只有拿到号今后才能够进行诊病。

体系会保护一个令牌(token)桶,以一个稳定的速度往桶里放入令牌(token),这时假如有恳求进来想要被处理,则需求先从桶里获取一个令牌(token),当桶里没有令牌(token)可取时,则该恳求将被拒绝服务。令牌桶算法经过操控桶的容量、发放令牌的速率,来到达对恳求的约束。

根据Guava东西类完成限流

Google开源东西包Guava提供了限流东西类RateLimiter,该类根据令牌桶算法完成流量约束,运用十分便利,并且十分高效,完成步骤如下:

第一步:引进guava依靠包

<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1-jre</version>
</dependency>

第二步:给接口加上限流逻辑

@Slf4j
@RestController
@RequestMapping("/limit")
publicclassLimitController{
/**
*限流策略:1秒钟2个恳求
*/
privatefinalRateLimiterlimiter=RateLimiter.create(2.0);
privateDateTimeFormatterdtf=DateTimeFormatter.ofPattern("yyyy-MM-ddHH:mm:ss");
@GetMapping("/test1")
publicStringtestLimiter(){
//500毫秒内,没拿到令牌,就直接进入服务降级
booleantryAcquire=limiter.tryAcquire(500,TimeUnit.MILLISECONDS);
if(!tryAcquire){
log.warn("进入服务降级,时刻{}",LocalDateTime.now().format(dtf));
return"当时排队人数较多,请稍后再试!";
}
log.info("获取令牌成功,时刻{}",LocalDateTime.now().format(dtf));
return"恳求成功";
}
}

以上用到了RateLimiter的2个中心方法:create()tryAcquire(),以下为具体阐明

  • acquire() 获取一个令牌, 改方法会堵塞直到获取到这一个令牌, 回来值为获取到这个令牌花费的时刻

  • acquire(int permits) 获取指定数量的令牌, 该方法也会堵塞, 回来值为获取到这 N 个令牌花费的时刻

  • tryAcquire() 判别时候能获取到令牌, 假如不能获取当即回来 false

  • tryAcquire(int permits) 获取指定数量的令牌, 假如不能获取当即回来 false

  • tryAcquire(long timeout, TimeUnit unit) 判别能否在指定时刻内获取到令牌, 假如不能获取当即回来 false

  • tryAcquire(int permits, long timeout, TimeUnit unit) 同上

第三步:体验作用

经过拜访测验地址:http://127.0.0.1:8080/limit/test1,重复刷新并调查后端日志

WARNLimitController:35-进入服务降级,时刻2021-09-2521:39:37
WARNLimitController:35-进入服务降级,时刻2021-09-2521:39:37
INFOLimitController:39-获取令牌成功,时刻2021-09-2521:39:37
WARNLimitController:35-进入服务降级,时刻2021-09-2521:39:37
WARNLimitController:35-进入服务降级,时刻2021-09-2521:39:37
INFOLimitController:39-获取令牌成功,时刻2021-09-2521:39:37
WARNLimitController:35-进入服务降级,时刻2021-09-2521:39:38
INFOLimitController:39-获取令牌成功,时刻2021-09-2521:39:38
WARNLimitController:35-进入服务降级,时刻2021-09-2521:39:38
INFOLimitController:39-获取令牌成功,时刻2021-09-2521:39:38

从以上日志能够看出,1秒钟内只有2次成功,其他都失利降级了,阐明咱们已经成功给接口加上了限流功能。

当然了,咱们在实践开发中并不能直接这样用。至于原因嘛,你想呀,你每个接口都需求手动给其加上tryAcquire(),业务代码和限流代码混在一同,并且明显违背了DRY准则,代码冗余,重复劳动。代码评定时必定会被老鸟们给讪笑一番,啥破玩意儿!

所以,咱们这里需求想办法将其优化 – 借助自定义注解+AOP完成接口限流。

根据AOP完成接口限流

根据AOP的完成方式也十分简略,完成过程如下:

第一步:参加AOP依靠

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

第二步:自定义限流注解

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public@interfaceLimit{
/**
*资源的key,仅有
*作用:不同的接口,不同的流量操控
*/
Stringkey()default"";
/**
*最多的拜访约束次数
*/
doublepermitsPerSecond();
/**
*获取令牌最大等待时刻
*/
longtimeout();
/**
*获取令牌最大等待时刻,单位(例:分钟/秒/毫秒)默许:毫秒
*/
TimeUnittimeunit()defaultTimeUnit.MILLISECONDS;
/**
*得不到令牌的提示语
*/
Stringmsg()default"体系繁忙,请稍后再试.";
}

第三步:运用AOP切面阻拦限流注解

@Slf4j
@Aspect
@Component
publicclassLimitAop{
/**
*不同的接口,不同的流量操控
*map的key为Limiter.key
*/
privatefinalMap<String,RateLimiter>limitMap=Maps.newConcurrentMap();
@Around("@annotation(com.jianzh5.blog.limit.Limit)")
publicObjectaround(ProceedingJoinPointjoinPoint)throwsThrowable{
MethodSignaturesignature=(MethodSignature)joinPoint.getSignature();
Methodmethod=signature.getMethod();
//拿limit的注解
Limitlimit=method.getAnnotation(Limit.class);
if(limit!=null){
//key作用:不同的接口,不同的流量操控
Stringkey=limit.key();
RateLimiterrateLimiter=null;
//验证缓存是否有命中key
if(!limitMap.containsKey(key)){
//创建令牌桶
rateLimiter=RateLimiter.create(limit.permitsPerSecond());
limitMap.put(key,rateLimiter);
log.info("新建了令牌桶={},容量={}",key,limit.permitsPerSecond());
}
rateLimiter=limitMap.get(key);
//拿令牌
booleanacquire=rateLimiter.tryAcquire(limit.timeout(),limit.timeunit());
//拿不到指令,直接回来异常提示
if(!acquire){
log.debug("令牌桶={},获取令牌失利",key);
this.responseFail(limit.msg());
returnnull;
}
}
returnjoinPoint.proceed();
}
/**
*直接向前端抛出异常
*@parammsg提示信息
*/
privatevoidresponseFail(Stringmsg){
HttpServletResponseresponse=((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getResponse();
ResultData<Object>resultData=ResultData.fail(ReturnCode.LIMIT_ERROR.getCode(),msg);
WebUtils.writeJson(response,resultData);
}
}

第四步:给需求限流的接口加上注解

@Slf4j
@RestController
@RequestMapping("/limit")
publicclassLimitController{

@GetMapping("/test2")
@Limit(key="limit2",permitsPerSecond=1,timeout=500,timeunit=TimeUnit.MILLISECONDS,msg="当时排队人数较多,请稍后再试!")
publicStringlimit2(){
log.info("令牌桶limit2获取令牌成功");
return"ok";
}
@GetMapping("/test3")
@Limit(key="limit3",permitsPerSecond=2,timeout=500,timeunit=TimeUnit.MILLISECONDS,msg="体系繁忙,请稍后再试!")
publicStringlimit3(){
log.info("令牌桶limit3获取令牌成功");
return"ok";
}
}

第五步:体验作用

经过拜访测验地址:http://127.0.0.1:8080/limit/test2,重复刷新并调查输出结果:

正常响应时:

{"status":100,"message":"操作成功","data":"ok","timestamp":1632579377104}

触发限流时:

{"status":2001,"message":"体系繁忙,请稍后再试!","data":null,"timestamp":1632579332177}

经过调查得之,根据自定义注解同样完成了接口限流的作用。

小结

一般在体系上线时咱们经过对体系压测能够评估出体系的功能阈值,然后给接口加上合理的限流参数,避免呈现大流量恳求时直接压垮体系。今天咱们介绍了几种常见的限流算法(重点关注令牌桶算法),根据Guava东西类完成了接口限流并使用AOP完成了对限流代码的优化。