分布式---基于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