核心概念
限流(Rate Limiting)是高并发系统中保护后端服务的重要手段,通过控制请求速率防止系统过载。在秒杀场景中,限流尤为关键,因为瞬时流量可能是平时的数百倍甚至数千倍。
常见的限流算法主要有以下四种:
- 固定窗口计数器(Fixed Window Counter)
- 滑动窗口计数器(Sliding Window Counter)
- 漏桶算法(Leaky Bucket)
- 令牌桶算法(Token Bucket)
各算法原理及实现
1. 固定窗口计数器
原理:将时间划分为固定的窗口(如 1 秒),每个窗口内维护一个计数器,请求到来时计数器 +1,超过阈值则拒绝。
优点:实现简单,内存占用小
缺点:存在 临界问题,窗口边界可能出现 2 倍流量(如 0.9s 和 1.1s 各来 100 个请求,实际 200ms 内通过 200 个)
public class FixedWindowRateLimiter {
private final int limit;
private final long windowSize;
private AtomicInteger counter = new AtomicInteger(0);
private long windowStart = System.currentTimeMillis();
public FixedWindowRateLimiter(int limit, long windowSizeMs) {
this.limit = limit;
this.windowSize = windowSizeMs;
}
public synchronized boolean tryAcquire() {
long now = System.currentTimeMillis();
// 判断是否进入新窗口
if (now - windowStart >= windowSize) {
counter.set(0);
windowStart = now;
}
if (counter.get() < limit) {
counter.incrementAndGet();
return true;
}
return false;
}
}
2. 滑动窗口计数器
原理:将固定窗口细分为多个小窗口(如 1 秒分为 10 个 100ms 的格子),统计滑动时间范围内的总请求数。
优点:解决了固定窗口的临界问题,流量更平滑
缺点:内存占用稍大(需存储多个格子的计数)
public class SlidingWindowRateLimiter {
private final int limit;
private final long windowSize;
private final int slotCount;
private final long slotSize;
private final AtomicInteger[] slots;
private long startTime = System.currentTimeMillis();
public SlidingWindowRateLimiter(int limit, long windowSizeMs, int slotCount) {
this.limit = limit;
this.windowSize = windowSizeMs;
this.slotCount = slotCount;
this.slotSize = windowSizeMs / slotCount;
this.slots = new AtomicInteger[slotCount];
for (int i = 0; i < slotCount; i++) {
slots[i] = new AtomicInteger(0);
}
}
public synchronized boolean tryAcquire() {
long now = System.currentTimeMillis();
int currentSlot = (int) ((now / slotSize) % slotCount);
// 清理过期的格子
long slideTime = now - windowSize;
for (int i = 0; i < slotCount; i++) {
long slotTime = startTime + i * slotSize;
if (slotTime < slideTime) {
slots[i].set(0);
}
}
// 计算当前窗口内总请求数
int total = 0;
for (AtomicInteger slot : slots) {
total += slot.get();
}
if (total < limit) {
slots[currentSlot].incrementAndGet();
return true;
}
return false;
}
}
3. 漏桶算法
原理:请求像水滴进入漏桶,以恒定速率流出,桶满则溢出(拒绝请求)。
优点:强制流量以恒定速率处理,流量整形效果好
缺点:无法应对突发流量,响应速度不灵活
public class LeakyBucketRateLimiter {
private final int capacity; // 桶容量
private final int leakRate; // 漏出速率(个/秒)
private int water; // 当前水量
private long lastLeakTime;
public LeakyBucketRateLimiter(int capacity, int leakRate) {
this.capacity = capacity;
this.leakRate = leakRate;
this.water = 0;
this.lastLeakTime = System.currentTimeMillis();
}
public synchronized boolean tryAcquire() {
long now = System.currentTimeMillis();
// 先漏水
long leaked = (now - lastLeakTime) * leakRate / 1000;
water = Math.max(0, (int) (water - leaked));
lastLeakTime = now;
// 判断是否能加水
if (water < capacity) {
water++;
return true;
}
return false;
}
}
4. 令牌桶算法(最常用)
原理:系统以恒定速率向桶中放入令牌,请求到来时先获取令牌,有令牌则通过,无令牌则拒绝或等待。桶有最大容量。
优点:可应对突发流量(桶中积累的令牌可一次性消费),同时限制平均速率
缺点:实现略复杂
public class TokenBucketRateLimiter {
private final int capacity; // 桶容量
private final int refillRate; // 令牌生成速率(个/秒)
private int tokens;
private long lastRefillTime;
public TokenBucketRateLimiter(int capacity, int refillRate) {
this.capacity = capacity;
this.refillRate = refillRate;
this.tokens = capacity;
this.lastRefillTime = System.currentTimeMillis();
}
public synchronized boolean tryAcquire() {
refillTokens();
if (tokens > 0) {
tokens--;
return true;
}
return false;
}
private void refillTokens() {
long now = System.currentTimeMillis();
long elapsedTime = now - lastRefillTime;
int tokensToAdd = (int) (elapsedTime * refillRate / 1000);
if (tokensToAdd > 0) {
tokens = Math.min(capacity, tokens + tokensToAdd);
lastRefillTime = now;
}
}
}
分布式场景考量
单机 vs 分布式
单机限流:使用上述本地算法即可,Guava 的 RateLimiter 基于令牌桶实现。
分布式限流:需要共享限流状态,常用方案:
1. Redis + Lua 实现
// 令牌桶的 Redis + Lua 实现
public class RedisTokenBucketRateLimiter {
private final RedisTemplate<String, String> redisTemplate;
private final String key;
private final int capacity;
private final int refillRate;
// Lua 脚本保证原子性
private static final String LUA_SCRIPT =
"local key = KEYS[1]\n" +
"local capacity = tonumber(ARGV[1])\n" +
"local rate = tonumber(ARGV[2])\n" +
"local now = tonumber(ARGV[3])\n" +
"local tokens = tonumber(redis.call('hget', key, 'tokens') or capacity)\n" +
"local last_time = tonumber(redis.call('hget', key, 'last_time') or now)\n" +
"local elapsed = now - last_time\n" +
"local new_tokens = math.min(capacity, tokens + elapsed * rate / 1000)\n" +
"if new_tokens >= 1 then\n" +
" redis.call('hset', key, 'tokens', new_tokens - 1)\n" +
" redis.call('hset', key, 'last_time', now)\n" +
" redis.call('expire', key, 60)\n" +
" return 1\n" +
"else\n" +
" return 0\n" +
"end";
public boolean tryAcquire() {
Long result = redisTemplate.execute(
new DefaultRedisScript<>(LUA_SCRIPT, Long.class),
Collections.singletonList(key),
String.valueOf(capacity),
String.valueOf(refillRate),
String.valueOf(System.currentTimeMillis())
);
return result != null && result == 1;
}
}
2. 使用现成组件
- Sentinel:阿里开源,支持流控、降级、热点参数限流
- Resilience4j:轻量级,支持限流、熔断、重试
- Spring Cloud Gateway:网关层限流,基于 Redis + Lua
性能优化建议
- 网关层限流优先:尽早拦截无效请求,减轻后端压力
- 多级限流:网关 + 应用层 + 数据库层,层层防护
- 热点参数限流:针对热门商品单独限流(Sentinel 支持)
- 限流 + 降级:超限后返回友好提示或引导到排队页面
- 预热机制:秒杀前逐步提升限流阈值,避免冷启动
面试答题总结
核心考点:
- 四种算法的原理差异(重点是令牌桶 vs 漏桶)
- 临界问题的理解(固定窗口的缺陷)
- 分布式场景下 Redis + Lua 的实现思路
- 实际项目中的限流方案选型(如 Sentinel)
回答框架:
- 先说明限流目的(保护系统)
- 列举四种算法并对比(令牌桶最常用)
- 说明分布式场景需要 Redis 共享状态
- 补充实际项目经验(如用过 Sentinel 或网关限流)