接口的屏蔽和限流很难么?Redis全搞定
需求
线上出现的问题是,一些非核心的查询数据业务,在请求超时或者错误的时候,用户会越查询,导致数据库cup飙升,拖垮核心的业务。
领导让我做三件事,一是把这些接口做一个限流,这些限流参数是可配的,第二是这些接口可以设置开关,当发现问题时,可以手动关闭这些接口,不至于数据库压力过大影响核心业务的服务。第三是做接口的熔断,熔断设置可以配置。
经过确定,前两个实现用redis来实现,第三个因为熔断讨论觉得比较复杂,决定采用我提出的用Hystrix,目前项目不能热加载生效配置中心的最新的配置,所以后期推荐使用Archaius,这些网上查到的,具体为啥不选其他的,原因就是其他的比较复杂,上手感觉这个最快。
这篇文章说实现,其他问题不涉及,请多多指教。思路
接口的屏蔽:通过AOP实现,每次访问接口的时候,通过接口的Key值,在Redis取到接口设置开关值,如果打开继续,否在拒绝。接口限流也是基于AOP,根据接口的Key值,取到这个接口的限流值,表示多长时间,限流几次,每次访问都会请求加一,通过比较,如果超过限制再返回,否在继续。代码
AccessLimiter接口,主要有两类方法,是否开启限流,取Redis中的限流值。packagecom。hcfc。auto。util。limit;importjava。util。concurrent。TimeUnit;创建人peng。wang描述访问限制器publicinterfaceAccessLimiter{检查指定的key是否收到访问限制paramkey限制接口的标识paramtimes访问次数paramper一段时间paramunit时间单位returnpublicbooleanisLimited(Stringkey,longtimes,longper,TimeUnitunit);移除访问限制paramkeypublicvoidrefreshLimited(Stringkey);接口是否打开returnpublicbooleanisStatus(StringredisKey);接口的限流大小paramredisKeyTimesreturnpubliclonggetTimes(StringredisKeyTimes);接口限流时间段paramredisKeyPerreturnpubliclonggetPer(StringredisKeyPer);接口的限流时间单位paramredisKeyUnitreturnpublicTimeUnitgetUnit(StringredisKeyUnit);是否删除接口限流paramredisKeyIsRefreshreturnpublicbooleangetIsRefresh(StringredisKeyIsRefresh);}
RedisAccessLimiter是AccessLimiter接口的实现类packagecom。hcfc。auto。util。limit;importorg。slf4j。Logger;importorg。slf4j。LoggerFactory;importorg。springframework。beans。factory。annotation。Autowired;importorg。springframework。data。redis。core。RedisTemplate;importorg。springframework。stereotype。Component;importjava。util。concurrent。TimeUnit;创建人peng。wang描述基于Redis的实现ComponentpublicclassRedisAccessLimiterimplementsAccessLimiter{privatestaticfinalLoggerLOGGERLoggerFactory。getLogger(RedisAccessLimiter。class);AutowiredprivateRedisTemplateredisTemplate;OverridepublicbooleanisLimited(Stringkey,longtimes,longper,TimeUnitunit){LongcurTimesredisTemplate。boundValueOps(key)。increment(1);LOGGER。info(curTimes{},curTimes);if(curTimestimes){LOGGER。debug(超频访问:〔{}〕,key);returntrue;}else{if(curTimes1){LOGGER。info(setexpire);redisTemplate。boundValueOps(key)。expire(per,unit);returnfalse;}else{returnfalse;}}}OverridepublicvoidrefreshLimited(Stringkey){redisTemplate。delete(key);}OverridepublicbooleanisStatus(StringredisKey){try{return(boolean)redisTemplate。opsForValue()。get(redisKeyIsOn);}catch(Exceptione){LOGGER。info(redisKeyisnotfindortypemismatch,key:,redisKey);returnfalse;}}OverridepubliclonggetTimes(StringredisKeyTimes){try{return(long)redisTemplate。opsForValue()。get(redisKeyTimesTimes);}catch(Exceptione){LOGGER。info(redisKeyisnotfindortypemismatch,key:,redisKeyTimes);return0;}}OverridepubliclonggetPer(StringredisKeyPer){try{return(long)redisTemplate。opsForValue()。get(redisKeyPerPer);}catch(Exceptione){LOGGER。info(redisKeyisnotfindortypemismatch,key:,redisKeyPer);return0;}}OverridepublicTimeUnitgetUnit(StringredisKeyUnit){try{return(TimeUnit)redisTemplate。opsForValue()。get(redisKeyUnitUnit);}catch(Exceptione){LOGGER。info(redisKeyisnotfindortypemismatch,key:,redisKeyUnit);returnTimeUnit。SECONDS;}}OverridepublicbooleangetIsRefresh(StringredisKeyIsRefresh){try{return(boolean)redisTemplate。opsForValue()。get(redisKeyIsRefreshIsRefresh);}catch(Exceptione){LOGGER。info(redisKeyisnotfindortypemismatch,key:,redisKeyIsRefresh);returnfalse;}}}
Limit标签接口,实现注解方式packagecom。hcfc。auto。util。limit;importjava。lang。annotation。;创建人peng。wang描述Target({ElementType。METHOD,ElementType。ANNOTATIONTYPE})Retention(RetentionPolicy。RUNTIME)DocumentedpublicinterfaceLimit{}
LimitAspect切面的实现,实现接口屏蔽和限流的逻辑packagecom。hcfc。auto。util。limit;importcom。hcfc。auto。vo。response。ResponseDto;importlombok。extern。slf4j。Slf4j;importorg。aspectj。lang。ProceedingJoinPoint;importorg。aspectj。lang。annotation。Around;importorg。aspectj。lang。annotation。Aspect;importorg。aspectj。lang。annotation。Pointcut;importorg。aspectj。lang。reflect。MethodSignature;importorg。slf4j。Logger;importorg。slf4j。LoggerFactory;importorg。springframework。beans。factory。annotation。Autowired;importorg。springframework。stereotype。Component;importorg。springframework。web。bind。annotation。RequestMapping;importjava。lang。reflect。Method;importjava。util。concurrent。TimeUnit;创建人peng。wang创建时间20191011描述Slf4jAspectComponentpublicclassLimitAspect{privatestaticfinalLoggerloggerLoggerFactory。getLogger(LimitAspect。class);AutowiredprivateAccessLimiterlimiter;AutowiredGenerateRedisKeygenerateRedisKey;Pointcut(annotation(com。hcfc。auto。util。limit。Limit))publicvoidlimitPointcut(){}Around(limitPointcut())publicObjectdoArround(ProceedingJoinPointjoinPoint)throwsThrowable{StringredisKeygenerateRedisKey。getMethodUrlConvertRedisKey(joinPoint);longperlimiter。getPer(redisKey);longtimeslimiter。getTimes(redisKey);TimeUnitunitlimiter。getUnit(redisKey);booleanisRefreshlimiter。getIsRefresh(redisKey);booleanmethodLimitStatuslimiter。isStatus(redisKey);StringbindingKeygenBindingKey(joinPoint);if(methodLimitStatus){logger。info(methodisclosed,key:,bindingKey);returnResponseDto。fail(40007,methodisclosed,key:bindingKey);thrownewOverLimitException(methodisclosed,key:bindingKey);}if(bindingKey!null){booleanisLimitedlimiter。isLimited(bindingKey,times,per,unit);if(isLimited){logger。info(limittakeseffect:{},bindingKey);returnResponseDto。fail(40006,accessoverlimit,key:bindingKey);thrownewOverLimitException(accessoverlimit,key:bindingKey);}}Objectresultnull;resultjoinPoint。proceed();if(bindingKey!nullisRefresh){limiter。refreshLimited(bindingKey);logger。info(limitrefreshed:{},bindingKey);}returnresult;}privateStringgenBindingKey(ProceedingJoinPointjoinPoint){try{Methodm((MethodSignature)joinPoint。getSignature())。getMethod();returnjoinPoint。getTarget()。getClass()。getName()。m。getName();}catch(Throwablee){returnnull;}}}
还有一个不重要的RedisKey实现类GenerateRedisKey和一个错误封装类,目前没有使用到,使用项目中其他的错误封装类了。
GenerateRedisKeypackagecom。hcfc。auto。util。limit;importorg。aspectj。lang。ProceedingJoinPoint;importorg。aspectj。lang。reflect。MethodSignature;importorg。springframework。stereotype。Component;importorg。springframework。web。bind。annotation。RequestMapping;importjava。lang。reflect。Method;创建人peng。wang描述ComponentpublicclassGenerateRedisKey{publicStringgetMethodUrlConvertRedisKey(ProceedingJoinPointjoinPoint){StringBuilderredisKeynewStringBuilder();Methodm((MethodSignature)joinPoint。getSignature())。getMethod();RequestMappingmethodAnnotationm。getAnnotation(RequestMapping。class);if(methodAnnotation!null){String〔〕methodValuemethodAnnotation。value();StringdscUrldiagonalLineToCamel(methodValue〔0〕);returnredisKey。append(RSK:)。append(interfaceIsOpen:)。append(dscUrl)。toString();}returnredisKey。toString();}privateStringdiagonalLineToCamel(Stringparam){charUNDERLINE;if(paramnull。equals(param。trim())){return;}intlenparam。length();StringBuildersbnewStringBuilder(len);for(inti1;ilen;i){charcparam。charAt(i);if(cUNDERLINE){if(ilen){sb。append(Character。toUpperCase(param。charAt(i)));}}else{sb。append(c);}}returnsb。toString();}}总结
关键的代码也就这几行,访问之前,对这个key值加一的操作,判断是否超过限制,如果等于一这个key加一之后的值为一,说明之前不存在,则设置这个key,放在Redis数据库中。
其实有更成熟的方案就是谷歌的Guava,领导说现在是咱们是分布式,不支持,还是用Redis实现吧,目前就这样实现了。其实我是新来的,好多东西还不太明白,很多决定都是上面决定的,我只是尽力实现罢了。不足之处,请多多指教!作者:IngramMSN
来源:blog。csdn。netu010843114articledetails102695570