2025-05-08🌱上海: ☀️ 🌡️+19°C 🌬️↖19km/h
# Part005 多种限流算法实现
- 为什么(Why)
# 1.1 项目背景
part005
模块实现了多种限流算法,用于解决高并发场景下服务过载问题。在微服务架构和分布式系统中,限流是保护系统稳定性的关键措施,通过控制请求速率,防止突发流量对系统造成冲击,确保核心业务的正常运行。本模块提供了多种限流算法的实现,展示了不同限流策略的特点和适用场景。
# 1.2 解决的问题
-
系统过载保护:通过限制请求速率,防止系统资源耗尽,保障系统稳定性。
-
突发流量应对:平滑处理流量峰值,避免瞬时高并发导致系统崩溃。
-
资源合理分配:根据业务优先级分配系统资源,确保核心业务正常运行。
-
服务质量保障:维持系统可用性和响应时间,提供一致的服务质量。
- 如何实现(How)
# 2.1 项目结构
part005
模块的项目结构如下:
part005/ | |
├── src/ | |
│ ├── main/ | |
│ │ ├── java/ | |
│ │ │ └── com/ | |
│ │ │ └── muzi/ | |
│ │ │ └── part5/ | |
│ │ │ ├── Counter/ # 固定窗口计数限流 | |
│ │ │ │ ├── CounterRateLimit.java | |
│ │ │ │ └── CounterRateLimitAspect.java | |
│ │ │ ├── SlidingWindow/ # 滑动窗口限流 | |
│ │ │ │ ├── SlidingWindowRateLimit.java | |
│ │ │ │ └── SlidingWindowRateLimitAspect.java | |
│ │ │ ├── TokenBucket/ # 令牌桶限流 | |
│ │ │ │ ├── TokenBucketRateLimit.java | |
│ │ │ │ └── TokenBucketRateLimitAspect.java | |
│ │ │ ├── LeakyBucket/ # 漏桶限流 | |
│ │ │ │ ├── LeakyBucketRateLimit.java | |
│ │ │ │ ├── LeakyBucketLimiter.java | |
│ │ │ │ └── LeakyBucketRateLimitAspect.java | |
│ │ │ ├── SemphoreTokenBucket/ # 信号量限流 | |
│ │ │ │ ├── FrequencyControl.java | |
│ │ │ │ ├── FrequncyControContainer.java | |
│ │ │ │ └── FrequrenControlAspect.java | |
│ │ │ ├── LoadRunnerUtils.java # 压测工具类 | |
│ │ │ ├── TestController.java # 测试接口 | |
│ │ │ └── part5Application.java # 应用启动类 | |
│ │ └── resources/ # 配置文件 | |
│ └── test/ # 测试类 | |
└── pom.xml # Maven配置文件 |
# 2.2 关键技术点
# 2.2.1 案例分析:固定窗口计数限流
技术实现: 固定窗口计数器是最简单的限流算法,它将时间划分为固定大小的窗口,并在每个窗口内进行计数:
@Around("@annotation(com.muzi.part5.Counter.CounterRateLimit)") | |
public Object rateLimit(ProceedingJoinPoint joinPoint) throws Throwable { | |
// 获取注解信息 | |
MethodSignature signature = (MethodSignature) joinPoint.getSignature(); | |
Method method = signature.getMethod(); | |
CounterRateLimit annotation = method.getAnnotation(CounterRateLimit.class); | |
// 获取注解的参数 | |
int maxRequest = annotation.maxRequest(); | |
long timeWindowInMillis = TimeUnit.SECONDS.toMillis(annotation.timeWindow()); | |
// 获取方法名 | |
String methodName = method.toString(); | |
// 初始化计数器和时间戳 | |
AtomicInteger count = REQUEST_COUNT.computeIfAbsent(methodName, x -> new AtomicInteger(0)); | |
long startTime = REQUEST_TIMESTAMP.computeIfAbsent(methodName, x -> System.currentTimeMillis()); | |
// 获取当前时间 | |
long currentTimeMillis = System.currentTimeMillis(); | |
// 判断:如果当前时间超出时间窗口,则重置 | |
if (currentTimeMillis - startTime > timeWindowInMillis) { | |
count.set(0); | |
REQUEST_TIMESTAMP.put(methodName, currentTimeMillis); | |
} | |
// 原子的增加计数器并检查其值 | |
if (count.incrementAndGet() > maxRequest) { | |
// 如果超出最大请求次数,递减计数器,并报错 | |
count.decrementAndGet(); | |
return "服务繁忙,请稍后重试"; | |
} | |
// 方法原执行 | |
return joinPoint.proceed(); | |
} |
原理分析:
-
固定窗口机制
-
将时间划分为固定长度的窗口(如 1 秒、1 分钟)
-
每个窗口内独立计数,窗口结束时重置计数器
-
通过注解参数控制窗口大小和最大请求数
-
-
窗口边界问题
-
存在临界问题:窗口切换时可能出现短时间内请求量超过限制
-
例如,两个窗口交界处(前一个窗口末尾和后一个窗口开始)可能集中大量请求
-
在高并发场景下可能导致系统瞬间过载
-
-
实现优势
-
算法简单,易于实现和理解
-
内存占用少,不需要记录每个请求的时间戳
-
计算复杂度低,性能高
-
# 2.2.2 案例分析:滑动窗口限流
技术实现: 滑动窗口算法通过记录请求的时间戳,动态计算时间窗口内的请求数量:
@Around("@annotation(com.muzi.part5.SlidingWindow.SlidingWindowRateLimit)") | |
public Object rateLimit(ProceedingJoinPoint joinPoint) throws Throwable { | |
// 获取注解信息和参数 | |
MethodSignature signature = (MethodSignature) joinPoint.getSignature(); | |
Method method = signature.getMethod(); | |
SlidingWindowRateLimit rateLimit = method.getAnnotation(SlidingWindowRateLimit.class); | |
// 允许的最大请求数和滑动窗口大小 (秒) | |
int requests = rateLimit.maxRequest(); | |
int timeWindow = rateLimit.timeWindow(); | |
// 获取方法名和请求时间戳队列 | |
String methodName = method.toString(); | |
ConcurrentLinkedQueue<Long> requestTimes = REQUEST_TIMES_MAP.computeIfAbsent(methodName, | |
k -> new ConcurrentLinkedQueue<>()); | |
// 当前时间和窗口开始时间 | |
long currentTime = System.currentTimeMillis(); | |
long thresholdTime = currentTime - TimeUnit.SECONDS.toMillis(timeWindow); | |
// 移除窗口之前的请求记录 | |
while (!requestTimes.isEmpty() && requestTimes.peek() < thresholdTime) { | |
requestTimes.poll(); | |
} | |
// 检查当前窗口内的请求数是否超过限制 | |
if (requestTimes.size() < requests) { | |
// 未超过限制,记录请求并继续执行 | |
requestTimes.add(currentTime); | |
return joinPoint.proceed(); | |
} else { | |
// 超过限制,返回错误信息 | |
return "服务繁忙,请稍后重试"; | |
} | |
} |
原理分析:
-
动态窗口机制
-
不以固定时间点划分窗口,而是以当前时间为基准,向前推移固定时间长度
-
持续跟踪时间窗口内的请求数量,动态调整窗口范围
-
通过队列记录每个请求的时间戳,移除窗口外的过期请求
-
-
平滑限流效果
-
解决了固定窗口在窗口边界的突发流量问题
-
提供更平滑的限流效果,请求计数随时间连续变化
-
能够更精准地控制系统的负载
-
-
实现特点
-
需要额外的内存空间存储请求时间戳
-
队列操作的时间复杂度为 O (1),整体性能良好
-
适用于对限流精度要求较高的场景
-
# 2.2.3 案例分析:令牌桶限流
技术实现: 令牌桶算法基于 Google Guava 的 RateLimiter 实现,以固定速率生成令牌:
@Around("@annotation(com.muzi.part5.TokenBucket.TokenBucketRateLimit)") | |
public Object rateLimit(ProceedingJoinPoint joinPoint) throws Throwable { | |
// 获取方法和注解信息 | |
MethodSignature signature = (MethodSignature) joinPoint.getSignature(); | |
Method method = signature.getMethod(); | |
TokenBucketRateLimit rateLimit = method.getAnnotation(TokenBucketRateLimit.class); | |
// 获取注解中定义的每秒令牌数 | |
double permitsPerSecond = rateLimit.permitsPerSecond(); | |
// 获取方法名,并创建或获取对应的限流器 | |
String methodName = method.toString(); | |
RateLimiter rateLimiter = limiters.computeIfAbsent(methodName, | |
k -> RateLimiter.create(permitsPerSecond)); | |
// 尝试获取令牌 | |
if (rateLimiter.tryAcquire()) { | |
return joinPoint.proceed(); | |
} else { | |
return "服务繁忙,请稍后重试"; | |
} | |
} |
原理分析:
-
令牌生成机制
-
以固定速率向令牌桶中放入令牌
-
请求到达时必须先获取令牌才能继续执行
-
当令牌不足时,请求被拒绝或等待
-
-
平滑处理突发流量
-
允许一定程度的突发流量(取决于桶的容量)
-
长期来看,请求速率不会超过令牌生成速率
-
提供流量整形能力,使系统负载更加均衡
-
-
主要优势
-
对突发流量有良好的适应性
-
通过调整令牌生成速率,精确控制请求处理速度
-
支持请求预热和平滑突发流量
-
# 2.2.4 案例分析:漏桶限流
技术实现: 漏桶算法将请求比作水滴,以固定速率流出:
public boolean tryAcquire() { | |
long currentTime = System.currentTimeMillis(); | |
synchronized (this) { | |
// 计算上次漏水到当前时间的时间间隔 | |
long leakDuration = currentTime - lastLeakTime; | |
// 如果时间间隔大于等于 1 秒,表示漏桶已经漏出一定数量的水 | |
if (leakDuration >= TimeUnit.SECONDS.toMillis(1)) { | |
// 计算漏出的水量 | |
long leakQuantity = leakDuration / TimeUnit.SECONDS.toMillis(1) * leakRate; | |
// 更新桶中的水量,不能低于 0 | |
water = (int) Math.max(0, water - leakQuantity); | |
lastLeakTime = currentTime; | |
} | |
// 判断桶中的水量是否小于容量 | |
if (water < capacity) { | |
water++; | |
return true; | |
} | |
} | |
// 桶满,获取令牌失败 | |
return false; | |
} |
原理分析:
-
固定出水速率
-
无论请求速率如何,都以固定速率处理请求
-
桶满时拒绝新的请求,确保系统不会过载
-
通过漏出速率控制系统最大处理能力
-
-
平滑流量特性
-
即使面对突发流量,也能保持稳定的处理速率
-
完全消除了突发流量的影响,保护系统稳定
-
但缺乏对突发流量的弹性处理能力
-
-
实现特点
-
需要记录上次漏水时间和当前水量
-
每次请求时计算已漏出的水量
-
适合对处理速率要求严格、需要严格控制资源使用的场景
-
# 2.2.5 案例分析:信号量限流
技术实现: 基于 Java 并发包中的 Semaphore 实现并发控制:
@Around("@annotation(com.muzi.part5.SemphoreTokenBucket.FrequencyControl)") | |
public Object around(ProceedingJoinPoint joinPoint) throws Throwable { | |
Method method = ((MethodSignature)joinPoint.getSignature()).getMethod(); | |
FrequencyControl[] annotationsByType = method.getAnnotationsByType(FrequencyControl.class); | |
List<Semaphore> semaphores = new ArrayList<>(); | |
boolean flag = true; | |
for (FrequencyControl frequencyControl : annotationsByType) { | |
Semaphore semaphore = new Semaphore(frequencyControl.permits()); | |
semaphores.add(semaphore); | |
boolean acquired = semaphore.tryAcquire(1, frequencyControl.timeout(), frequencyControl.unit()); | |
if (!acquired) { | |
flag = false; | |
} | |
} | |
if (flag) { | |
try { | |
return joinPoint.proceed(); | |
} finally { | |
// 释放所有信号量 | |
for (Semaphore semaphore : semaphores) { | |
semaphore.release(); | |
} | |
} | |
} else { | |
return "系统繁忙,请稍后重试"; | |
} | |
} |
原理分析:
-
并发控制机制
-
通过 Semaphore 控制同时执行的请求数量
-
每个请求需要获取许可才能执行,执行完毕后释放许可
-
可以设置获取许可的超时时间,避免长时间等待
-
-
线程级别控制
-
直接控制线程级别的执行权限,而非请求速率
-
可以精确控制系统的并发度,防止资源耗尽
-
适合控制数据库连接数、线程池等有限资源的使用
-
-
实现优势
-
实现简单,基于 Java 标准库
-
可以与其他限流算法结合使用
-
支持公平模式和非公平模式
-
# 3. 技术点详解(Detail)
# 3.1 AOP 实现限流
所有限流算法都通过 Spring AOP 实现,主要基于以下几点:
-
注解定义
-
每种限流算法定义专用注解,包含必要的配置参数
-
通过注解可以灵活配置限流策略,无需修改业务代码
-
-
环绕通知
-
使用 @Around 注解实现环绕通知,在方法执行前后添加限流逻辑
-
获取注解信息,根据配置执行相应的限流算法
-
-
非侵入式集成
-
业务代码只需添加注解,无需关心限流实现细节
-
限流逻辑与业务逻辑解耦,便于维护和更新
-
# 3.2 限流算法对比
各种限流算法的特点对比:
暂时无法在飞书文档外展示此内容
# 3.3 限流算法的实现要点
-
线程安全
-
限流器在高并发环境下使用,必须确保线程安全
-
使用 AtomicInteger、ConcurrentHashMap 等并发容器
-
关键操作需使用 synchronized 或锁保护
-
-
性能考量
-
限流逻辑执行频繁,必须保证高性能
-
避免使用重量级锁,减少同步范围
-
缓存限流器实例,避免重复创建
-
-
状态管理
-
管理限流器状态,包括计数器、时间戳等
-
针对不同方法使用不同的限流器实例
-
有效处理限流器的生命周期
-
# 3.4 分布式限流考量
当前实现主要针对单机场景,在分布式环境中需要考虑以下因素:
-
全局视图
-
需要全局视角的限流计数,通常基于 Redis 等分布式存储
-
服务实例间共享限流状态,确保整体限流效果
-
-
一致性问题
-
分布式环境下的计数一致性保障
-
CAP 理论下的取舍,通常选择可用性和分区容错性
-
-
性能开销
-
分布式限流引入网络通信开销
-
需要平衡限流精度和性能成本
-
# 4. 使用示例(Usage)
# 4.1 固定窗口计数限流
@GetMapping("/counter") | |
@CounterRateLimit(maxRequest = 50, timeWindow = 2) | |
public String counter() { | |
return "下单成功"; | |
} |
配置说明:
-
maxRequest:时间窗口内允许的最大请求数
-
timeWindow:时间窗口大小,单位为秒
# 4.2 滑动窗口限流
@GetMapping("/slidingWindow") | |
@SlidingWindowRateLimit(maxRequest = 50, timeWindow = 2) | |
public String slidingWindow() { | |
return "下单成功"; | |
} |
配置说明:
-
maxRequest:滑动窗口内允许的最大请求数
-
timeWindow:滑动窗口大小,单位为秒
# 4.3 令牌桶限流
@GetMapping("/tokenBucket") | |
@TokenBucketRateLimit(permitsPerSecond = 50) | |
public String tokenBucket() { | |
return "下单成功"; | |
} |
配置说明:
- permitsPerSecond:每秒生成的令牌数量
# 4.4 漏桶限流
@GetMapping("/leakyBucket") | |
@LeakyBucketRateLimit(capacity = 50, leakRate = 2) | |
public String leakyBucket() { | |
return "下单成功"; | |
} |
配置说明:
-
capacity:漏桶容量
-
leakRate:漏出速率,单位为每秒
# 4.5 信号量限流
@GetMapping("/placeOrder") | |
@FrequencyControl(permits = 50, timeout = 1) | |
public String placeOrder() throws InterruptedException { | |
TimeUnit.SECONDS.sleep(2); | |
return "下单成功"; | |
} |
配置说明:
-
permits:允许的并发数
-
timeout:获取许可的超时时间,单位为秒
# 5. 总结与优化方向(Summary)
# 5.1 技术总结
本模块实现了多种限流算法,解决了高并发场景下的服务保护问题:
-
提供了从简单到复杂的多种限流方案,适应不同的业务场景
-
通过 AOP 实现非侵入式集成,便于在项目中灵活应用
-
各种算法各有特点,可以根据具体需求选择适合的方案
-
良好的代码结构和注释,便于理解和扩展
# 5.2 优化方向
-
分布式限流支持
-
基于 Redis 实现分布式限流,解决集群环境下的限流问题
-
使用 Redis 的 Lua 脚本保证原子性操作
-
-
自适应限流
-
根据系统负载动态调整限流参数
-
结合机器学习算法预测流量,提前调整限流策略
-
-
多维度限流
-
支持基于 IP、用户 ID、接口等多维度限流
-
实现更精细化的流量控制
-
-
流量调度
-
按优先级处理请求,保证核心业务
-
实现请求排队和延迟处理机制
-
-
监控和可视化
-
提供限流指标的监控和统计
-
开发可视化界面,便于运维人员调整限流策略
-