Spring AOP实战开发(四)AOP+Redis实现接口限流
需求:要求同一个接口同一时间 仅能处理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 uploadAttach(@RequestParam("file") MultipartFile file, @RequestParam(value = "type", required = false, defaultValue = "default") String type) { // 附件的访问地址 return uploadService.uploadSingleFile(file, type); }
对应的切面类如下
@Aspect@Componentpublic 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 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); }