需求:要求同一个接口同一时间 仅能处理N个请求,超过就返回;不是时间窗口也不是秒杀,接口处理完成后又可以继续接收请求;
分析:有点像令牌桶算法,但是并不是每隔一段时间放几个令牌,而是永远只有这几个令牌;计划采用redis的increase方式,设定一个最大值Max,每次请求先判定当前key是否超过Max,如果已经超过就返回,没有超过就自增,分布式可用;
实现:
创建一个注解类AccessLimit ,key是需要需要自增的redis中的key,可以由接口提供,limit就是最大值
@Inherited
@Documented
@Target({ElementType.FIELD, ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AccessLimit {
/**
* 标识 指定sec时间段内的访问次数限制
*/
String key() default "default";
/**
* 标识 指定sec时间段内的访问次数限制
*/
int limit() default 5;
/**
* 标识 时间段
**/
int sec() default 5;
}
controller中写法如下,#type 表示获取参数名称为type的值
@AccessLimit(key = "#type")
@PostMapping("/single")
public CloudwalkResult<String> uploadAttach(@RequestParam("file") MultipartFile file,
@RequestParam(value = "type", required = false, defaultValue = "default") String type) {
// 附件的访问地址
return uploadService.uploadSingleFile(file, type);
}
对应的切面类如下
@Aspect
@Component
public class AccessLimitAspect {
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private CacheHandler cacheHandler;
/**
* 这里我们使用注解的形式
* 当然,我们也可以通过切点表达式直接指定需要拦截的package,需要拦截的class 以及 method
* 切点表达式: execution(...)
*/
@Pointcut("@annotation(cn.cloudwalk.developer.center.web.common.aspect.AccessLimit)")
public void logPointCut() {
}
/**
* 环绕通知 @Around , 当然也可以使用 @Before (前置通知) @After (后置通知)
*
* @param pjp
* @return
* @throws Throwable
*/
@Around("logPointCut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
// 获取注解的方法参数列表
Object[] args = pjp.getArgs();
// 获取被注解的方法
MethodInvocationProceedingJoinPoint mjp = (MethodInvocationProceedingJoinPoint) pjp;
MethodSignature signature = (MethodSignature) mjp.getSignature();
Method method = signature.getMethod();
String[] parameterNames = signature.getParameterNames();
// 获取方法上的注解
try {
AccessLimit accessLimit = method.getAnnotation(AccessLimit.class);
if (accessLimit == null) {
// 如果没有注解
return pjp.proceed();
}
int limit = accessLimit.limit();
String key = accessLimit.key();
if (key.startsWith("#")) {
key = key.replaceFirst("#", "");
}
String keyTemp = key;
// 获取方法上的key的值
try {
for (int i = 0; i < parameterNames.length; i++) {
if (parameterNames[i].equals(key)) {
key = (String) args[i];
break;
}
}
} catch (Exception e) {
key = keyTemp;
}
if (!cacheHandler.limitForRequest(key, limit)) {
return Result.fail(ServiceCodeConstants.UP_IS_ERROR, "同一时间上传请求过多,请稍后再试");
}
} catch (Exception e) {
logger.error("AccessLimit 切面方法失败", e);
}
return pjp.proceed();
}
}
这里仅是加+ ,需要在业务代码中减一,如下 核心代码 cacheHandler.releaseForRequest(type);
@Override
public CloudwalkResult<String> uploadSingleFile(MultipartFile file, String type) {
String imgurl = "";
String fileUploadPath = IMAGES_PATH + type;
// 上传
try {
...
} catch (IOException e) {
logger.error("附件访问地址获取异常,原因:", e);
} finally {
cacheHandler.releaseForRequest(type);
}
return CloudwalkResult.success(imgurl);
}