限流
创建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; } } }