综合利用CacheBuilder和RateLimiter进行限流控制
需求
系统拦截白名单下有部分接口是公开的,便于外部查询数据,这些接口均需要传输accessKey作为访问钥匙,如下有三种适用场景:
- 没有传输该参数值可第一时间打回;
- 创建错误的accessKey校验直接打回;
- 传输正确的accessKey,查询账号相关信息,然后进行查询操作;
细分思路,最终将上述三种场景划分为:
- 没有传输该参数值可第一时间打回;
- 创建错误的accessKey校验直接打回;
- 第一次传输正确的accessKey,查询账号相关信息,然后进行查询操作(缓存该accessKey并设置过期时间,便于下次直接查询到账号相关信息);
- 再次传输正确的accessKey,直接从缓存中读取账号信息,刷新缓存过期时间,然后进行查询操作;
解决方案
Limiter
功能:创建Limiter接口来定义令牌数量。
示例代码:
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Limiter {
double LimitNum() default 10; //默认每秒产生10个令牌
}
RateLimiterAspect
功能:设置Cache<String, RateLimiter> RATE_LIMITER 来缓存有效的账号信息,五分钟有效,下次访问已有缓存会刷新过期时间。
示例代码:
private static final Logger LOGGER = LoggerFactory.getLogger(RateLimiterAspect.class);
public static final double RATE = 100;
private static final Cache<String, RateLimiter> RATE_LIMITER = CacheBuilder
.newBuilder()
.maximumSize(100L)
.expireAfterAccess(5L, TimeUnit.MINUTES)
.build();
@Pointcut("@annotation(xyz.ddsofcai.www.web.aop.limit.Limiter)")
public void rateLimit() {
}
@Around("rateLimit()")
public Object pointcut(ProceedingJoinPoint point) throws Throwable {
String accessKey = getAccessKey(point);
RateLimiter rateLimiter = RATE_LIMITER.get(accessKey, () -> {
Limiter limiter = getAnnotation(point);
return RateLimiter.create(limiter.LimitNum());
});
if (rateLimiter.tryAcquire()) {
return point.proceed();
}
LOGGER.info("频繁请求限制 accessKey: {}, methodName: {}", accessKey, point.getSignature().getName());
throw new BizException("频繁请求限制");
}
private Limiter getAnnotation(ProceedingJoinPoint point) throws NoSuchMethodException {
//获取拦截的方法名
Signature sig = point.getSignature();
//获取拦截的方法名
MethodSignature msig = (MethodSignature) sig;
//返回被织入增加处理目标对象
Object target = point.getTarget();
//为了获取注解信息
Method method = target.getClass()
.getMethod(msig.getName(), msig.getParameterTypes());
//获取注解信息
return method.getAnnotation(Limiter.class);
}
private String getAccessKey(ProceedingJoinPoint point) {
return Arrays.stream(point.getArgs())
.filter(e -> e instanceof AccessKeyParam)
.findFirst()
.map(o -> ((AccessKeyParam) o).getAccessKey())
.orElseThrow(() -> new BizException("accessKey不能为空"));
}
}
注解使用
@GetMapping("/report")
@Limiter(LimitNum = RateLimiterAspect.RATE)
public Page<Map<String, Object>> report(ReportPageQuery pageQuery) {
return reportService.report(pageQuery);
}