分布式---基于Redis进行接口IP限流

场景:为了防止我们的接口被人恶意访问,比如有人通过JMeter工具频繁访问我们的接口,导致接口响应变慢甚至崩溃,所以我们需要对一些特定的接口进行IP限流,即一定时间内同一IP访问的次数是有限的。

实现原理:用Redis作为限流组件的核心的原理,将用户的IP地址当Key,一段时间内访问次数为value,同时设置该Key过期时间。

比如某接口设置相同IP10秒内请求5次,超过5次不让访问该接口。

1 第一次该IP地址存入redis的时候,key值为IP地址,value值为1,设置key值过期时间为10秒。
2 第二次该IP地址存入redis时,如果key没有过期,那么更新value为2。
3 以此类推当value已经为5时,如果下次该IP地址在存入redis同时key还没有过期,那么该Ip就不能访问了。
4 当10秒后,该key值过期,那么该IP地址再进来,value又从1开始,过期时间还是10秒,这样反反复复。

说明从上面的逻辑可以看出,是一时间段内访问次数受限,不是完全不让该IP访问接口。

技术框架 SpringBoot + RedisTemplate (采用自定义注解完成)

这个可以用于真实项目开发场景。

一、代码

1、自定义注解

这边采用自定义注解的目的就是,在接口上使用自定义注解,让代码看去非常整洁。

IpLimiter

 1 @Target(ElementType.METHOD)
 2 @Retention(RetentionPolicy.RUNTIME)
 3 @Documented
 4 public @interface IpLimiter {
 5     /**
 6      * 限流ip
 7      */
 8     String ipAdress() ;
 9     /**
10      * 单位时间限制通过请求数
11      */
12     long limit() default 10;
13     /**
14      * 单位时间,单位秒
15      */
16     long time() default 1;
17     /**
18      * 达到限流提示语
19      */
20     String message();
21 }

2、测试接口

在接口上使用了自定义注解@IpLimiter

 1 @Controller
 2 public class IpController {
 3     
 4     private static final Logger LOGGER = LoggerFactory.getLogger(IpController.class);
 5     private static final String MESSAGE = "请求失败,你的IP访问太频繁";
 6  
 7     //这里就不获取请求的ip,而是写死一个IP
 8     @ResponseBody
 9     @RequestMapping("iplimiter")
10     @IpLimiter(ipAdress = "127.198.66.01", limit = 5, time = 10, message = MESSAGE)
11     public String sendPayment(HttpServletRequest request) throws Exception {
12         return "请求成功";
13     }
14     @ResponseBody
15     @RequestMapping("iplimiter1")
16     @IpLimiter(ipAdress = "127.188.145.54", limit = 4, time = 10, message = MESSAGE)
17     public String sendPayment1(HttpServletRequest request) throws Exception {
18         return "请求成功";
19     }
20 }

3、处理IpLimiter注解的AOP

这边采用切面的方式处理自定义注解。同时为了保证原子性,这边写了redis脚本ipLimiter.lua来执行redis命令,来保证操作原子性。

 1 @Aspect
 2 @Component
 3 public class IpLimterHandler {
 4  
 5     private static final Logger LOGGER = LoggerFactory.getLogger(IpLimterHandler.class);
 6  
 7     @Autowired
 8     RedisTemplate redisTemplate;
 9  
10     /**
11      * getRedisScript 读取脚本工具类
12      * 这里设置为Long,是因为ipLimiter.lua 脚本返回的是数字类型
13      */
14     private DefaultRedisScript<Long> getRedisScript;
15  
16     @PostConstruct
17     public void init() {
18         getRedisScript = new DefaultRedisScript<>();
19         getRedisScript.setResultType(Long.class);
20         getRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("ipLimiter.lua")));
21         LOGGER.info("IpLimterHandler[分布式限流处理器]脚本加载完成");
22     }
23  
24     /**
25      * 这个切点可以不要,因为下面的本身就是个注解
26      */
27 //    @Pointcut("@annotation(com.jincou.iplimiter.annotation.IpLimiter)")
28 //    public void rateLimiter() {}
29  
30     /**
31      * 如果保留上面这个切点,那么这里可以写成
32      * @Around("rateLimiter()&&@annotation(ipLimiter)")
33      */
34     @Around("@annotation(ipLimiter)")
35     public Object around(ProceedingJoinPoint proceedingJoinPoint, IpLimiter ipLimiter) throws Throwable {
36         if (LOGGER.isDebugEnabled()) {
37             LOGGER.debug("IpLimterHandler[分布式限流处理器]开始执行限流操作");
38         }
39         Signature signature = proceedingJoinPoint.getSignature();
40         if (!(signature instanceof MethodSignature)) {
41             throw new IllegalArgumentException("the Annotation @IpLimter must used on method!");
42         }
43         /**
44          * 获取注解参数
45          */
46         // 限流模块IP
47         String limitIp = ipLimiter.ipAdress();
48         Preconditions.checkNotNull(limitIp);
49         // 限流阈值
50         long limitTimes = ipLimiter.limit();
51         // 限流超时时间
52         long expireTime = ipLimiter.time();
53         if (LOGGER.isDebugEnabled()) {
54             LOGGER.debug("IpLimterHandler[分布式限流处理器]参数值为-limitTimes={},limitTimeout={}", limitTimes, expireTime);
55         }
56         // 限流提示语
57         String message = ipLimiter.message();
58         /**
59          * 执行Lua脚本
60          */
61         List<String> ipList = new ArrayList();
62         // 设置key值为注解中的值
63         ipList.add(limitIp);
64         /**
65          * 调用脚本并执行
66          */
67         Long result = (Long) redisTemplate.execute(getRedisScript, ipList, expireTime, limitTimes);
68         if (result == 0) {
69             String msg = "由于超过单位时间=" + expireTime + "-允许的请求次数=" + limitTimes + "[触发限流]";
70             LOGGER.debug(msg);
71             // 达到限流返回给前端信息
72             return message;
73         }
74         if (LOGGER.isDebugEnabled()) {
75             LOGGER.debug("IpLimterHandler[分布式限流处理器]限流执行结果-result={},请求[正常]响应", result);
76         }
77         return proceedingJoinPoint.proceed();
78     }
79 }

