
Guava实现限流
网上讲解限流算法的很多,自己实践一下印象会更深刻,所以在自己项目上实现了引入Guava做限流。@Retention(RetentionPolicy.RUNTIME)//修饰注解,用来表示注解的生命周期@Target({ElementType.METHOD})//注解的作用目标,这个表示注解到方法/*** 资源的key,唯一* 作用:不同的接口,不同的流量控制*//*** 限制次数(每秒)*//**
·
目录
前言
网上讲解限流算法的很多,自己实践一下印象会更深刻,所以在自己项目上实现了引入Guava做限流。
一、硬编码方式
1.引入guava依赖包
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1-jre</version><!--选择自己需要的版本-->
</dependency>
2.给接口添加限流逻辑
@Controller
@RequestMapping("/area")
public class AreaController {
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private IAreaService areaService;
//每秒钟获3个请求
private final RateLimiter limiter = RateLimiter.create(3);
@RequestMapping("/testLimit")
@ResponseBody
public ResponseEntity testLimit(){
try {
//添加限流的逻辑
//500毫秒没获取到令牌就返回失败
boolean success = limiter.tryAcquire(500, TimeUnit.MILLISECONDS);
if(!success){
logger.info("硬编码接口未获取到令牌,返回");
return new ResponseEntity(false, "系统繁忙,请稍后重试");
}
logger.info("硬编码接口获取到了令牌,执行业务");
List<String> schoolIdList = new ArrayList<>();
schoolIdList.add("1");
return areaService.queryCount(schoolIdList);
} catch (Exception e) {
logger.error("查询校区异常", Throwables.getStackTraceAsString(e));
return new ResponseEntity(false, "查询异常");
}
}
}
因为是在自己真实项目上引入的,所以有一些业务代码,请忽略。
重要方法介绍:
- acquire() 获取一个令牌, 该方法会阻塞直到获取到这一个令牌, 返回值为获取到这个令牌花费的时间
- acquire(int permits) 获取指定数量的令牌, 该方法也会阻塞, 返回值为获取到这 N 个令牌花费的时间
- tryAcquire() 判断是否能获取到令牌, 如果不能获取立即返回 false
- tryAcquire(int permits) 获取指定数量的令牌, 如果不能获取立即返回 false
- tryAcquire(long timeout, TimeUnit unit) 判断能否在指定时间内获取到令牌, 如果不能获取立即返回 false
- tryAcquire(int permits, long timeout, TimeUnit unit) 同上
3.测试结果
参数设置:设置每秒获取三个令牌,接口测试5秒执行100次请求。
结果:20次获取到了令牌,80次没有获取到令牌。
误差分析:标准结果应该是15次获取到了令牌,分析误差产生的可能原因有两点:1)采用单机发送请求,跟真实场景有差异。2)等待时间500毫秒也会影响结果。
二、注解方式
实际使用的时候我们肯定不能每次都硬编码,可以使用注解的方式。
自己写注解,基于aop实现。
1.加入AOP依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.自定义限流注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;
@Retention(RetentionPolicy.RUNTIME)//修饰注解,用来表示注解的生命周期
@Target({ElementType.METHOD})//注解的作用目标,这个表示注解到方法
public @interface Limit {
/**
* 资源的key,唯一
* 作用:不同的接口,不同的流量控制
*/
String key() default "";
/**
* 限制次数(每秒)
*/
double permitsPerSecond();
/**
* 最大等待时间
*/
long timeOut();
/**
* 最大等待时间的单位,默认毫秒
*/
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
/**
* 获取不到令牌时候的提示语
*/
String message() default "系统繁忙,请稍后重试";
}
3.使用AOP切面拦截
@Slf4j
@Aspect//定义为切面
@Component
public class LimitAspect {
/**
* 不同的接口,不同的流量控制,map的key为Limit.key
*/
private final Map<String , RateLimiter> limiterMap = Maps.newConcurrentMap();
@Around("@annotation(cn.xdf.iteach.mdm.common.annotation.Limit)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature)joinPoint.getSignature();
Method method = signature.getMethod();
Limit limit = method.getAnnotation(Limit.class);//获取到方法上的Limit注解
if(limit != null){
RateLimiter rateLimiter = limiterMap.get(limit.key());
if(null == rateLimiter){
//创建当前key的RateLimiter
rateLimiter = RateLimiter.create(limit.permitsPerSecond());
limiterMap.put(limit.key(),rateLimiter);
log.info("创建了新的令牌桶,key={},容量={}",limit.key(),limit.permitsPerSecond());
}
boolean success = rateLimiter.tryAcquire(0, limit.timeUnit());
if(!success){
log.info("获取令牌失败,key={}",limit.key());
this.responseFail(limit.message());
return null;
}
log.info("获取令牌成功,key={}",limit.key());
}
return joinPoint.proceed();
}
private void responseFail(String message) throws IOException {
HttpServletResponse response=((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=UTF-8");
PrintWriter pw = response.getWriter();
JSONObject object = new JSONObject();
object.put("success",false);
object.put("code",2001);
object.put("message",message);
pw.write(object.toJSONString());
pw.flush();
pw.close();
}
}
4.给接口添加注解
@Controller
@RequestMapping("/area")
public class AreaController {
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private IAreaService areaService;
@RequestMapping("/testLimitWithAnnotation")
@ResponseBody
@Limit(key="/area/testLimitWithAnnotation",permitsPerSecond = 3,timeOut = 500L)
public ResponseEntity testLimitWithAnnotation(){
try {
List<String> schoolIdList = new ArrayList<>();
schoolIdList.add("1");
return areaService.queryCount(schoolIdList);
} catch (Exception e) {
logger.error("查询校区异常", Throwables.getStackTraceAsString(e));
return new ResponseEntity(false, "查询异常");
}
}
}
5.测试结果
参数设置:设置每秒获取三个令牌,接口测试5秒执行100次请求。
结果:18次获取到了令牌,82次没有获取到令牌。
三、其他限流方式
Guava是单机版的限流方式,还有一些其他方式的限流
1)基于sentinel限流(分布式版)
2)基于redis+lua限流(分布式版)
3)网关限流(分布式版)
有兴趣的同学可以尝试一下。
分布式版是控制的是接口的所有部署节点,准确性上肯定是更高的。但因为跨网络,性能上会稍差一些。
参考文档
更多推荐
所有评论(0)