本文目录导读:

- 目录导读
- 限流:为什么它是高并发系统的“安全气囊”?
- 四大经典限流算法核心原理与Java手写实现
- 生产级限流利器:Guava RateLimiter与Redisson RRateLimiter
- 实战案例:基于Spring Boot + AOP + 自定义注解实现接口限流
- 热点问答(FAQ):面试官最爱问的限流陷阱
- 性能对比与选型建议:何时用本地限流,何时用分布式限流?
目录导读
- 限流:为什么它是高并发系统的“安全气囊”?
- 四大经典限流算法核心原理与Java手写实现
- 1 固定窗口计数器(Fixed Window Counter)
- 2 滑动窗口日志(Sliding Window Log)
- 3 漏桶算法(Leaky Bucket)
- 4 令牌桶算法(Token Bucket)
- 生产级限流利器:Guava RateLimiter与Redisson RRateLimiter
- 实战案例:基于Spring Boot + AOP + 自定义注解实现接口限流
- 热点问答(FAQ):面试官最爱问的限流陷阱
- 性能对比与选型建议:何时用本地限流,何时用分布式限流?
限流:为什么它是高并发系统的“安全气囊”?
在微服务架构与秒杀场景中,流量洪峰往往在毫秒级爆发,如果不加控制,数据库连接池会被瞬间打满,线程池阻塞,最终导致雪崩。限流(Rate Limiting) 的核心目标是:控制单位时间内的请求速率,保证系统的可用性与稳定性,它与熔断(Circuit Breaker)、降级(Degradation)并称高可用“三剑客”。
关键术语:
- QPS(Queries Per Second):每秒查询率,衡量吞吐量。
- 阈值(Threshold):允许的最大请求数/速率。
- 超卖/超限:超过阈值后的策略(拒绝、排队、随机丢弃)。
四大经典限流算法核心原理与Java手写实现
1 固定窗口计数器(Fixed Window Counter)
原理:将时间划分为固定大小的窗口(如1秒),每个窗口维护一个计数器,请求进入则计数器+1,若计数器超过阈值则拒绝,直到下个窗口重置。
缺点:存在“临界突变”问题——假设阈值10,前0.9秒无请求,最后0.1秒涌入10个请求,紧接着下个窗口又涌入10个请求,实际20个请求在0.2秒内通过,可能压垮服务。
代码案例:
public class FixedWindowCounter {
private final int maxCount; // 窗口允许的最大请求数
private final long windowSizeMs; // 窗口大小(毫秒)
private long windowStart = System.currentTimeMillis();
private int count;
public FixedWindowCounter(int maxCount, long windowSizeMs) {
this.maxCount = maxCount;
this.windowSizeMs = windowSizeMs;
}
public synchronized boolean tryAcquire() {
long now = System.currentTimeMillis();
if (now - windowStart >= windowSizeMs) {
windowStart = now;
count = 0;
}
if (count < maxCount) {
count++;
return true;
}
return false;
}
}
2 滑动窗口日志(Sliding Window Log)
原理:记录每个请求的时间戳,存入有序集合(如TreeSet),每次请求时,剔除所有超出当前窗口(如1秒)的旧时间戳,若剩余数量 < 阈值则放行。
优点:解决了固定窗口的临界问题,精度高(可精确到毫秒粒度)。 缺点:内存占用高(需要存储所有请求时间戳)。
代码案例:
public class SlidingWindowLog {
private final int maxCount;
private final long windowSizeMs;
private final TreeSet<Long> timestamps = new TreeSet<>();
public SlidingWindowLog(int maxCount, long windowSizeMs) {
this.maxCount = maxCount;
this.windowSizeMs = windowSizeMs;
}
public synchronized boolean tryAcquire() {
long now = System.currentTimeMillis();
long earliest = now - windowSizeMs;
// 移除过期时间戳
timestamps.headSet(earliest + 1).clear();
if (timestamps.size() < maxCount) {
timestamps.add(now);
return true;
}
return false;
}
}
3 漏桶算法(Leaky Bucket)
原理:请求先进入一个固定容量的桶,桶底以固定速率“漏出”请求(即处理请求),如果桶满了,则拒绝新请求。控制的是“流出速率”。
代码案例(基于线程池模拟):
public class LeakyBucket {
private final int capacity; // 桶容量
private final double leakRatePerSec; // 溢出速率(每秒)
private long lastLeakTime = System.nanoTime();
private double water = 0;
public LeakyBucket(int capacity, double leakRatePerSec) {
this.capacity = capacity;
this.leakRatePerSec = leakRatePerSec;
}
public synchronized boolean tryAcquire() {
long now = System.nanoTime();
// 先漏水:根据时间差计算漏掉的水量
double elapsed = (now - lastLeakTime) / 1e9;
water = Math.max(0, water - elapsed * leakRatePerSec);
lastLeakTime = now;
if (water < capacity) {
water += 1; // 加水
return true;
}
return false;
}
}
4 令牌桶算法(Token Bucket)—— 最推荐方案
原理:系统以恒定速率往桶里放入令牌(token),请求必须获取到令牌才能被处理,桶有最大容量(突发流量可预存令牌)。控制的是“流入速率”,且允许短暂突发。
代码案例:
public class TokenBucket {
private final int capacity; // 桶容量(最大令牌数)
private final double refillRatePerSec; // 每秒放入令牌数
private double tokens = 0;
private long lastRefillTime = System.nanoTime();
public TokenBucket(int capacity, double refillRatePerSec) {
this.capacity = capacity;
this.refillRatePerSec = refillRatePerSec;
}
public synchronized boolean tryAcquire(int numTokens) {
long now = System.nanoTime();
double elapsed = (now - lastRefillTime) / 1e9;
// 补充令牌,但不能超过容量
tokens = Math.min(capacity, tokens + elapsed * refillRatePerSec);
lastRefillTime = now;
if (tokens >= numTokens) {
tokens -= numTokens;
return true;
}
return false;
}
}
生产级限流利器:Guava RateLimiter与Redisson RRateLimiter
手写代码适合理解原理,但生产环境建议使用成熟类库。
- Guava RateLimiter:基于令牌桶实现,但内部使用平滑突发限制(SmoothBursty) 与平滑预热限制(SmoothWarmingUp) 两种模式,支持
acquire()(阻塞)与tryAcquire()(非阻塞)。RateLimiter limiter = RateLimiter.create(10); // 每秒10个令牌 if (limiter.tryAcquire(1, 100, TimeUnit.MILLISECONDS)) { // 业务处理 } - Redisson RRateLimiter:分布式限流,底层基于Redis Lua脚本,原子性保证多实例下的精确控制,适用于微服务集群。
实战案例:基于Spring Boot + AOP + 自定义注解实现接口限流
场景:对 @RateLimit 注解标注的接口进行每秒最多5次访问限制。
步骤:
-
定义注解:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RateLimit { double qps() default 5.0; } -
切面类(使用Guava RateLimiter,ConcurrentHashMap存储不同方法的RateLimiter):
@Aspect @Component public class RateLimitAspect { private final Map<String, RateLimiter> limiters = new ConcurrentHashMap<>(); @Around("@annotation(rateLimit)") public Object around(ProceedingJoinPoint pjp, RateLimit rateLimit) throws Throwable { String methodKey = pjp.getSignature().toLongString(); RateLimiter limiter = limiters.computeIfAbsent(methodKey, k -> RateLimiter.create(rateLimit.qps())); if (!limiter.tryAcquire(1, 50, TimeUnit.MILLISECONDS)) { throw new RuntimeException("Too many requests, please try later."); } return pjp.proceed(); } } -
在Controller使用:
@RestController public class DemoController { @GetMapping("/api/order") @RateLimit(qps = 5) public String createOrder() { return "order created"; } }
扩展:如需分布式,可将 RateLimiter 替换为Redisson的 RRateLimiter,传入统一的Redis连接。
热点问答(FAQ):面试官最爱问的限流陷阱
Q1:固定窗口计数器和滑动窗口日志的区别?何时选哪个?
A:固定窗口实现简单,但有“临界突变”风险;滑动窗口日志通过存储时间戳解决精准问题,但内存开销大。如果内存紧张且QPS低于1000,选固定窗口+随机丢弃策略;如果需要严格平滑,选滑动窗口或令牌桶。
Q2:漏桶和令牌桶的根本差异?
A:漏桶强制平滑流出(不管流量多突发,处理速率恒定),适合“保护下游系统”的场景(如数据库写入),但不允许突发,令牌桶允许预存令牌,能应对突发流量(如电商大促闪购),适合“提升用户体验”的API网关。
Q3:Guava RateLimiter为什么不支持分布式?
A:Guava的令牌桶状态存储在本地JVM内存中,多实例部署时各自独立,无法共享全局速率,若需集群限流,必须用Redis或Sentinel(阿里开源)等中央存储方案。
Q4:限流应该放在网关还是业务层?
A:最佳实践是两层都做,网关层(如Spring Cloud Gateway)做全局粗粒度限流(按IP或用户);业务层针对特殊接口(如秒杀)做精细粒度限流(按用户ID)。
性能对比与选型建议:何时用本地限流,何时用分布式限流?
| 算法/工具 | 精度 | 内存开销 | 允许突发 | 分布式支持 | 推荐场景 |
|---|---|---|---|---|---|
| 固定窗口计数器 | 低 | 低 | 是 | 需自实现 | 简单内部接口(QPS<500) |
| 滑动窗口日志 | 高 | 高 | 否 | 需自实现 | 对平滑度要求极高的金融接口 |
| 漏桶 | 中 | 低 | 否 | 需自实现 | 保护下游慢资源(写数据库) |
| 令牌桶(Guava) | 高 | 低 | 是 | 否 | 单机应用默认首选 |
| 令牌桶(Redisson) | 高 | 低(居中) | 是 | 是 | 微服务集群、跨实例全局限流 |
核心选型建议:
- 单机应用:直接使用Guava RateLimiter,代码简洁,性能损耗小于0.1ms。
- 微服务集群(无网关):使用Redisson RRateLimiter(基于Redis Lua脚本),注意Redis单点问题,可搭配集群模式。
- 有网关(如Spring Cloud Gateway):优先使用网关内置的
RequestRateLimiter过滤器(底层是Redis令牌桶),业务层无需再写限流逻辑。
最后总结:限流算法的本质是“用空间换时间”或“用时间换空间”的取舍,理解四种算法的数学原理,掌握Guava与Redisson的API,并能在Spring Boot中通过AOP优雅落地,是Java后端工程师进阶的必备技能,建议在实际项目中先压测确定阈值,再根据下游依赖的“脆弱程度”选择最合适的算法。