4、RedisCacheConfig(配置类)

 1 @Configuration
 2 public class RedisCacheConfig {
 3  
 4     private static final Logger LOGGER = LoggerFactory.getLogger(RedisCacheConfig.class);
 5  
 6     @Bean
 7     public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
 8         RedisTemplate<String, Object> template = new RedisTemplate<>();
 9         template.setConnectionFactory(factory);
10  
11         //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
12         Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);
13  
14         ObjectMapper mapper = new ObjectMapper();
15         mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
16         mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
17         serializer.setObjectMapper(mapper);
18  
19         template.setValueSerializer(serializer);
20         //使用StringRedisSerializer来序列化和反序列化redis的key值
21         template.setKeySerializer(new StringRedisSerializer());
22         template.afterPropertiesSet();
23         LOGGER.info("Springboot RedisTemplate 加载完成");
24         return template;
25     }
26 }

5、IpLimiter.lua脚本

优点
减少网络的开销:   脚本只执行一次,不需要发送多次请求, 减少网络传输;
保证原子操作:   整个脚本作为一个原子执行, 就不用担心并发问题;

 1 --获取KEY
 2 local key1 = KEYS[1]
 3  
 4 local val = redis.call('incr', key1)
 5 local ttl = redis.call('ttl', key1)
 6  
 7 --获取ARGV内的参数并打印
 8 local expire = ARGV[1]
 9 local times = ARGV[2]
10  
11 redis.log(redis.LOG_DEBUG,tostring(times))
12 redis.log(redis.LOG_DEBUG,tostring(expire))
13  
14 redis.log(redis.LOG_NOTICE, "incr "..key1.." "..val);
15 if val == 1 then
16     redis.call('expire', key1, tonumber(expire))
17 else
18     if ttl == -1 then
19         redis.call('expire', key1, tonumber(expire))
20     end
21 end
22  
23 if val > tonumber(times) then
24     return 0
25 end
26 return 1

6、application.properties

 1 #redis
 2 spring.redis.hostName=
 3 spring.redis.host=
 4 spring.redis.port=6379
 5 spring.redis.jedis.pool.max-active=8
 6 spring.redis.jedis.pool.max-wait=
 7 spring.redis.jedis.pool.max-idle=8
 8 spring.redis.jedis.pool.min-idle=10
 9 spring.redis.timeout=100ms
10 spring.redis.password=
11  
12 logging.path= /Users/xub/log
13 logging.level.com.jincou.iplimiter=DEBUG
14 server.port=8888

7、SpringBoot启动类

1 @SpringBootApplication
2 public class Application {
3  
4     public static void main(String[] args) {
5         SpringApplication.run(Application.class, args);
6     }
7 }

8、测试

上面这个测试非常符合我们的预期,前五次访问接口是成功的,后面就失败了,直到10秒后才可以重新访问,这样反反复复。

其它的这边就不一一展示了,附上该项目源码。

Github地址:https://github.com/yudiandemingzi/spring-boot-redis-ip-limiter

 

转载于:https://blog.csdn.net/adparking/article/details/114637200

 

posted @ 2022-04-12 22:48  jarsing  阅读(545)  评论(0编辑  收藏  举报