• springboot+Redis+AOP实现请求限流器


    写在开头

    本文参考技术帖 程序员那点事
    主要对学习经验进行总结,也会加上自己的理解注释。

    配置RedisTemplate实例

    //配置redis 使用String数据结构
    //对key  value 进行序列化
    //根据配置连接redis
    @Configuration
    public class RedisLimiterHelper {
    
        @Bean
        public RedisTemplate<String, Serializable> limitRedisTemplate(LettuceConnectionFactory redisConnectionFactory) {
            RedisTemplate<String, Serializable> template = new RedisTemplate<>();
            template.setKeySerializer(new StringRedisSerializer());
            template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
            template.setConnectionFactory(redisConnectionFactory);
            return template;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    限流枚举类

    //针对访客和ip进行限流
    public enum LimitType {
    
        /**
         * 自定义key
         */
        CUSTOMER,
    
        /**
         * 请求者IP
         */
        IP;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    自定义注解,用于AOP切点

    我们自定义个@Limit注解,注解类型为ElementType.METHOD即作用于方法上。

    period表示请求限制时间段,count表示在period这个时间段内允许放行请求的次数。limitType代表限流的类型,可以根据请求的IP、自定义key,如果不传limitType属性则默认用方法名作为默认key

    @Target({ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Inherited
    @Documented
    public @interface Limit {
    
        /**
         * 名字
         */
        String name() default "";
    
        /**
         * key
         */
        String key() default "";
    
        /**
         * Key的前缀
         */
        String prefix() default "";
    
        /**
         * 给定的时间范围 单位(秒)
         */
        int period();
    
        /**
         * 一定时间内最多访问次数
         */
        int count();
    
        /**
         * 限流的类型(用户自定义key 或者 请求ip)
         */
        LimitType limitType() default LimitType.CUSTOMER;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    AOP切面

    执行主要的限流功能

    @Aspect
    @Configuration
    public class LimitInterceptor {
    
        private static final Logger logger = LoggerFactory.getLogger(LimitInterceptor.class);
    
    	//用于ip判断
        private static final String UNKNOWN = "unknown";
    
        private final RedisTemplate<String, Serializable> limitRedisTemplate;
    
    	//构造注入
        @Autowired
        public LimitInterceptor(RedisTemplate<String, Serializable> limitRedisTemplate) {
            this.limitRedisTemplate = limitRedisTemplate;
        }
    
        @Pointcut("execution(public * *(..)) && @annotation(com.limit.api.Limit)")
        public void limitPointCut(){} //切点名称,方便通知使用
        
    	//环绕通知,执行限流业务
    	//@Limit(key = "customer_limit_test", period = 10, count = 3, limitType = LimitType.CUSTOMER) 可根据此注解调用AOP
        @Around("limitPointCut()")
        public Object interceptor(ProceedingJoinPoint pjp) {
            MethodSignature signature = (MethodSignature) pjp.getSignature();
            Method method = signature.getMethod();
            Limit limitAnnotation = method.getAnnotation(Limit.class);
            LimitType limitType = limitAnnotation.limitType();
            String name = limitAnnotation.name();
            String key;
            int limitPeriod = limitAnnotation.period();
            int limitCount = limitAnnotation.count();
    
            /**
             * 根据限流类型获取不同的key ,如果不传我们会以方法名作为key
             * IP类型则使用ipAddress,CUSTOMER则使用注解key
             */
            switch (limitType) {
                case IP:
                    key = getIpAddress();
                    break;
                case CUSTOMER:
                    key = limitAnnotation.key();
                    break;
                default:
                    key = StringUtils.upperCase(method.getName());
            }
    		//将key转化为list 方便后续调用lua的keys参数
            ImmutableList<String> keys = ImmutableList.of(StringUtils.join(limitAnnotation.prefix(), key));
            try {
                String luaScript = buildLuaScript();
                RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);
                Number count = limitRedisTemplate.execute(redisScript, keys, limitCount, limitPeriod);
                logger.info("Access try count is {} for name={} and key = {}", count, name, key);
                //执行控制器Controller的业务逻辑
                if (count != null && count.intValue() <= limitCount) {
                    return pjp.proceed();
                } else {
                    throw new RuntimeException("You have been dragged into the blacklist");
                }
            } catch (Throwable e) {
                if (e instanceof RuntimeException) {
                    throw new RuntimeException(e.getLocalizedMessage());
                }
                throw new RuntimeException("server exception");
            }
        }
    
    	//lua脚本处理限流逻辑
    	//查找key下的访问次数c,若c存在且大于限流值,直接返回
    	//key下的c自增1,若key是第一次新建,给过期时间为限流时间
    	//返回此次访问后的访问次数c
        public String buildLuaScript() {
            StringBuilder lua = new StringBuilder();
            lua.append("local c");
            lua.append("\nc = redis.call('get',KEYS[1])");
            // 调用超过了最大值,则直接返回
            lua.append("\nif c and tonumber(c) > tonumber(ARGV[1]) then");
            lua.append("\nreturn c;");
            lua.append("\nend");
            // 执行计算器自加
            lua.append("\nc = redis.call('incr',KEYS[1])");
            lua.append("\nif tonumber(c) == 1 then");
            // 从第一次调用开始限流,设置对应键值的过期
            lua.append("\nredis.call('expire',KEYS[1],ARGV[2])");
            lua.append("\nend");
            lua.append("\nreturn c;");
            return lua.toString();
        }
    
    
       	//获取ip地址,通过ip限流
        public String getIpAddress() {
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            String ip = request.getHeader("x-forwarded-for");
            if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
                ip = request.getHeader("Proxy-Client-IP");
            }
            if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
                ip = request.getHeader("WL-Proxy-Client-IP");
            }
            if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
                ip = request.getRemoteAddr();
            }
            return ip;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107

    在Controller中可通过注解@Limit的方式调用AOP限流

    如一下分析,业务代码省略

    @Limit(key = "limitTest", period = 10, count = 3)
    key为方法名,10s请求3@Limit(key = "customer_limit_test", period = 10, count = 3, limitType = LimitType.CUSTOMER)
    key为"customer_limit_test"10s请求3@Limit(key = "ip_limit_test", period = 10, count = 3, limitType = LimitType.IP)
    key为ipAddress,10s请求3
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
  • 相关阅读:
    Python如何优雅地可视化目标检测框
    ssm基于Java web 的人人影视网站管理系统毕业设计源码290915
    【强化学习】《动手学强化学习》马尔可夫决策过程
    ElasticSearch之score打分机制原理
    git 基础
    Kotlin File useLines nameWithoutExtension extension
    流媒体播放器EasyPlayer.js无法播放H.265的情况是什么原因?该如何解决?
    Java并发(二十)----synchronized原理进阶
    @Controller与@RestController
    JS-Number数字类型详解
  • 原文地址:https://blog.csdn.net/qq_41138191/article/details/134198737