使用过滤器配合redis,完成限流操作

264 阅读3分钟

限流

创建filter,使用redis实现,且执行顺序在跨域后,防止浪费资源

image-20230818095217778

image-20230818095324713

实现逻辑

  1. 在过滤器中,先获取请求的IP地址。
  2. 调用tryCount方法,检查该IP是否已经被限制访问。如果被限制访问,则返回false。
  3. 如果未被限制访问,调用limitPeriodCheck方法进行周期限制检查。
  4. 在limitPeriodCheck方法中,使用synchronized关键字对IP进行同步操作,保证多线程环境下的数据一致性。
  5. 判断该IP在周期内的访问次数是否超过阈值。如果超过阈值,则将该IP加入到限制访问列表中,并返回false。
  6. 如果未超过阈值,则将周期内的访问次数加1,并返回true。
  7. 如果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;
        }
    }
}