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时间内的最大请求数

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

相关推荐
苏-言2 分钟前
Spring IOC实战指南:从零到一的构建过程
java·数据库·spring
界面开发小八哥9 分钟前
更高效的Java 23开发,IntelliJ IDEA助力全面升级
java·开发语言·ide·intellij-idea·开发工具
草莓base22 分钟前
【手写一个spring】spring源码的简单实现--容器启动
java·后端·spring
Allen Bright35 分钟前
maven概述
java·maven
Ljw...37 分钟前
表的增删改查(MySQL)
数据库·后端·mysql·表的增删查改
编程重生之路38 分钟前
Springboot启动异常 错误: 找不到或无法加载主类 xxx.Application异常
java·spring boot·后端
薯条不要番茄酱38 分钟前
数据结构-8.Java. 七大排序算法(中篇)
java·开发语言·数据结构·后端·算法·排序算法·intellij-idea
努力进修1 小时前
“探索Java List的无限可能:从基础到高级应用“
java·开发语言·list
politeboy1 小时前
k8s启动springboot容器的时候,显示找不到application.yml文件
java·spring boot·kubernetes
Daniel 大东2 小时前
BugJson因为json格式问题OOM怎么办
java·安全