SpringBoot限流攔截器(結合業務)
背景
從網絡安全和系統穩定性來看,限流是非常有必要的。
一些網關,可以幫我們完成限流熔斷。但是,在某些場景,當與實際業務相結合時,網關的限流也就不那麼方便了。
1.目的
1.解決業務和限流合併的情況。
如,同一個接口,每個用戶,在一段時間(10秒)內只能請求幾次(4次)。
2.並且可以快速的調整這個限制的頻率(動態修改)
- SpringBoot
- Redis
2.配置關係
Redis > application.yml > 類中默認
PS:這裏是將Redis作爲一個配置中心,定時讀取。
也可以自行修改爲配置中心,如Nacos等,這裏不做展開。
注意:如果用Redis做配置中心,定時讀取的話,注意在啓動類添加@EnableScheduling
註解
3.代碼
配置類
@Data
@Configuration
@ConfigurationProperties("request.limit")
public class RequestLimitProperties {
// 配置文件,默認配置 10秒內4次
/**
* 判斷週期
*/
private Integer cycle = 10;
/**
* 每週期請求的次數
*/
private Integer times = 4;
}
攔截器
/**
* 每個請求次數限制攔截器
*
* @author litong
*/
@Order
public class RequestLimitInterceptor implements HandlerInterceptor {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 獲取次數
*/
private static final String CYCLE_KEY = getCycleKey();
/**
* 獲取單位時間
*/
private static final String TIMES_KEY = getTimesKey();
/**
* 默認判斷週期
*/
private Integer cycle = 10;
/**
* 默認每週期請求的次數
*/
private Integer times = 4;
@Autowired
private RequestLimitProperties requestLimitProperties;
@PostConstruct
public void pInit() {
cycle = requestLimitProperties.getCycle();
times = requestLimitProperties.getTimes();
}
@Scheduled(fixedDelay = 1000)
public void fetchRequestLimitProperties() {
//在有值,且是有效值時,認爲Redis優先級更高,更新 cycle 和 times
Integer redisCycle = (Integer) redisTemplate.opsForValue().get(CYCLE_KEY);
Integer redisTimes = (Integer) redisTemplate.opsForValue().get(TIMES_KEY);
if (redisCycle != null) {
cycle = redisCycle;
}
if (redisTimes != null) {
times = redisTimes;
}
}
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) {
// 獲取sid
String token = httpServletRequest.getHeader("token");
// 獲取請求接口路徑
String requestURI = httpServletRequest.getRequestURI();
if (requestURI != null) {
String[] split = requestURI.split("/");
if (split.length != 0) {
requestURI = split[split.length - 1];
}
}
// 獲取Redis中的緩存
Long expire = redisTemplate.getExpire(getKey(token, requestURI), TimeUnit.SECONDS);
if (expire == null || expire == -2) {
// 第一次請求,記錄請求
redisTemplate.opsForValue().set(getKey(token, requestURI), 1, cycle, TimeUnit.SECONDS);
} else {
// 後續請求,請求次數加1
Integer s = (Integer) redisTemplate.opsForValue().get(getKey(token, requestURI));
int time = 0;
if (s != null && s != -2) {
time = s;
}
int count = time + 1;
if (count >= times + 1) {
throw new AuthenticationException("接口請求頻率過高,sid:" + token + ",url:" + requestURI + ",當前閾值:" + times + "/" + cycle);
}
redisTemplate.opsForValue().set(getKey(token, requestURI), count, expire == 0 ? 1 : expire, TimeUnit.SECONDS);
}
return true;
}
private String getKey(String token, String requestURI) {
return "REQUEST_LIMIT_REDIS:" + requestURI + ":" + token;
}
private static String getCycleKey() {
return "REQUEST_LIMIT_CYCLE:";
}
private static String getTimesKey() {
return "REQUEST_LIMIT_TIMES:";
}
}