1. 程式人生 > 其它 >分散式---基於Redis進行介面IP限流

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