在高并发系统中,限流是保护服务稳定性的核心手段。当请求量超过系统承载能力时,通过拒绝或延迟部分请求,可以防止级联故障,保障核心功能的可用性。理解不同限流算法的原理与适用场景,是设计高可用系统的必备技能。
一、限流的核心目标
| 目标 | 说明 |
|---|---|
| 保护服务 | 防止突发流量压垮后端 |
| 资源公平 | 防止单一用户/租户耗尽资源 |
| 降级预案 | 非核心请求让路给核心请求 |
| 成本可控 | 防止异常调用产生高额账单 |
二、计数器限流(固定窗口)
2.1 原理
时间窗口:每秒允许 100 个请求
0s-1s 1s-2s 2s-3s
┌──────┐ ┌──────┐ ┌──────┐
│ ████ │ │ ████ │ │ │
│ 80/100│ │ 100/100│ │ 0/100 │
└──────┘ └──────┘ └──────┘
问题:窗口切换瞬间可能允许 2 倍流量(边界突发)
2.2 Java 实现
public class FixedWindowRateLimiter {
private final long windowSizeMs;
private final int maxRequests;
private final AtomicInteger counter = new AtomicInteger(0);
private volatile long windowStart = System.currentTimeMillis();
public FixedWindowRateLimiter(int maxRequests, long windowSizeMs) {
this.maxRequests = maxRequests;
this.windowSizeMs = windowSizeMs;
}
public synchronized boolean tryAcquire() {
long now = System.currentTimeMillis();
// 检查是否需要重置窗口
if (now - windowStart >= windowSizeMs) {
counter.set(0);
windowStart = now;
}
int current = counter.incrementAndGet();
if (current <= maxRequests) {
return true;
}
// 超限,回滚计数
counter.decrementAndGet();
return false;
}
}
// 使用:每秒限制 100 次
FixedWindowRateLimiter limiter = new FixedWindowRateLimiter(100, 1000);
// 在 Filter/Interceptor 中使用
if (!limiter.tryAcquire()) {
response.setStatus(429);
return;
}
2.3 Redis 分布式实现
@Service
public class RedisFixedWindowRateLimiter {
@Autowired
private StringRedisTemplate redisTemplate;
public boolean isAllowed(String key, int maxRequests, int windowSeconds) {
String redisKey = "ratelimit:" + key;
long now = System.currentTimeMillis();
long windowStart = now / (windowSeconds * 1000) * (windowSeconds * 1000);
String windowKey = redisKey + ":" + windowStart;
Long count = redisTemplate.opsForValue().increment(windowKey);
if (count == 1) {
redisTemplate.expire(windowKey, windowSeconds, TimeUnit.SECONDS);
}
return count <= maxRequests;
}
}
2.4 优缺点
| 优点 | 缺点 |
|---|---|
| 实现简单 | 临界突发问题(2 倍流量) |
| 内存开销小 | 不平滑,窗口切换瞬时不均匀 |
| 不支持突发流量后快速恢复 |
三、滑动窗口限流
3.1 原理
滑动窗口将时间划分为更小的格子:
0 200 400 600 800 1000ms
|----|----|----|----|----|
▓▓ ▓ ▓▓ ░ ░ ▓ = 有请求,░ = 无请求
当前时间 = 850ms,窗口大小 = 1000ms
统计 50ms~850ms 区间的请求数,与前 200ms、400ms 等格子按时间比例加权
3.2 Java 实现
public class SlidingWindowRateLimiter {
private final int maxRequests;
private final long windowSizeMs;
private final int gridNum; // 窗口细分格子数
private final long gridSizeMs;
private final AtomicInteger[] grids; // 每个格子的计数
private volatile long windowStart;
public SlidingWindowRateLimiter(int maxRequests, long windowSizeMs, int gridNum) {
this.maxRequests = maxRequests;
this.windowSizeMs = windowSizeMs;
this.gridNum = gridNum;
this.gridSizeMs = windowSizeMs / gridNum;
this.grids = new AtomicInteger[gridNum];
for (int i = 0; i < gridNum; i++) {
grids[i] = new AtomicInteger(0);
}
this.windowStart = System.currentTimeMillis();
}
public synchronized boolean tryAcquire() {
long now = System.currentTimeMillis();
long elapsed = now - windowStart;
// 计算需要滑动的格子数
int shiftGrids = (int) (elapsed / gridSizeMs);
if (shiftGrids >= gridNum) {
// 整个窗口已过期,全部清零
for (AtomicInteger grid : grids) {
grid.set(0);
}
windowStart = now;
shiftGrids = 0;
} else if (shiftGrids > 0) {
// 滑动:清零过期的格子
for (int i = 0; i < shiftGrids; i++) {
grids[(i + getCurrentGridIndex()) % gridNum].set(0);
}
windowStart += shiftGrids * gridSizeMs;
}
// 统计当前窗口内请求总数
int total = Arrays.stream(grids).mapToInt(AtomicInteger::get).sum();
if (total < maxRequests) {
grids[getCurrentGridIndex()].incrementAndGet();
return true;
}
return false;
}
private int getCurrentGridIndex() {
return (int) ((System.currentTimeMillis() / gridSizeMs) % gridNum);
}
}
四、令牌桶算法
4.1 原理
令牌以固定速率放入桶中,请求需要消耗令牌才能通过:
┌─────────┐
│ Token │ ← 以 rate 速率放入令牌
│ Bucket │
│ [●●●] │ ← 桶容量 capacity
└────┬────┘
│ 请求来时取令牌
▼
Service
优点:允许一定突发(桶中有积累),平滑限流
4.2 Guava RateLimiter
import com.google.common.util.concurrent.RateLimiter;
@Service
public class GuavaRateLimitService {
// 每秒 1000 个许可(平滑突发)
private final RateLimiter rateLimiter = RateLimiter.create(1000.0);
// 每秒 1000 个许可(平滑预热,启动时逐渐加速)
private final RateLimiter warmingLimiter = RateLimiter.create(
1000.0, // QPS
5, // 预热时间(秒)
TimeUnit.SECONDS // 时间单位
);
public void processRequest() {
// 阻塞获取(直到获得许可)
rateLimiter.acquire();
doWork();
}
public boolean tryProcess() {
// 非阻塞尝试(立即返回)
if (rateLimiter.tryAcquire()) {
doWork();
return true;
}
return false; // 被限流
}
public boolean tryProcessWithTimeout() {
// 带超时的尝试
if (rateLimiter.tryAcquire(100, TimeUnit.MILLISECONDS)) {
doWork();
return true;
}
return false;
}
}
4.3 自建令牌桶
public class TokenBucketRateLimiter {
private final long capacity; // 桶容量
private final double ratePerMs; // 令牌产生速率(每毫秒)
private final AtomicLong tokens; // 当前令牌数(放大 1000 倍避免浮点)
private volatile long lastRefillTime;
public TokenBucketRateLimiter(long capacity, double permitsPerSecond) {
this.capacity = capacity * 1000;
this.ratePerMs = permitsPerSecond;
this.tokens = new AtomicLong(this.capacity);
this.lastRefillTime = System.currentTimeMillis();
}
public synchronized boolean tryAcquire(int permits) {
refill();
long required = permits * 1000L;
long current = tokens.get();
if (current >= required) {
tokens.addAndGet(-required);
return true;
}
return false;
}
private void refill() {
long now = System.currentTimeMillis();
long elapsed = now - lastRefillTime;
if (elapsed > 0) {
long newTokens = (long) (elapsed * ratePerMs * 1000);
long current = tokens.get();
long updated = Math.min(capacity, current + newTokens);
tokens.set(updated);
lastRefillTime = now;
}
}
}
五、漏桶算法
5.1 原理
请求像水一样流入桶,以固定速率流出:
请求流入(任意速率)
│
▼
┌───────────┐
│ ██ │ ← 桶(有容量限制,满了溢出)
│ ██ │
└────┬──────┘
│ 固定速率流出
▼
处理
特点:强行限制流出速率,绝对平滑,但不灵活
5.2 Java 实现
public class LeakyBucketRateLimiter {
private final long capacity; // 桶容量
private final long leakRateMs; // 漏出速率(每毫秒)
private final BlockingQueue<Long> bucket; // 用队列模拟桶
private volatile long lastLeakTime;
public LeakyBucketRateLimiter(long capacity, long permitsPerSecond) {
this.capacity = capacity;
this.leakRateMs = permitsPerSecond / 1000;
this.bucket = new LinkedBlockingQueue<>((int) capacity);
this.lastLeakTime = System.currentTimeMillis();
// 启动漏出线程
startLeaking();
}
private void startLeaking() {
Thread leakThread = new Thread(() -> {
while (!Thread.interrupted()) {
try {
Long request = bucket.poll(leakRateMs, TimeUnit.MILLISECONDS);
if (request != null) {
processRequest(request);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
});
leakThread.setDaemon(true);
leakThread.start();
}
public boolean tryAcquire() {
return bucket.offer(System.currentTimeMillis());
}
private void processRequest(long timestamp) {
// 处理请求
}
}
六、算法对比与选型
| 算法 | 突发处理 | 平滑性 | 实现复杂度 | 内存占用 | 适用场景 |
|---|---|---|---|---|---|
| 固定窗口 | 差 | 差 | 简单 | 低 | 简单计数场景 |
| 滑动窗口 | 中 | 中 | 中等 | 中 | 较为精确的限流 |
| 令牌桶 | 好 | 好 | 中等 | 低 | 允许突发,平滑限流 |
| 漏桶 | 无 | 极好 | 中等 | 低 | 严格匀速处理 |
6.1 应用场景
// 场景 1:API 网关限流(令牌桶,允许突发)
RateLimiter apiLimiter = RateLimiter.create(10000); // 10k QPS
// 场景 2:短信发送(漏桶,严格匀速)
// 每秒最多发 10 条,避免触发运营商限制
// 场景 3:用户级限流(Redis 滑动窗口)
// 每个用户每分钟最多操作 60 次
// 场景 4:系统保护(固定窗口,简单有效)
// 全局每秒最多 50,000 请求
七、Sentinel 中的限流实现
7.1 滑动窗口统计
Sentinel 使用高性能的滑动窗口算法:
// 核心数据结构(LeapArray)
public abstract class LeapArray<T> {
protected int windowLengthInMs; // 窗口长度(默认 500ms)
protected int sampleCount; // 采样数(默认 2)
protected int intervalInMs; // 统计间隔(默认 1000ms)
protected final AtomicReferenceArray<WindowWrap<T>> array;
// 获取当前窗口(无锁设计,CAS 操作)
public WindowWrap<T> currentWindow(long timeMillis) {
// 计算时间戳对应的窗口索引
int idx = calculateTimeIdx(timeMillis);
// 计算窗口开始时间
long windowStart = calculateWindowStart(timeMillis);
while (true) {
WindowWrap<T> old = array.get(idx);
if (old == null) {
// 窗口未初始化,CAS 创建
WindowWrap<T> window = new WindowWrap<>(windowLengthInMs, windowStart, newEmptyBucket());
if (array.compareAndSet(idx, null, window)) {
return window;
}
} else if (windowStart == old.windowStart()) {
// 窗口命中
return old;
} else if (windowStart > old.windowStart()) {
// 窗口已过期,重置
if (updateLock.tryLock()) {
try {
return resetWindowTo(old, windowStart);
} finally {
updateLock.unlock();
}
}
}
// else: 时钟回拨,等待
}
}
}
7.2 热点参数限流
@GetMapping("/api/goods/{goodsId}")
@SentinelResource(value = "getGoods", blockHandler = "blockHandler")
public Goods getGoods(@PathVariable Long goodsId) {
return goodsService.getById(goodsId);
}
// 规则配置:对 goodsId 维度限流
ParamFlowRule rule = new ParamFlowRule("getGoods")
.setParamIdx(0) // 第 0 个参数
.setGrade(RuleConstant.FLOW_GRADE_QPS)
.setCount(100) // 每个 goodsId 100 QPS
.setDurationInSec(1); // 统计周期 1 秒
八、分布式限流
8.1 Redis + Lua 实现
-- redis-ratelimiter.lua
local key = KEYS[1]
local window = tonumber(ARGV[1])
local threshold = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
-- 清理过期数据
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
-- 统计当前窗口请求数
local count = redis.call('ZCARD', key)
if count < threshold then
-- 允许,记录请求
redis.call('ZADD', key, now, now .. ':' .. redis.call('INCR', key .. ':seq'))
redis.call('EXPIRE', key, math.ceil(window / 1000))
return 1
else
return 0
end
@Service
public class DistributedRateLimiter {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String LUA_SCRIPT =
"local key = KEYS[1] " +
"local window = tonumber(ARGV[1]) " +
"local threshold = tonumber(ARGV[2]) " +
"local now = tonumber(ARGV[3]) " +
"redis.call('ZREMRANGEBYSCORE', key, 0, now - window) " +
"local count = redis.call('ZCARD', key) " +
"if count < threshold then " +
" redis.call('ZADD', key, now, now) " +
" redis.call('EXPIRE', key, math.ceil(window / 1000)) " +
" return 1 " +
"else " +
" return 0 " +
"end";
private final RedisScript<Long> script = new DefaultRedisScript<>(LUA_SCRIPT, Long.class);
public boolean isAllowed(String key, int maxRequests, long windowMs) {
Long result = redisTemplate.execute(
script,
Collections.singletonList("ratelimit:" + key),
String.valueOf(windowMs),
String.valueOf(maxRequests),
String.valueOf(System.currentTimeMillis())
);
return result != null && result == 1;
}
}
8.2 Redisson 限流器
@Service
public class RedissonRateLimitService {
@Autowired
private RedissonClient redisson;
public boolean tryAcquire(String key, long rate, long rateInterval, RateIntervalUnit unit) {
RRateLimiter limiter = redisson.getRateLimiter("rl:" + key);
// 初始化限流器(首次调用)
limiter.trySetRate(RateType.OVERALL, rate, rateInterval, unit);
return limiter.tryAcquire();
}
}
// 使用
boolean allowed = redissonRateLimitService.tryAcquire(
"user:" + userId, // key
10, // 10 个许可
1, // 时间间隔
RateIntervalUnit.MINUTES // 每分钟
);
九、最佳实践
9.1 分层限流策略
流量入口层级:
第一层:Nginx / CDN
└── 基于 IP / User-Agent 的粗粒度限流
第二层:API Gateway(Spring Cloud Gateway + Sentinel)
└── 路由级、API 分组级限流
第三层:服务层(Sentinel / Guava)
└── 接口级、用户级、热点参数级限流
第四层:数据库连接池 / 线程池
└── 资源级硬限制
9.2 限流响应设计
// 统一的限流响应
public class RateLimitResponse {
public static final ErrorResponse TOO_MANY_REQUESTS = new ErrorResponse(
429,
"Too Many Requests",
"请求过于频繁,请稍后重试",
Map.of(
"Retry-After", "60", // 建议客户端等待秒数
"X-RateLimit-Limit", "100", // 限制次数
"X-RateLimit-Remaining", "0", // 剩余次数
"X-RateLimit-Reset", "1234567890" // 重置时间戳
)
);
}
9.3 监控与告警
@Component
public class RateLimitMetrics {
private final MeterRegistry meterRegistry;
private final Counter blockedCounter;
private final Counter allowedCounter;
public RateLimitMetrics(MeterRegistry registry) {
this.meterRegistry = registry;
this.blockedCounter = Counter.builder("ratelimit.blocked")
.description("被限流的请求数")
.register(registry);
this.allowedCounter = Counter.builder("ratelimit.allowed")
.description("通过的请求数")
.register(registry);
}
public void record(boolean allowed) {
if (allowed) {
allowedCounter.increment();
} else {
blockedCounter.increment();
}
}
}
十、总结
| 算法 | 核心思想 | 选择建议 |
|---|---|---|
| 固定窗口 | 计数 + 定时重置 | 简单场景,容忍临界突发 |
| 滑动窗口 | 细分格子滑动统计 | 需要较精确限流 |
| 令牌桶 | 定速生产 + 消耗 | 允许突发,平滑输出 |
| 漏桶 | 定速消费 + 缓存 | 严格限速,强制匀速 |
限流是高并发系统的"保险丝"。在实际工程中,通常采用多层限流策略:网关层做粗粒度防护,服务层做精细控制,配合监控告警和降级预案,构建完整的流量治理能力。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。