在你的项目中,有没有遇到用户重复提交的场景,即当用户因为网络延迟等情况把已经提交过一次的东西再次进行了提价,本篇文章将向各位介绍使用滑动窗口限流的方式来防止用户重复提交,并通过我们的自定义注解来进行封装功能。
首先,导入相关依赖:
<!-- 引入切面依赖-->
<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>
然后,我们先写一下滑动窗口限流的逻辑:
//滑动窗口限流逻辑
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切面,来实现我们的自定义注解
@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进行实现。
然后,自定义一个注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
int timeWindow() default 60; // 时间窗口大小,单位为秒
int maxRequests() default 10; //最大请求次数
}
以上代码写好之后,其实整个关键的代码就完成了,你可以随便在你的项目中找一个接口试一下,如下:
maxRequests表示在timeWindow时间内的最大请求数
结果如下,当然如果需要在前台显示,可以稍微改一下异常的处理方式,让提示信息能在前台显示: