通过redis阻止接口被多次调用

背景

我们服务器提供的接口,访问的客户端有移动端,web端,小程序,H5等。随着我们移动端业务越做越复杂,比如我们是做智能台灯硬件业务的,有块核心的业务是状态机,其中智能台灯的状态有10多种, 每个状态变更后APP都需要把整个设备列表接口都查一遍,该接口是整个核心业务,不乏涉及到很多的关联表,移动端逻辑肯定是要修改的, 目前我想通过云端的限制阻止单位时间过多次数的接口调用。

接口限制处理

1、注解类定义

/**
 * @Description: 用于全局限制接口调用次数,防止接口同一时间多次无意义调用
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface GlobalRequestLimit {

    /**
     * 限制重复提交次数 , 默认单位时间内只限制一次请求
     * @return
     */
    long count() default 1L;

    /**
     * 默认重复请求的拦截时间是5S
     * @return
     */
    long time() default 5L;
}

2、AOP类实现

@Order(1)
@Aspect
@Component
public class GlobalRequestLimitAspect {

    /**
     * 业务缓存通用前缀
     */
    final private static String PRI_KEY = "s:limit:";


    @Autowired
    private RedisClient redisClient;

    @Pointcut("execution(* com.xxx.controller.*.*( ..))")
    public void pointcut() {
    }

    @Pointcut("@annotation( com.xxx.GlobalRequestLimit )")
    public void limitPointcut() {}


    @Around(" pointcut() && limitPointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        if(Objects.nonNull(joinPoint.getArgs()) && joinPoint.getArgs().length > 0 ){
          //获取请求的参数
            String paramJson = JSON.toJSONString(joinPoint.getArgs()[0]);
          //将请求参数转成字符串,生成hash值
            String cacheKey = PRI_KEY.concat( Math.abs(paramJson.hashCode()) + "" );
          //获取限流注解类
            GlobalRequestLimit cloudGlobalCache = methodSignature.getMethod().getAnnotation(GlobalRequestLimit.class);
        	 //获取接口中配置的单位时间内限制请求次数
          Long limitRequestCountValue = cloudGlobalCache.count();
            log.info( "重复提交判断cacheKey={} ,limitRequestCountValue={} " , cacheKey , limitRequestCountValue );
            //当前的业务只需要对单位时间限制即可,无需考虑续时的情况
            Boolean b = redisClient.setNX( cacheKey , 1 , cloudGlobalCache.time() ,TimeUnit.SECONDS );
            //没有获取到锁
            if(!b){
                //请求接口是否配置了允许多次请求?
                if( limitRequestCountValue > 1 ){
                  	//获取当前请求单位时间的请求次数
                    Integer sourceLimitValue = redisClient.get( cacheKey );
                  //拦截处理
                    if( sourceLimitValue != null && sourceLimitValue >= limitRequestCountValue){
                        log.warn( "当前用户请求重复被拒接 param={} ,url={}" , JSON.toJSONString( paramJson ) ,getRequestUrl() );
                        return Result.error(ResponseCodeEnum.REQUEST_REPEAT.getCode() ,ResponseCodeEnum.REQUEST_REPEAT.getName() );
                    }
                    //将请求的次数进行保存
                    redisClient.increment(cacheKey ) ;
                }else{
                    log.warn( "当前用户请求重复被拒接 param={} ,url={}" , JSON.toJSONString( paramJson ) ,getRequestUrl() );
                    return Result.error(ResponseCodeEnum.REQUEST_REPEAT.getCode() ,ResponseCodeEnum.REQUEST_REPEAT.getName() );
                }
            }
        }
       //请求放行
        return joinPoint.proceed();
    }

    private String getRequestUrl(){
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        return request.getRequestURI();
    }

}

总结

我们通过注解及内部参数的定义,给出了拦截接口请求限制的定义,通过AOP切面的方式做一些计算,如果参数转字符串的hash值是一样的,我们认为当前的请求是重复调用的,如果符合单位事件调用次数,我们将存取到redis中,对应的单位时间就是注解中定义的单位时间,单位这种做法不是最好的, 但是在业务紧急开发的情况下,保证重量型接口阻止单位时间内过的调用次数是比较有效。后续我们移动端的优化会采用事件驱动模型,本地更新+接口请求更新模式结合来进行优化。

发表评论
留言与评论(共有 0 条评论) “”
   
验证码:

相关文章

推荐文章