1 滑动窗口算法
滑动窗口算法是一种广泛应用于计算机科学和数据分析中的数据流算法,特别适用于处理具有时间序列特性的数据,如网络流量监控、速率限制、数据分析等领域。其核心思想是在一个固定大小的“窗口”内对数据进行统计分析,这个窗口会随着数据的流入而向前滑动,始终保持最新一段时间内的数据统计。
基本概念
-
窗口大小:滑动窗口有一个固定的尺寸,表示你关心的数据的时间范围或数据数量。例如,如果你关注的是过去5分钟内的数据,那么窗口大小就是5分钟。
-
滑动/移动:随着时间的推移或新数据的到来,窗口会不断向前移动,丢弃最旧的数据点,同时纳入最新的数据点,始终保持窗口内数据的新鲜度。
-
数据处理:在窗口内的数据会被用来进行各种计算,比如求平均值、最大值、最小值、计数等,具体取决于应用场景。
应用实例
-
网络流量控制:在网络传输中,滑动窗口常用来控制发送速率,避免拥塞。TCP协议中的拥塞控制就采用了类似滑动窗口的机制来调整数据包的发送速率。
-
速率限制(Rate Limiting):在Web服务中,滑动窗口算法可以用来实现对API调用或其他请求的速率限制,确保服务不会因为过多的请求而过载。通过控制窗口期内的请求总数或特定时间段内的请求频率,可以平滑系统负载。
-
交易监控:在金融系统中,滑动窗口可用于监控交易活动,比如检测是否存在异常交易模式,通过分析一段时间内的交易频次和金额分布。
实现要点
-
数据结构选择:为了高效实现滑动窗口,通常使用队列或哈希表等数据结构来存储窗口内的数据,便于快速插入和删除元素。
-
窗口边界处理:需要准确地管理窗口的边界,确保当新数据到来时,能及时移除窗口最左边(或最旧)的数据点,同时加入新数据点。
-
时间复杂度:理想情况下,滑动窗口算法的操作(如添加元素、移除元素、计算窗口内统计量)应该能在常数时间内完成,以保证算法的高效性。
滑动窗口算法因其灵活性和高效性,在众多领域中都有重要应用,是理解和处理时间序列数据的一个非常实用的工具。
要实现AOP结合滑动窗口算法来实现自定义规则的限流,我们可以在原有的基础上进一步扩展,以支持更灵活的配置和更复杂的规则。以下是一个基于Spring AOP和滑动窗口算法的简单示例,包括自定义注解来设置限流规则,以及如何在切面中应用这些规则。
2 定义缓存注解
首先,定义一个自定义注解来标记需要限流的方法,并允许传入限流的具体规则
package com.example.demo.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* SlidingWindowRateLimiter : 滑动窗口限流注解
*
* @author zyw
* @create 2024-06-06 17:20
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface WindowRateLimit {
// 允许的最大请求数
int limit();
// 窗口时间长度,单位毫秒
long timeWindowMilliseconds();
}
3 滑动窗口限流器
接下来,实现滑动窗口限流器,这里简化处理,直接使用内存实现,实际应用中可能需要基于Redis等持久化存储以适应分布式场景:
核心思想:每次请求进来时,获取当前时间的时间戳,将每次请求的时间戳存储到LinkedList集合中,同时以当前时间为窗口期的结束点,删除往前一个窗口期内所有的请求时间戳,将LinkedList集合剩余数据的个数与自定义设置的窗口期请求峰值进行对比,若等于则直接限流。
package com.example.demo.uitls;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.LinkedList;
/**
* SlidingWindowRateLimiter : 滑动窗口限流算法
*
* @author zyw
* @create 2024-06-07 15:16
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SlidingWindowRateLimiter implements Serializable {
/**
* 请求队列
*/
private LinkedList<Long> requests = new LinkedList<>();
/**
* 最大请求数
*/
private int maxRequests;
/**
* 窗口大小
*/
private long windowSizeInMilliseconds;
public SlidingWindowRateLimiter(int maxRequests, long windowSizeInMilliseconds) {
this.maxRequests = maxRequests;
this.windowSizeInMilliseconds = windowSizeInMilliseconds;
}
/**
* 判断是否允许请求
* @return
*/
public synchronized boolean allowRequest() {
// 获取当前时间
long currentTime = System.currentTimeMillis();
// 清除窗口之外的旧请求
while (!requests.isEmpty() && currentTime - requests.peekFirst() > windowSizeInMilliseconds) {
requests.removeFirst();
}
// 如果当前窗口请求未达到上限,则允许请求并记录
if (requests.size() < maxRequests) {
requests.addLast(currentTime);
return true;
} else {
// 达到限流阈值,拒绝请求
return false;
}
}
}
4 AOP切面实现
最后,创建AOP切面来应用限流逻辑:
将需要限流的方法所初始化的滑动窗口限流器缓存到Redis中,过期时间设置为对应的窗口时间。
一个窗口时间内,若没有新的请求进来,即存储的请求时间戳都为窗口期外的,因此可以直接清除掉已减少缓存占用空间。
package com.example.demo.aspect;
import com.example.demo.annotation.WindowRateLimit;
import com.example.demo.config.redis.RedisKeyEnum;
import com.example.demo.uitls.RedisUtil;
import com.example.demo.uitls.SlidingWindowRateLimiter;
import jakarta.annotation.Resource;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
/**
* RateLimiterAspect :
*
* @author zyw
* @create 2024-06-06 17:21
*/
@Aspect
@Component
public class SlidingWindowRateLimiterAspect {
@Resource
private RedisUtil redisUtil;
@Around("@annotation(rateLimit)")
public Object applyRateLimit(ProceedingJoinPoint joinPoint, WindowRateLimit rateLimit) throws Throwable {
// 获取调用的方法名
String methodName = joinPoint.getSignature().getName();
// 获取方法对应的缓存滑动窗口限流器KEY
String key = RedisKeyEnum.WINDOW_CURRENT_LIMITING.getKey() + methodName;
// 从缓存中获取滑动窗口限流器
SlidingWindowRateLimiter rateLimiter = redisUtil.getCacheObject(key);
// 如果滑动窗口限流器不存在,则创建一个新限流器
if (rateLimiter == null) {
rateLimiter = new SlidingWindowRateLimiter(rateLimit.limit(), rateLimit.timeWindowMilliseconds());
}
// 如果滑动窗口限流器存在,则判断是否允许请求
if (!rateLimiter.allowRequest()) {
throw new RuntimeException("Too many requests, please try again later.");
}
// 如果允许请求,则更新滑动窗口限流器,缓存过期时间设置为滑动窗口限流器时间窗口
redisUtil.setCacheObject(key, rateLimiter, rateLimit.timeWindowMilliseconds(), TimeUnit.MILLISECONDS);
// 允许执行方法
return joinPoint.proceed();
}
}
5 应用限流注解
在需要做限流的方法上加上注解,在注解参数中设定 允许的最大请求数 和 窗口时间长度(单位毫秒)
package com.example.demo.service.impl;
import com.example.demo.annotation.WindowRateLimit;
import com.example.demo.service.TestService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
/**
* TestServiceImpl :
*
* @author zyw
* @create 2023-12-18 15:15
*/
@Service
public class TestServiceImpl implements TestService {
@Override
@WindowRateLimit(limit = 5, timeWindowMilliseconds = 60L*1000) // 每最多允许5次请求
public String getContent() {
return "Hello Word";
}
}
首次请求时,初始化滑动窗口限流器,记录第一次请求的时间戳
窗口期内,记录了五次请求的时间戳后,已达到我们在注解中设置的窗口期最大请求量
此时接口限流