java_使用redis 進行請求限流
應用場景:在后端處理流程復雜,前端可能會高頻點擊的情況下,做請求限流來進行系統保護;
本文的應用場景為:前端請求導出excel,出現大數據量的情況下,限制每個ip 用戶對于每個地區條件,一分鐘內只能請求兩次
思路:使用注解在對要限流的方法進行標識,自定義攔截器,在進入這個方法體之前,通過redis 獲取緩存的key,該key 設置一個超時時間,該key 的value 為訪問次數,每訪問一次便+1,達到次數后則不進入方法體,直接返回請求已滿,直到key 過期;
代碼實現:
0.配置信息:
pom.xml :
<dependency>? <groupId>org.springframework.boot</groupId>? <artifactId>spring-boot-starter-data-redis</artifactId>? <version>2.5.7</version></dependency>
1, 自定義注解 ======================================================
@Retention(RUNTIME)//運行時有效
@Target(ElementType.METHOD)//用在方法上
public @interface AccessLimit {
?? //時間范圍(單位:秒)
?? int seconds();
?? //在這個時間范圍內最大訪問次數
?? int maxCount();
}
2.自定義攔截器============================================================
@Slf4j
@Component
public class ExportExcelLimitInterceptor implements HandlerInterceptor {
?? @Autowired
?? private RedisTemplate redisTemplate;
?? @Override
?? public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
? ? ?? if (handler instanceof HandlerMethod) {
? ? ? ? ?? HandlerMethod hm = (HandlerMethod) handler;
? ? ? ? ?? //設置redisTemplate的序列化方式(必須設置為這種方式,因為要用到incr)
? ? ? ? ?? redisTemplate.setKeySerializer(new StringRedisSerializer());
? ? ? ? ?? redisTemplate.setValueSerializer(new StringRedisSerializer());
? ? ? ? ?? AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);
? ? ? ? ?? if (null == accessLimit) {
? ? ? ? ? ? ?? return true;
? ? ? ? ?? }
? ? ? ? ?? int seconds = accessLimit.seconds();
? ? ? ? ?? int maxCount = accessLimit.maxCount();
? ? ? ? ?? String ipAddr = IpUtil.getIpAddr(request);
? ? ? ? ?? String key = request.getContextPath() + ":" + request.getServletPath() + "_exportExcel"+":" + ipAddr ;
? ? ? ? ?? String countStr = (String) redisTemplate.opsForValue().get(key);
? ? ? ? ?? Integer count = null;
? ? ? ? ?? //如果不是第一次訪問,則把訪問次數轉換為integer類型
? ? ? ? ?? if(countStr != null){
? ? ? ? ? ? ?? count = Integer.valueOf(redisTemplate.opsForValue().get(key).toString());
? ? ? ? ?? }
? ? ? ? ?? log.info("count:{}",count);
? ? ? ? ?? //拿到訪問次數的過期時間
? ? ? ? ?? Long keySeconds = redisTemplate.getExpire(key);
? ? ? ? ?? if (null == count || -1 == count) {
? ? ? ? ? ? ?? redisTemplate.opsForValue().set(key,String.valueOf(1));
? ? ? ? ? ? ?? //設置過期時間
? ? ? ? ? ? ?? redisTemplate.expire(key,seconds, TimeUnit.SECONDS);
? ? ? ? ? ? ?? return true;
? ? ? ? ?? }
? ? ? ? ?? if (count < maxCount) {
? ? ? ? ? ? ?? log.info("count:{}",count);
? ? ? ? ? ? ?? //訪問次數+1
? ? ? ? ? ? ?? redisTemplate.opsForValue().increment(key,1);
? ? ? ? ? ? ?? //設置剩余過期時間(修改完該key的value值后,對應的過期時間會失效,需重新設置)
? ? ? ? ? ? ?? redisTemplate.expire(key,keySeconds, TimeUnit.SECONDS);
? ? ? ? ? ? ?? return true;
? ? ? ? ?? }
? ? ? ? ?? if (count >= maxCount) {
// ? ? ? ? ? ? ?? response 返回 json 請求過于頻繁請稍后再試
? ? ? ? ? ? ?? this.responseResult(response, new Result(500, "訪問次數已超過限制,請稍后重試"));
? ? ? ? ? ? ?? return false;
? ? ? ? ?? }
? ? ?? }
? ? ?? return true;
?? }
?? void responseResult(HttpServletResponse response, Result result) throws IOException {
? ? ?? response.setHeader("Content-Type", "application/json;charset=utf8");
? ? ?? Writer writer = response.getWriter();
? ? ?? writer.write(JSONObject.toJSONString(result));
? ? ?? writer.close();
?? }
}
3.添加攔截器到webconfig ===============================================================================
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
?? @Autowired
?? private ExportExcelLimitInterceptor excelLimitInterceptor; // 訪問限制攔截器
?? @Override
?? public void addInterceptors(InterceptorRegistry registry) {
? ? ?? registry.addInterceptor(excelLimitInterceptor)
? ? ? ? ? ? ?? .addPathPatterns("/qiannan/test");
?? }
}
4.添加注解到方法體===================================================================================
@AccessLimit(seconds=30,maxCount=2)
@GetMapping("/qiannan/test")
public Object test(String id) throws IOException {
?? return linkFeignClient.detail(id);
}
5. 用到的工具類:
public class IpUtil {
?? private IpUtil() { }
?? /**
? ? * 獲取登錄用戶的IP地址
? ? * @param request
? ? * @return
? ? */
?? public static String getIpAddr(HttpServletRequest request) {
? ? ?? String ip = request.getHeader("x-forwarded-for");
? ? ?? if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
? ? ? ? ?? ip = request.getHeader("Proxy-Client-IP");
? ? ?? }
? ? ?? if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
? ? ? ? ?? ip = request.getHeader("WL-Proxy-Client-IP");
? ? ?? }
? ? ?? if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
? ? ? ? ?? ip = request.getRemoteAddr();
? ? ?? }
? ? ?? if (ip.equals("0:0:0:0:0:0:0:1")) {
? ? ? ? ?? ip = "127.0.0.1";
? ? ?? }
? ? ?? if (ip.split(",").length > 1) {
? ? ? ? ?? ip = ip.split(",")[0];
? ? ?? }
? ? ?? return ip;
?? }
?? //ip轉化為有序的長整形
?? public static long getIp2long(String ip) {
? ? ?? ip = ip.trim();
? ? ?? String[] ips = ip.split("\\.");
? ? ?? long ip2long = 0L;
? ? ?? for (int i = 0; i < 4; ++i) {
? ? ? ? ?? ip2long = ip2long << 8 | Integer.parseInt(ips[i]);
? ? ?? }
? ? ?? return ip2long;
?? }
}