SpringBoot使用滑动窗口限流防止用户重复提交(自定义注解实现)

在你的项目中,有没有遇到用户重复提交的场景,即当用户因为网络延迟等情况把已经提交过一次的东西再次进行了提价,本篇文章将向各位介绍使用滑动窗口限流的方式来防止用户重复提交,并通过我们的自定义注解来进行封装功能。

首先,导入相关依赖:

XML 复制代码
<!--        引入切面依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
        </dependency>

然后,我们先写一下滑动窗口限流的逻辑:

java 复制代码
//滑动窗口限流逻辑
public class RateLimiter {
    private static ConcurrentHashMap<String, Deque<Long>> requestTimestamps=new ConcurrentHashMap<>();
    public static boolean isAllowed(String userId,int timeWindow,int maxRequests){
        long now =System.currentTimeMillis();
        long windowStart=now -(timeWindow*1000);

        requestTimestamps.putIfAbsent(userId,new LinkedList<>());
        Deque<Long> timestamps=requestTimestamps.get(userId);

        synchronized (timestamps){
            // 移除窗口外的时间戳
            while(!timestamps.isEmpty()&& timestamps.peekFirst()<windowStart){
                timestamps.pollFirst();
            }
            // 如果时间戳数量小于最大请求数,允许访问并添加时间戳
            if(timestamps.size()<maxRequests){
                timestamps.addLast(now);
                return true;
            }else{
                return false;
            }
        }
    }
}
主要部分解释
1. 定义 requestTimestamps 变量

private static ConcurrentHashMap<String, Deque<Long>> requestTimestamps = new ConcurrentHashMap<>();

  • requestTimestamps 是一个并发的哈希映射,用于存储每个用户的请求时间戳。
  • 键(String)是用户ID。
  • 值(Deque<Long>)是一个双端队列,用于存储用户请求的时间戳(以毫秒为单位)。
2. isAllowed 方法

public static boolean isAllowed(String userId, int timeWindow, int maxRequests) {

  • 该方法接受三个参数:
    • userId:用户ID。
    • timeWindow:时间窗口,单位为秒。
    • maxRequests:时间窗口内允许的最大请求数。
  • 方法返回一个布尔值,表示用户是否被允许发出请求。
3. 获取当前时间和时间窗口开始时间

long now = System.currentTimeMillis(); long windowStart = now - (timeWindow * 1000);

  • now:当前时间,以毫秒为单位。
  • windowStart:时间窗口的开始时间,即当前时间减去时间窗口长度,以毫秒为单位。
4. 初始化用户的请求时间戳队列

requestTimestamps.putIfAbsent(userId, new LinkedList<>()); Deque<Long> timestamps = requestTimestamps.get(userId);

  • requestTimestamps.putIfAbsent(userId, new LinkedList<>()):如果 requestTimestamps 中没有该用户的记录,则为其初始化一个空的 LinkedList
  • timestamps:获取该用户对应的时间戳队列。
5. 同步时间戳队列

synchronized (timestamps) {

  • 同步块:对用户的时间戳队列进行同步,以确保线程安全。
6. 移除窗口外的时间戳

while (!timestamps.isEmpty() && timestamps.peekFirst() < windowStart) { timestamps.pollFirst(); }

  • 循环检查并移除队列中位于时间窗口之外的时间戳(即小于 windowStart 的时间戳)。
7. 检查请求数并更新时间戳队列

if (timestamps.size() < maxRequests) { timestamps.addLast(now); return true; } else { return false; }

  • 如果时间戳队列的大小小于 maxRequests,说明在时间窗口内的请求次数未超过限制:
    • 将当前时间戳添加到队列的末尾。
    • 返回 true,表示允许请求。
  • 否则,返回 false,表示拒绝请求。

接下来我们需要实现一个AOP切面,来实现我们的自定义注解

java 复制代码
@Component
@Aspect
public class RateLimitInterceptor {
//    private HashMap<String,String> info;

    @Autowired
    private RedisTemplate<String,String> redisTemplate;
    @Around("@annotation(rateLimit)")
    public Object interceptor(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
        String userid= redisTemplate.opsForValue().get("loginId");   //获取用户ID
        System.out.println("userid:"+userid);
        int timeWindow=rateLimit.timeWindow();
        int maxRequests=rateLimit.maxRequests();

        if(RateLimiter.isAllowed(userid,timeWindow,maxRequests)){
            return joinPoint.proceed();
        }
        else{
            throw new RepeatException("访问过于频繁,请稍后再试");
        }
    }
}

获取用户ID的逻辑需要根据你的项目实际情况进行编写,我这里是把id存在redis里面的,但是也是存在问题的,读者可以尝试使用RabbitMQ进行实现。

然后,自定义一个注解

java 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
    int timeWindow() default 60; // 时间窗口大小,单位为秒
    int maxRequests() default 10;  //最大请求次数
}

以上代码写好之后,其实整个关键的代码就完成了,你可以随便在你的项目中找一个接口试一下,如下:

maxRequests表示在timeWindow时间内的最大请求数

结果如下,当然如果需要在前台显示,可以稍微改一下异常的处理方式,让提示信息能在前台显示:

相关推荐
逊嘘13 分钟前
【Java语言】抽象类与接口
java·开发语言·jvm
morris13120 分钟前
【SpringBoot】Xss的常见攻击方式与防御手段
java·spring boot·xss·csp
monkey_meng37 分钟前
【Rust中的迭代器】
开发语言·后端·rust
余衫马40 分钟前
Rust-Trait 特征编程
开发语言·后端·rust
monkey_meng44 分钟前
【Rust中多线程同步机制】
开发语言·redis·后端·rust
七星静香1 小时前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
Jacob程序员1 小时前
java导出word文件(手绘)
java·开发语言·word
ZHOUPUYU1 小时前
IntelliJ IDEA超详细下载安装教程(附安装包)
java·ide·intellij-idea
stewie61 小时前
在IDEA中使用Git
java·git
Elaine2023911 小时前
06 网络编程基础
java·网络