接口限流方案
大家应该都知道,在高并发系统中,为保障服务稳定可靠,限流和降级是最常见的操作手段,能有效地保证服务可持续输出,达到保护系统的目的。那么如何去限流呢?
首先需要知道系统的吞吐量,比如我们通过压力测试,得到系统吞吐量的阙值是100,那就意味着只要单位时间内的请求不超过100,系统就会稳定运行,一旦达到或超过这个阈值,就需要采取一些措施以限制服务访问流量不超过阙值,达到限制流量的目的。常用的策略如:延迟处理,拒绝处理,或者部分拒绝处理等等。
限流算法
1、固定窗口计数算法(Fixed Window Counter)
一种简单的限流算法,是将时间分为N个固定的窗口,计数当前窗口内的请求数。每当一个请求到来时,计数器就加1,如果计数器的值超过了设定的阈值,则后续的请求会被拒绝。

优点:
- 实现计数算法非常简单,易于理解和实现。
- 在分布式系统中,可以通过 Redis 等工具的原子操作来实现计数器的更新,确保计数的准确性。
缺点:
- 突发流量的问题:在时间窗口边界处,可能会出现突发流量的问题。
- 临界的问题:在固定时间窗口内,如果请求集中在窗口的最后一段时间内到达,会导致虽然每个窗口内的请求没有超过阈值,但总体上超过了系统的处理能力
2、滑动窗口计数算法(Sliding Window Counter)
在固定窗口计数的基础上,引入滑动窗口,细化时间粒度。它将单位时间周期分为n个小周期,分别记录每个小周期内接口的访问次数,并且根据时间滑动删除过期的小周期。它可以解决固定窗口临界值的问题。

优点:
- 能够更精确地控制流量,避免固定窗口算法在边界处的突变问题,使得流量限制更加平滑。
缺点:
- 相对于固定窗口算法,实现复杂度较高,需要维护多个子窗口的计数器状态。
3、漏桶算法(Leaky Bucket)
这个算法的思路很简单,把请求的比作水滴,让水滴先流进漏桶,在漏桶的下面,按需做对应大小的孔,让水按固定速率流出,当流出的速率小于流入的速率时,由于桶的容量有限,后续进入的水直接溢出(拒绝请求),以此实现限流。

优点:
- 简单易实现,容易理解和部署。
- 需通过固定输出速率,保证了网络延迟的稳定性。
缺点:
- 由于输出速率固定,用户请求的响应时间不可预测,会因请求量的变化而波动。
- 漏桶算法不允许突发流量,对于需要高突发容量的应用效果不佳。
4、令牌桶算法(Token Bucket)
以固定的速率颁发令牌,请求获得令牌则被接受,否则被拒绝。此算法主要用于网络流量整形和速率限制的算法,用来控制系统的请求处理速率,防止突发流量导致服务过载。其核心思想是通过“令牌”的生成和消费来控制请求的速率,同时允许一定程度的流量突发。

优点:
- 实现相对简单,通过固定速率向桶中添加令牌,每个请求都需要消耗一个令牌,如果桶中有足够的令牌,请求将立即被处理;如果没有令牌,请求可以被延迟处理或拒绝。
- 桶中可以积累令牌以应对短时间内的请求峰值。
缺点:
- 可能会导致响应时间的变化。当令牌桶中的令牌不足时,请求会被延迟处理或拒绝,这可能导致用户体验下降。
- 在低负载情况下,令牌桶令牌不能被使用,可能会造成资源浪费。
限流实现
注解定义
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
int count() default 100; // 默认每秒100次请求
int time() default 1; // 时间窗口(秒)
}切面拦截
java
@Aspect
@Component
public class RateLimitAspect {
private final Map<String, RateLimiter> limiters = new ConcurrentHashMap<>();
@Before("@annotation(rateLimit)")
public void checkRate(JoinPoint jp, RateLimit rateLimit) {
String key = jp.getSignature().toLongString();
RateLimiter limiter = limiters.computeIfAbsent(key,
k -> RateLimiter.create(rateLimit.count() / (double)rateLimit.time()));
if (!limiter.tryAcquire()) {
throw new RuntimeException("请求过于频繁");
}
}
}接口示例
java
@RestController
public class TestController {
@RateLimit(count=10, time=1) // 限制每秒10次
@GetMapping("/test")
public String testRateLimit() {
return "ok";
}
}