场景 :为了防止接口被人恶意访问,比如有人通过JMeter等压测工具频繁访问我们的接口,导致接口响应变慢甚至崩溃,所以我们需要对一些特定的接口进行IP限流,即一定时间内同一IP访问的次数是有限的。
实现原理 :用Redis作为限流组件的核心的原理,将用户的IP地址当Key,一段时间内访问次数为value,同时设置该Key过期时间。
比如某接口设置相同IP10秒内请求5次,超过5次不让访问该接口。
1 2 3 4 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 秒,这样反反复复。
1.自定义注解(IpLimiter) 采用自定义注解的目的就是,在接口上使用自定义注解,让代码看去非常整洁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface IpLimiter { String ipAdress () ; long limit () default 10 ; long time () default 1 ; String message () ; }
2.在接口上使用自定义注解(@IpLimiter) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Controller public class IpController { private static final Logger LOGGER = LoggerFactory.getLogger(IpController.class); private static final String MESSAGE = "请求失败,你的IP访问太频繁" ; @ResponseBody @RequestMapping("iplimiter") @IpLimiter(ipAdress = "127.198.66.01", limit = 5, time = 10, message = MESSAGE) public String sendPayment (HttpServletRequest request) throws Exception { return "请求成功" ; } @ResponseBody @RequestMapping("iplimiter1") @IpLimiter(ipAdress = "127.188.145.54", limit = 4, time = 10, message = MESSAGE) public String sendPayment1 (HttpServletRequest request) throws Exception { return "请求成功" ; } }
3.处理IpLimiter注解的AOP 这边采用切面的方式处理自定义注解。同时为了保证原子性,这边写了redis脚本ipLimiter.lua来执行redis命令,来保证操作原子性。
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 @Aspect @Component public class IpLimterHandler { private static final Logger LOGGER = LoggerFactory.getLogger(IpLimterHandler.class); @Autowired RedisTemplate redisTemplate; private DefaultRedisScript<Long> getRedisScript; @PostConstruct public void init () { getRedisScript = new DefaultRedisScript <>(); getRedisScript.setResultType(Long.class); getRedisScript.setScriptSource(new ResourceScriptSource (new ClassPathResource ("ipLimiter.lua" ))); LOGGER.info("IpLimterHandler[分布式限流处理器]脚本加载完成" ); } @Around("@annotation(ipLimiter)") public Object around (ProceedingJoinPoint proceedingJoinPoint, IpLimiter ipLimiter) throws Throwable { if (LOGGER.isDebugEnabled()) { LOGGER.debug("IpLimterHandler[分布式限流处理器]开始执行限流操作" ); } Signature signature = proceedingJoinPoint.getSignature(); if (!(signature instanceof MethodSignature)) { throw new IllegalArgumentException ("the Annotation @IpLimter must used on method!" ); } String limitIp = ipLimiter.ipAdress(); Preconditions.checkNotNull(limitIp); long limitTimes = ipLimiter.limit(); long expireTime = ipLimiter.time(); if (LOGGER.isDebugEnabled()) { LOGGER.debug("IpLimterHandler[分布式限流处理器]参数值为-limitTimes={},limitTimeout={}" , limitTimes, expireTime); } String message = ipLimiter.message(); List<String> ipList = new ArrayList (); ipList.add(limitIp); Long result = (Long) redisTemplate.execute(getRedisScript, ipList, expireTime, limitTimes); if (result == 0 ) { String msg = "由于超过单位时间=" + expireTime + "-允许的请求次数=" + limitTimes + "[触发限流]" ; LOGGER.debug(msg); return message; } if (LOGGER.isDebugEnabled()) { LOGGER.debug("IpLimterHandler[分布式限流处理器]限流执行结果-result={},请求[正常]响应" , result); } return proceedingJoinPoint.proceed(); } }
4.RedisCacheConfig(配置类) 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 @Configuration public class RedisCacheConfig { private static final Logger LOGGER = LoggerFactory.getLogger(RedisCacheConfig.class); @Bean public RedisTemplate<String, Object> redisTemplate (RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate <>(); template.setConnectionFactory(factory); Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer (Object.class); ObjectMapper mapper = new ObjectMapper (); mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); serializer.setObjectMapper(mapper); template.setValueSerializer(serializer); template.setKeySerializer(new StringRedisSerializer ()); template.afterPropertiesSet(); LOGGER.info("Springboot RedisTemplate 加载完成" ); return template; } }
5.IpLimiter.lua脚本 优点 减少网络的开销
: 脚本只执行一次,不需要发送多次请求, 减少网络传输;保证原子操作
: 整个脚本作为一个原子执行, 就不用担心并发问题;
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 local key1 = KEYS[1 ]local val = redis.call('incr' , key1)local ttl = redis.call('ttl' , key1)local expire = ARGV[1 ]local times = ARGV[2 ] redis.log (redis.LOG_DEBUG,tostring (times)) redis.log (redis.LOG_DEBUG,tostring (expire)) redis.log (redis.LOG_NOTICE, "incr " ..key1.." " ..val);if val == 1 then redis.call('expire' , key1, tonumber (expire))else if ttl == -1 then redis.call('expire' , key1, tonumber (expire)) end end if val > tonumber (times) then return 0 end return 1