美文网首页
接口的屏蔽和限流很难么?Redis全搞定!

接口的屏蔽和限流很难么?Redis全搞定!

作者: 小郭子 | 来源:发表于2022-03-03 13:07 被阅读0次

    来源:公众号 作者:Java知音
    链接:https://mp.weixin.qq.com/s/zWaggYOwaD-PVJsZd8zkjg

    需求

    线上出现的问题是,一些非核心的查询数据业务,在请求超时或者错误的时候,用户会越查询,导致数据库cup飙升,拖垮核心的业务。

    领导让我做三件事,一是把这些接口做一个限流,这些限流参数是可配的,第二是这些接口可以设置开关,当发现问题时,可以手动关闭这些接口,不至于数据库压力过大影响核心业务的服务。第三是做接口的熔断,熔断设置可以配置。

    经过确定,前两个实现用redis来实现,第三个因为熔断讨论觉得比较复杂,决定采用我提出的用Hystrix,目前项目不能热加载生效配置中心的最新的配置,所以后期推荐使用Archaius,这些网上查到的,具体为啥不选其他的,原因就是其他的比较复杂,上手感觉这个最快。

    这篇文章说实现,其他问题不涉及,请多多指教。

    思路

    接口的屏蔽:通过AOP实现,每次访问接口的时候,通过接口的Key值,在Redis取到接口设置开关值,如果打开继续,否在拒绝。接口限流也是基于AOP,根据接口的Key值,取到这个接口的限流值,表示多长时间,限流几次,每次访问都会请求加一,通过比较,如果超过限制再返回,否在继续。

    代码

    AccessLimiter接口,主要有两类方法,是否开启限流,取Redis中的限流值

    package com.hcfc.auto.util.limit;
     
    import java.util.concurrent.TimeUnit;
     
    /**
     * @创建人 peng.wang
     * @描述 访问限制器
     */
    public interface AccessLimiter {
        /**
         * 检查指定的key是否收到访问限制
         * @param key   限制接口的标识
         * @param times 访问次数
         * @param per   一段时间
         * @param unit  时间单位
         * @return
         */
        public boolean isLimited(String key, long times, long per, TimeUnit unit);
     
        /**
         * 移除访问限制
         * @param key
         */
        public void refreshLimited(String key);
     
        /**
         * 接口是否打开
         * @return
         */
        public boolean isStatus(String redisKey);
     
        /**
         * 接口的限流大小
         * @param redisKeyTimes
         * @return
         */
        public long getTimes(String redisKeyTimes);
     
        /**
         * 接口限流时间段
         * @param redisKeyPer
         * @return
         */
        public long getPer(String redisKeyPer);
     
        /**
         * 接口的限流时间单位
         * @param redisKeyUnit
         * @return
         */
        public TimeUnit getUnit(String redisKeyUnit);
     
        /**
         * 是否删除接口限流
         * @param redisKeyIsRefresh
         * @return
         */
        public boolean getIsRefresh(String redisKeyIsRefresh);
    }
    

    RedisAccessLimiter是AccessLimiter接口的实现类

    package com.hcfc.auto.util.limit;
     
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.stereotype.Component;
     
    import java.util.concurrent.TimeUnit;
     
    /**
     * @创建人 peng.wang
     * @描述 基于Redis的实现
     */
    @Component
    public class RedisAccessLimiter implements AccessLimiter {
     
        private static final Logger LOGGER = LoggerFactory.getLogger(RedisAccessLimiter.class);
     
        @Autowired
        private RedisTemplate redisTemplate;
     
        @Override
        public boolean isLimited(String key, long times, long per, TimeUnit unit) {
            Long curTimes = redisTemplate.boundValueOps(key).increment(1);
            LOGGER.info("curTimes {}",curTimes);
            if(curTimes > times) {
                LOGGER.debug("超频访问:[{}]",key);
                return true;
            } else {
                if(curTimes == 1) {
                    LOGGER.info(" set expire ");
                    redisTemplate.boundValueOps(key).expire(per, unit);
                    return false;
                } else {
                    return false;
                }
            }
        }
     
        @Override
        public void refreshLimited(String key) {
            redisTemplate.delete(key);
        }
     
        @Override
        public boolean isStatus(String redisKey) {
            try {
                return (boolean)redisTemplate.opsForValue().get(redisKey+"IsOn");
            }catch (Exception e){
                LOGGER.info("redisKey is not find or type mismatch, key: ", redisKey);
                return false;
            }
        }
     
        @Override
        public long getTimes(String redisKeyTimes) {
            try {
                return (long)redisTemplate.opsForValue().get(redisKeyTimes+"Times");
            }catch (Exception e){
                LOGGER.info("redisKey is not find or type mismatch, key: ", redisKeyTimes);
                return 0;
            }
        }
     
        @Override
        public long getPer(String redisKeyPer) {
            try {
                return (long)redisTemplate.opsForValue().get(redisKeyPer+"Per");
            }catch (Exception e){
                LOGGER.info("redisKey is not find or type mismatch, key: ", redisKeyPer);
                return 0;
            }
        }
     
        @Override
        public TimeUnit getUnit(String redisKeyUnit) {
            try {
                return (TimeUnit) redisTemplate.opsForValue().get(redisKeyUnit+"Unit");
            }catch (Exception e){
                LOGGER.info("redisKey is not find or type mismatch, key: ", redisKeyUnit);
                return TimeUnit.SECONDS;
            }
        }
     
        @Override
        public boolean getIsRefresh(String redisKeyIsRefresh) {
            try {
                return (boolean)redisTemplate.opsForValue().get(redisKeyIsRefresh+"IsRefresh");
            }catch (Exception e){
                LOGGER.info("redisKey is not find or type mismatch, key: ", redisKeyIsRefresh);
                return false;
            }
        }
    }
    

    Limit标签接口,实现注解方式

    package com.hcfc.auto.util.limit;
     
    import java.lang.annotation.*;
     
    /**
     * @创建人 peng.wang
     * @描述
     */
    @Target({ElementType.METHOD,ElementType.ANNOTATION_TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface Limit {}
    

    LimitAspect 切面的实现,实现接口屏蔽和限流的逻辑

    package com.hcfc.auto.util.limit;
     
    import com.hcfc.auto.vo.response.ResponseDto;
    import lombok.extern.slf4j.Slf4j;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    import org.springframework.web.bind.annotation.RequestMapping;
     
    import java.lang.reflect.Method;
    import java.util.concurrent.TimeUnit;
     
    /**
     * @创建人 peng.wang
     * @创建时间 2019/10/11
     * @描述
     */
    @Slf4j
    @Aspect
    @Component
    public class LimitAspect {
        private static final Logger logger = LoggerFactory.getLogger(LimitAspect.class);
     
        @Autowired
        private AccessLimiter limiter;
     
        @Autowired
        GenerateRedisKey generateRedisKey;
     
        @Pointcut("@annotation(com.hcfc.auto.util.limit.Limit)")
        public void limitPointcut() {}
     
        @Around("limitPointcut()")
        public Object doArround(ProceedingJoinPoint joinPoint) throws Throwable {
            String redisKey = generateRedisKey.getMethodUrlConvertRedisKey(joinPoint);
            long per = limiter.getPer(redisKey);
            long times = limiter.getTimes(redisKey);
            TimeUnit unit = limiter.getUnit(redisKey);
            boolean isRefresh =limiter.getIsRefresh(redisKey);
            boolean methodLimitStatus = limiter.isStatus(redisKey);
            String bindingKey = genBindingKey(joinPoint);
            if (methodLimitStatus) {
                logger.info("method is closed, key: ", bindingKey);
                return ResponseDto.fail("40007", "method is closed, key:"+bindingKey);
                //throw new OverLimitException("method is closed, key: "+bindingKey);
            }
            if(bindingKey !=null){
                boolean isLimited = limiter.isLimited(bindingKey, times, per, unit);
                if(isLimited){
                    logger.info("limit takes effect: {}", bindingKey);
                    return ResponseDto.fail("40006", "access over limit, key: "+bindingKey);
                    //throw new OverLimitException("access over limit, key: "+bindingKey);
                }
            }
            Object result = null;
            result = joinPoint.proceed();
            if(bindingKey!=null && isRefresh) {
                limiter.refreshLimited(bindingKey);
                logger.info("limit refreshed: {}", bindingKey);
            }
            return result;
        }
     
        private String genBindingKey(ProceedingJoinPoint joinPoint){
            try{
                Method m = ((MethodSignature) joinPoint.getSignature()).getMethod();
                return joinPoint.getTarget().getClass().getName() + "." + m.getName();
            }catch (Throwable e){
                return null;
            }
        }
    }
    

    还有一个不重要的RedisKey实现类GenerateRedisKey和一个错误封装类,目前没有使用到,使用项目中其他的错误封装类了

    GenerateRedisKey

    package com.hcfc.auto.util.limit;
     
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.stereotype.Component;
    import org.springframework.web.bind.annotation.RequestMapping;
     
    import java.lang.reflect.Method;
     
    /**
     * @创建人 peng.wang
     * @描述
     */
    @Component
    public class GenerateRedisKey {
     
        public String getMethodUrlConvertRedisKey(ProceedingJoinPoint joinPoint){
            StringBuilder redisKey =new StringBuilder("");
            Method m = ((MethodSignature)joinPoint.getSignature()).getMethod();
            RequestMapping methodAnnotation = m.getAnnotation(RequestMapping.class);
            if (methodAnnotation != null) {
                String[] methodValue = methodAnnotation.value();
                String dscUrl = diagonalLineToCamel(methodValue[0]);
                return redisKey.append("RSK:").append("interfaceIsOpen:").append(dscUrl).toString();
            }
            return redisKey.toString();
        }
        private String diagonalLineToCamel(String param){
            char UNDERLINE='/';
            if (param==null||"".equals(param.trim())){
                return "";
            }
            int len=param.length();
            StringBuilder sb=new StringBuilder(len);
            for (int i = 1; i < len; i++) {
                char c=param.charAt(i);
                if (c==UNDERLINE){
                    if (++i<len){
                        sb.append(Character.toUpperCase(param.charAt(i)));
                    }
                }else{
                    sb.append(c);
                }
            }
            return sb.toString();
        }
    }
    

    总结

    关键的代码也就这几行,访问之前,对这个key值加一的操作,判断是否超过限制,如果等于一这个key加一之后的值为一,说明之前不存在,则设置这个key,放在Redis数据库中。

    其实有更成熟的方案就是谷歌的Guava,领导说现在是咱们是分布式,不支持,还是用Redis实现吧,目前就这样实现了。其实我是新来的,好多东西还不太明白,很多决定都是上面决定的,我只是尽力实现罢了。不足之处,请多多指教!

    相关文章

      网友评论

          本文标题:接口的屏蔽和限流很难么?Redis全搞定!

          本文链接:https://www.haomeiwen.com/subject/pbjdrrtx.html