0
点赞
收藏
分享

微信扫一扫

SpringBoot 如何进行限流?老鸟们都这么玩的!

令牌桶算法的原理也比较简单,我们可以理解成医院的挂号看病,只有拿到号以后才可以进行诊病。

系统会维护一个令牌(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")

public class LimitController {

/**

  • 限流策略 : 1秒钟2个请求

*/

private final RateLimiter limiter = RateLimiter.create(2.0);

private DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

@GetMapping("/test1")

public String testLimiter() {

//500毫秒内,没拿到令牌,就直接进入服务降级

boolean tryAcquire = 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,反复刷新并观察后端日志

WARN LimitController:35 - 进入服务降级,时间2021-09-25 21:39:37

WARN LimitController:35 - 进入服务降级,时间2021-09-25 21:39:37

INFO LimitController:39 - 获取令牌成功,时间2021-09-25 21:39:37

WARN LimitController:35 - 进入服务降级,时间2021-09-25 21:39:37

WARN LimitController:35 - 进入服务降级,时间2021-09-25 21:39:37

INFO LimitController:39 - 获取令牌成功,时间2021-09-25 21:39:37

WARN LimitController:35 - 进入服务降级,时间2021-09-25 21:39:38

INFO LimitController:39 - 获取令牌成功,时间2021-09-25 21:39:38

WARN LimitController:35 - 进入服务降级,时间2021-09-25 21:39:38

INFO LimitController:39 - 获取令牌成功,时间2021-09-25 21:39:38

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

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

image-20210716084136689

所以,我们这里需要想办法将其优化 - 借助自定义注解+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 @interface Limit {

/**

  • 资源的key,唯一

  • 作用:不同的接口,不同的流量控制

*/

String key() default "";

/**

  • 最多的访问限制次数

*/

double permitsPerSecond () ;

/**

  • 获取令牌最大等待时间

*/

long timeout();

/**

  • 获取令牌最大等待时间,单位(例:分钟/秒/毫秒) 默认:毫秒

*/

TimeUnit timeunit() default TimeUnit.MILLISECONDS;

/**

  • 得不到令牌的提示语

*/

String msg() default "系统繁忙,请稍后再试.";

}

第三步:使用AOP切面拦截限流注解

@Slf4j

@Aspect

@Component

public class LimitAop {

/**

  • 不同的接口,不同的流量控制

  • map的key为 Limiter.key

*/

private final Map<String, RateLimiter> limitMap = Maps.newConcurrentMap();

@Around("@annotation(com.jianzh5.blog.limit.Limit)")

public Object around(ProceedingJoinPoint joinPoint) throws Throwable{

MethodSignature signature = (MethodSignature) joinPoint.getSignature();

Method method = signature.getMethod();

//拿limit的注解

Limit limit = method.getAnnotation(Limit.class);

if (limit != null) {

//key作用:不同的接口,不同的流量控制

String key=limit.key();

RateLimiter rateLimiter = null;

//验证缓存是否有命中key

if (!limitMap.containsKey(key)) {

// 创建令牌桶

rateLimiter = RateLimiter.create(limit.permitsPerSecond());

limitMap.put(key, rateLimiter);

log.info("新建了令牌桶={},容量={}",key,limit.permitsPerSecond());

}

rateLimiter = limitMap.get(key);

// 拿令牌

boolean acquire = rateLimiter.tryAcquire(limit.timeout(), limit.timeunit());

// 拿不到命令,直接返回异常提示

if (!acquire) {

log.debug("令牌桶={},获取令牌失败",key);

this.responseFail(limit.msg());

return null;

}

}

return joinPoint.proceed();

}

/**

  • 直接向前端抛出异常

  • @param msg 提示信息

*/

private void responseFail(String msg) {

HttpServletRespo

nse response=((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();

ResultData<Object> resultData = ResultData.fail(ReturnCode.LIMIT_ERROR.getCode(), msg);

WebUtils.writeJson(response,resultData);

}

}

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

@Slf4j

@RestController

@RequestMapping("/limit")

public class LimitController {

@GetMapping("/test2")

@Limit(key = "limit2", permitsPerSecond = 1, timeout = 500, timeunit = TimeUnit.MILLISECONDS,msg = "当前排队人数较多,请稍后再试!")

public String limit2() {

log.info("令牌桶limit2获取令牌成功");

return "ok";

}

@GetMapping("/test3")

@Limit(key = "limit3", permitsPerSecond = 2, timeout = 500, timeunit = TimeUnit.MILLISECONDS,msg = "系统繁忙,请稍后再试!")

public String limit3() {

log.info("令牌桶limit3获取令牌成功");

return "ok";

}

}

第五步:体验效果

通过访问测试地址: http://127.0.0.1:8080/limit/test2,反复刷新并观察输出结果:

举报

相关推荐

0 条评论