技术介绍
RateLimiter 从概念上来讲,速率限制器会在可配置的速率下分配许可证。如果必要的话,每个acquire() 会阻塞当前线程直到许可证可用后获取该许可证。一旦获取到许可证,不需要再释放许可证。
原理
RateLimiter 使用的是一种叫令牌桶的流控算法,令牌桶算法是网络流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一种算法。
令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。从原理上看,令牌桶算法和漏桶算法是相反的,一个“进水”,一个是“漏水”。
RateLimiter 会按照一定的频率往桶里扔令牌,线程拿到令牌才能执行,比如你希望自己的应用程序QPS不要超过1000,那么RateLimiter设置1000的速率后,就会每秒往桶里扔1000个令牌。
RateLimiter 经常用于限制对一些物理资源或者逻辑资源的访问速率。与Semaphore 相比,Semaphore 限制了并发访问的数量而不是使用速率。(注意尽管并发性和速率是紧密相关的,比如参考Little定律)
对于很多应用场景来说,除了要求能够限制数据的平均传输速率外,还要求允许某种程度的突发传输。这时候漏桶算法可能就不合适了,令牌桶算法更为适合。
方法摘要
解决方案
- 采用aop思想实现,对全部controller层方法进行限制。
- 限流方式:RateLimiter + Cache(Guava),采用Cache存储某个”ip+方法“对应的RateLimiterr,并设置过期策略。
具体实现
1.引入依赖
<!--Guava-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.1-jre</version>
</dependency>
<!--aop-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.自定义注解
@Target(ElementType.METHOD) // 作用到方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时有效
/**
* @功能描述 平滑突发限流注解,接口限流自定义注解
* @author zjq
* @date 2024-03-04
*/
public @interface RateLimit {
/**
* @return 每秒向令牌桶发放的令牌数量,不填默认100
* */
double rate() default 100;
/**
* @return 超时等待,不填默认0
* */
long timeOut() default 0;
/**
* @return 超时等待时间单位
* */
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
}
3.编写切面
@Aspect
@Component
/**
* @功能描述 接口限流AOP切面
* @author zjq
* @date 2024-03-04
*/
@Order(1001)
public class RateLimitConfig {
private static RateLimiter rateLimiter = RateLimiter.create(Double.MAX_VALUE);
/**
* 业务层切点
*/
@Pointcut("@annotation(com.maizhiyu.yzt.annotation.RateLimit)"
/*+ "|| execution(* com.jeethink.boot..*Controller.*(..))"*/)
public void RateLimiterAspect() {
}
@Around("RateLimiterAspect()")
public Object around(ProceedingJoinPoint joinPoint) {
Object obj = null;
long timeOut = 0;
TimeUnit timeUnit = TimeUnit.MILLISECONDS;
try {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
if (method.isAnnotationPresent(RateLimit.class)) {
//获取方法上的注解对象@RateLimit
RateLimit rateLimitAnnotation = method.getAnnotation(RateLimit.class);
//核心方法,内部采用令牌捅算法实现,每秒只发出X个令牌,既允许每秒X个不同线程访问
rateLimiter.setRate(rateLimitAnnotation.rate());
timeOut = rateLimitAnnotation.timeOut();
timeUnit = rateLimitAnnotation.timeUnit();
} else {
//100QPS
rateLimiter.setRate(100);
}
//tryAcquire()是非阻塞, rateLimiter.acquire()是阻塞的
if (rateLimiter.tryAcquire(timeOut, timeUnit)) {
obj = joinPoint.proceed();
} else {
//拒绝了请求(服务降级)
return Result.failure(10000, "系统繁忙,请稍后再试!");
}
} catch (Throwable e) {
e.printStackTrace();
}
return obj;
}
}
4.添加注解
@GetMapping("/getBillListNew")
@RateLimit(rate = 1,timeOut = 50,timeUnit = TimeUnit.MILLISECONDS)
public Result<BuAchievementsVo> getBillListNew(Integer timeType, String time, String departmentId, String name) {
HsUser user = jwtUtil.getCurrentUser();
Long customerId =user.getCustomerId();
String doctorId =user.getHisId();
Long userId = user.getId();
BuAchievementsVo vo = buPrescriptionItemTaskService.cureAchievements(customerId,timeType,time,departmentId,name,doctorId,userId);
return Result.success(vo);
}