分散式---基於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 @Documented4 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