限流
创建filter,使用redis实现,且执行顺序在跨域后,防止浪费资源
实现逻辑
- 在过滤器中,先获取请求的IP地址。
- 调用tryCount方法,检查该IP是否已经被限制访问。如果被限制访问,则返回false。
- 如果未被限制访问,调用limitPeriodCheck方法进行周期限制检查。
- 在limitPeriodCheck方法中,使用synchronized关键字对IP进行同步操作,保证多线程环境下的数据一致性。
- 判断该IP在周期内的访问次数是否超过阈值。如果超过阈值,则将该IP加入到限制访问列表中,并返回false。
- 如果未超过阈值,则将周期内的访问次数加1,并返回true。
- 如果tryCount方法返回true,则继续执行过滤器链,否则返回一个拒绝访问的响应。
需要注意的是,在访问次数超过阈值后,将该IP加入到限制访问列表中,并设置了一个30秒的过期时间。在这个过期时间内,该IP将无法继续访问接口,这样就完成了限流操作。
实现
看代码吧比较简单
package com.jinze.filter;
import com.jinze.constant.Const;
import com.jinze.domain.RestBean;
import jakarta.annotation.Resource;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
/**
* @author jinze
* @version 1.0
* @description: 限制用户访问同一接口
* @date 2023/8/18 9:18
*/
@Component
@Order(Const.ORDER_Flow)
public class FlowLimitFilter extends HttpFilter {
@Resource
StringRedisTemplate stringRedisTemplate;
/**
* IP访问频率限制过滤器
*/
@Override
protected void doFilter(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
// 获取请求来源的IP地址
String ip = request.getRemoteAddr();
// 判断该IP的访问次数是否超过限制
if (tryCount(ip)){
// 没有超过限制,继续处理请求
chain.doFilter(request,response);
}else {
// 超过限制,返回提示信息
this.writeBlockMessage(response);
}
}
/**
* 为响应编写拦截内容,提示用户操作频繁
*
* @param response 响应
* @throws IOException 可能的异常
*/
private void writeBlockMessage(HttpServletResponse response) throws IOException {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(RestBean.forbidden("操作频繁,请稍后再试").toJsonString());
}
/**
* 检查同一IP是否达到访问上限
*
* @param ip 用户IP地址
* @return 是否达到上限
*/
private boolean tryCount(String ip) {
// 使用 synchronized 将代码块加锁,确保同一时间只有一个线程进入
synchronized (ip.intern()) {
// 如果 Redis 中已存在指定键(Const.FLOW_LIMIT_BLOCK + ip),表示该IP已被限制访问
if (Boolean.TRUE.equals(stringRedisTemplate.hasKey(Const.FLOW_LIMIT_BLOCK + ip))) {
return false;
}
// 进一步检查同一IP在限定时间内请求的次数是否超过上限
return this.limitPeriodCheck(ip);
}
}
/**
* 检查同一IP在限定时间内请求的次数是否超过上限
*
* @param ip 用户IP地址
* @return 是否达到上限
*/
private boolean limitPeriodCheck(String ip) {
// 使用 synchronized 将代码块加锁,确保同一时间只有一个线程进入
synchronized (ip.intern()) {
// 如果 Redis 中已存在指定键(Const.FLOW_LIMIT_COUNTER + ip),表示该IP已有请求记录
if (Boolean.TRUE.equals(stringRedisTemplate.hasKey(Const.FLOW_LIMIT_COUNTER + ip))) {
// 将计数器加1,如果不存在默认为0
Long increment = Optional.ofNullable(stringRedisTemplate.opsForValue()
.increment(Const.FLOW_LIMIT_COUNTER + ip))
.orElse(0L);
// 如果请求次数大于10,将该IP加入限制列表,并返回 false
if (increment > 10) {
// 设置限制键(Const.FLOW_LIMIT_BLOCK + ip)的值为空字符串,并设置过期时间为30秒
stringRedisTemplate.opsForValue()
.set(Const.FLOW_LIMIT_BLOCK + ip, "", 30, TimeUnit.SECONDS);
return false;
}
}
// 请求次数未达到上限,返回 true
return true;
}
}
}