实际工作中有这种需求:
某个接口被不同的外部ip大量访问,且每个IP的访问频率很高。为了节约后端服务器资源,于是想通过
RateLimiter令牌桶+自定义Aop注解来限制同一ip在单位时间内只能访问固定的次数,超过这个次数的请求被拦截掉。具体实现如下:
1、自定义Aop注解
package com.fancetech.tools.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface RequestLimitAnnotate {
/**
* 请求次数限制
*/
long requestLimitNum();
/**
* 限流的时间间隔,单位秒
*/
long timeInterval();
}
2、注解实现类
@Order(3)
@Aspect
@Component
@Slf4j
public class RequestLimitAop extends CBaseController {
/**
* 每秒产生的令牌数
*/
private double permitsPerSecond;
@Pointcut("@annotation(com.fancetech.tools.annotation.RequestLimitAnnotate)")
public void annotationMethod() {
}
/**
* google的缓存,maximumSize 存储的最大缓存个数,当缓存达到设置数时,会采取默认的淘汰策略(即清楚最近访问频率不高的key)
*/
private final LoadingCache<String, RateLimiter> ipRequestCaches = CacheBuilder.newBuilder()
.maximumSize(10000)// 设置缓存个数(是key的个数)
.expireAfterAccess(180, TimeUnit.SECONDS) //从最后一次访问该缓存计时开始,在180s内,如果该缓存没有再被访问,则清楚该缓存
.build(new CacheLoader<String, RateLimiter>() {
@Override
public RateLimiter load(String ipUrl) {
//log.info("permitsPerSecond:{}",permitsPerSecond);
return RateLimiter.create(permitsPerSecond);
}
});
@Around("annotationMethod()")
public Object interceptRequest(ProceedingJoinPoint joinPoint) {
try {
String ip = WebUtils.getValidIp(request);
if (EmptyUtils.isEmpty(ip)) {
log.warn("当前请求是内部服务器发出,不需要拦截");
return joinPoint.proceed();
}
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
RequestLimitAnnotate annotation = method.getAnnotation(RequestLimitAnnotate.class);
String url = request.getRequestURI();
//次数限制
long limitNum = annotation.requestLimitNum();
//时间间隔
long timeInterval = annotation.timeInterval();
this.permitsPerSecond = (double) limitNum / timeInterval;
RateLimiter rateLimiter = ipRequestCaches.get(ip + url);
if (rateLimiter.tryAcquire()) {
return joinPoint.proceed();
}
} catch (Throwable e) {
log.error("request aop 异常", e);
}
throw new RequestLimitException("当前请求次数频繁,请稍后再试。");
}
}
3、具体细节讲解:
a、首先 this.permitsPerSecond = (double) limitNum / timeInterval;计算出每秒需要该接口通过的访问次数,也就是令牌桶产生令牌的个数。
b、Guava 缓存库中的 CacheBuilder.newBuilder() 方法来创建一个 LoadingCache 实例。LoadingCache 是 Guava 提供的一种特殊的缓存实现,它能够在缓存未命中时自动加载缓存值,也就是说当调用ipRequestCaches.get(key)方法时,如果没有这个缓存,则自动创建缓存。如上图所示,创建缓存值时,key=ip+url,value=RateLimiter.create(permitsPerSecond) 这个RateLimiter对象。
maximumSize(10000) 表示这个缓存对象里面只能存10000个键值对。
c、RateLimiter rateLimiter = ipRequestCaches.get(ip + url); 当过来一个ip后,访问缓存里面的令牌桶对象,如果能拿到令牌桶对象并能获取到令牌,则执行接口的业务方法,否则抛出业务异常。
d、通过上述就可以实现每个ip在单位时间内(其实就是每秒内)只能访问某个接口指定的次数
4、实际应用
@RequestLimitAnnotate(requestLimitNum = 6,timeInterval = 10)
@ApiOperation(value = "生成访问时长", notes = "生成访问时长(定时调用时间就是randomTime单位是秒)")
@PostMapping("/brow/time")
public ResultBean<Object> shareBrowTime(@RequestBody @Valid KbDataFlowVo kbDataFlowVo) {
outInfoService.kbShareBrowTime(kbDataFlowVo.getBrowId(), kbDataFlowVo.getRandomTime(), kbDataFlowVo.getType());
return this.success();
}