美文网首页
基于Redis+Lua+Annotation的分布式应用限流

基于Redis+Lua+Annotation的分布式应用限流

作者: YAOAORAN | 来源:发表于2019-08-13 18:28 被阅读0次

    为什么要做?
    网站免费接口被竞品恶意请求,导致数据量和并发暴增,一开始封禁了他的IP,但是后面他又换了IP来搞我们,没办法只能被迫"应战"。

    为什么不在网关层做?
    也有想过用Nginx,但是我们想要的是更加细粒度的控制和更加灵活的配置。

    Redis+Lua+Annotation限流实例

    引入相关依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
    </dependencies>
    

    Lua脚本

    local key = "rate.limit:" .. KEYS[1] --限流KEY
    local limit = tonumber(ARGV[1])        --限流大小
    local current = tonumber(redis.call('get', key) or "0")
    if current + 1 > limit then --如果超出限流大小
      return 0
    else  --请求数+1,并设置2秒过期
      redis.call("INCRBY", key,"1")
       redis.call("expire", key,"2")
       return current + 1
    end
    

    1、通过KEYS[1] 获取传入的key参数
    2、通过ARGV[1]获取传入的limit参数
    3、redis.call方法,从缓存中get和key相关的值,如果为nil那么就返回0
    4、接着判断缓存中记录的数值是否会大于限制大小,如果超出表示该被限流,返回0
    5、如果未超过,那么该key的缓存值+1,并设置过期时间为1秒钟以后,并返回缓存值+1

    注解(支持SPEL表达式)

    /**
     * 分布式限流注解
     * @since 1.0.0
     */
    @Target(value = {ElementType.METHOD})
    @Retention(value = RetentionPolicy.RUNTIME)
    public @interface RateLimit {
        /**
         * 限流唯一标示 支持spel表达式
         *
         * @return
         */
        String key() default "";
        /**
         * 限流时间 单位:秒
         *
         * @return
         */
        long time();
        /**
         * 限流次数 单位:次
         *
         * @return
         */
        int count();
    }
    

    自动装配

    public class RateLimitAutoConfiguration {
    
        /**
         * 初始化限流脚本
         *
         * @return
         */
        @Bean
        public DefaultRedisScript<Number> redisluaScript() {
            DefaultRedisScript<Number> redisScript = new DefaultRedisScript<>();
            redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("/redisScript/rateLimit.lua")));
            redisScript.setResultType(Number.class);
            return redisScript;
        }
    }
    

    拦截器

    @Aspect
    @Configuration
    @Slf4j
    public class LimitAspect {
    
        @Autowired
        private DefaultRedisScript<Number> redisluaScript;
    
        /**
         * 参数发现器
         */
        private static final ParameterNameDiscoverer NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
        /**
         * Express语法解析器
         */
        private static final ExpressionParser PARSER = new SpelExpressionParser();
    
        @Around("@annotation(RateLimit)")
        public Object interceptor(ProceedingJoinPoint joinPoint) throws Throwable {
            //得到被代理的方法
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();
            Class<?> targetClass = method.getDeclaringClass();
            RateLimit rateLimit = method.getAnnotation(RateLimit.class);
            //得到被切面修饰的方法的参数列表
            Object[] args = joinPoint.getArgs();
    
            if (rateLimit != null) {
                //得到注解的key
                String key = rateLimit.key();
                //清除当前类的缓存
                EvaluationContext context = new MethodBasedEvaluationContext(null, method, args, NAME_DISCOVERER);
                //解析spel表达式
                String realKey = String.valueOf(PARSER.parseExpression(key).getValue(context));
                //获取请求ip
                HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
                String ipAddress = IpUtil.getIpAddr(request);
                //拼接redis-key
                StringBuffer stringBuffer = new StringBuffer();
                stringBuffer.append(ipAddress).append("-")
                        .append(targetClass.getName()).append("-")
                        .append(method.getName()).append("-")
                        .append(realKey);
    
                List<String> keys = Collections.singletonList(stringBuffer.toString());
    
                Number number = RedisUtil.getRedisTemplate().execute(redisluaScript, keys, rateLimit.count(), rateLimit.time());
    
                if (number != null && number.intValue() != 0 && number.intValue() <= rateLimit.count()) {
                    return joinPoint.proceed();
                }
            } else {
                return joinPoint.proceed();
            }
            return RetMsg.fail("访问过于频繁,请稍后再试!");
        }
    }
    

    使用示范

        //每个用户 每60秒可以访问五次  注:subject为登录实体 包含登录信息
        @RateLimit(key = "#subject.user.id", time = 60, count = 5)
        public RetMsg getBaseInfo(Subject subject) {
          //do something
        }
    

    本文参考:
    https://segmentfault.com/a/1190000016042927

    相关文章

      网友评论

          本文标题:基于Redis+Lua+Annotation的分布式应用限流

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