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:";
}
}