限流算法实战:令牌桶、漏桶与滑动窗口的工程实现
2026/6/27大约 7 分钟
限流算法实战:令牌桶、漏桶与滑动窗口的工程实现
秒杀系统被刷爆?API 网关被打满?下游服务扛不住?限流是高并发系统的第一道防线。四种限流算法 + 单机/分布式实现,一篇讲透。
一、四种限流算法
1.1 固定窗口
固定窗口(Fixed Window):
将时间划分为固定窗口(如每秒一个窗口)
窗口内请求数超过阈值则拒绝
0-1s: 100 请求(阈值 100)→ 放行
1-2s: 50 请求 → 放行
问题:临界点突发流量
0.9s: 100 请求 → 放行
1.1s: 100 请求 → 放行
→ 0.2 秒内放了 200 请求!1.2 滑动窗口
滑动窗口(Sliding Window):
窗口随时间滑动,统计过去 N 秒内的请求数
当前时间 1.5s,窗口 1s:
统计 0.5s ~ 1.5s 内的请求数
→ 避免了固定窗口的临界问题
实现方式:
1. 滑动窗口日志:记录每个请求时间戳,过期清理
2. 滑动窗口计数:将窗口分成小格,每格独立计数1.3 漏桶
漏桶(Leaky Bucket):
请求像水滴一样倒入桶中
桶以固定速率漏水(处理请求)
桶满了则拒绝新请求
┌─── 水滴(请求)流入 ───┐
│ │
│ ┌─────────┐ │
│ │ 桶(队列)│ │
│ │ ████░░░ │ │
│ └────┬────┘ │
│ │ 漏水 │
│ ▼ │
│ 固定速率流出 │
└───────────────────────┘
特点:平滑流量,但无法应对突发1.4 令牌桶
令牌桶(Token Bucket):
以固定速率往桶里放令牌
请求来时取一个令牌,取不到则拒绝
桶满了则丢弃多余令牌
┌─── 令牌以固定速率放入 ───┐
│ │
│ ┌──────────┐ │
│ │ 令牌桶 │ │
│ │ ◯◯◯◯◯◯◯ │ │
│ └────┬─────┘ │
│ │ 取令牌 │
│ ▼ │
│ 请求处理 │
└──────────────────────────┘
特点:允许突发流量(桶里积攒的令牌可以瞬间消费)1.5 漏桶 vs 令牌桶
| 维度 | 漏桶 | 令牌桶 |
|---|---|---|
| 流量 | 固定速率流出 | 允许突发 |
| 实现 | 队列 + 定时器 | 计数器 + 时间计算 |
| 适合 | 保护下游(平滑流量) | API 限流(允许突发) |
| 主流 | Guava RateLimiter | Sentinel |
二、单机限流实现
2.1 Guava RateLimiter(令牌桶)
import com.google.common.util.concurrent.RateLimiter;
// 每秒 100 个令牌(QPS = 100)
RateLimiter limiter = RateLimiter.create(100);
// 同步阻塞等待
for (int i = 0; i < 200; i++) {
double waitTime = limiter.acquire(); // 获取令牌,返回等待时间
System.out.println("Request " + i + " waited " + waitTime + "s");
}
// 超时获取
if (limiter.tryAcquire(1, 500, TimeUnit.MILLISECONDS)) {
// 500ms 内获取到令牌
handleRequest();
} else {
// 获取失败,拒绝
rejectRequest();
}
// 预热模式(SmoothWarmingUp)
RateLimiter warmupLimiter = RateLimiter.create(100, 10, TimeUnit.SECONDS);
// 前 10 秒令牌生成速率逐渐提升到 100/s,避免冷启动问题2.2 滑动窗口实现
public class SlidingWindowRateLimiter {
private final int maxRequests; // 窗口内最大请求数
private final long windowSizeMs; // 窗口大小(毫秒)
private final Deque<Long> timestamps; // 请求时间戳队列
public SlidingWindowRateLimiter(int maxRequests, long windowSizeMs) {
this.maxRequests = maxRequests;
this.windowSizeMs = windowSizeMs;
this.timestamps = new LinkedList<>();
}
public synchronized boolean tryAcquire() {
long now = System.currentTimeMillis();
long windowStart = now - windowSizeMs;
// 移除窗口外的时间戳
while (!timestamps.isEmpty() && timestamps.peekFirst() < windowStart) {
timestamps.pollFirst();
}
// 检查窗口内请求数
if (timestamps.size() < maxRequests) {
timestamps.addLast(now);
return true;
}
return false; // 限流
}
}
// 使用:1 秒内最多 100 个请求
SlidingWindowRateLimiter limiter = new SlidingWindowRateLimiter(100, 1000);
if (limiter.tryAcquire()) {
handleRequest();
} else {
rejectRequest();
}2.3 令牌桶实现
public class TokenBucketRateLimiter {
private final long capacity; // 桶容量
private final long refillRate; // 每毫秒补充令牌数
private long availableTokens; // 当前可用令牌
private long lastRefillTimestamp; // 上次补充时间
public TokenBucketRateLimiter(long capacity, long refillRatePerSecond) {
this.capacity = capacity;
this.refillRate = refillRatePerSecond / 1000;
this.availableTokens = capacity;
this.lastRefillTimestamp = System.currentTimeMillis();
}
public synchronized boolean tryAcquire(int tokens) {
refill();
if (availableTokens >= tokens) {
availableTokens -= tokens;
return true;
}
return false;
}
public synchronized boolean tryAcquire() {
return tryAcquire(1);
}
private void refill() {
long now = System.currentTimeMillis();
long elapsed = now - lastRefillTimestamp;
long newTokens = elapsed * refillRate;
if (newTokens > 0) {
availableTokens = Math.min(capacity, availableTokens + newTokens);
lastRefillTimestamp = now;
}
}
}
// 使用:桶容量 100,每秒补充 100 个令牌
TokenBucketRateLimiter limiter = new TokenBucketRateLimiter(100, 100);
// 允许瞬间消费 100 个令牌(突发),之后限制在 100 QPS三、分布式限流
3.1 Redis + Lua
-- 令牌桶限流 Lua 脚本
-- KEYS[1]: 限流 key
-- ARGV[1]: 桶容量
-- ARGV[2]: 每秒补充速率
-- ARGV[3]: 当前时间戳(毫秒)
-- ARGV[4]: 请求令牌数
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local refillRate = tonumber(ARGV[2]) / 1000 -- 转为每毫秒
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
-- 获取当前状态
local bucket = redis.call("HMGET", key, "tokens", "timestamp")
local tokens = tonumber(bucket[1]) or capacity
local lastTimestamp = tonumber(bucket[2]) or now
-- 计算补充的令牌
local elapsed = math.max(0, now - lastTimestamp)
local newTokens = math.min(capacity, tokens + elapsed * refillRate)
-- 判断是否允许
if newTokens >= requested then
newTokens = newTokens - requested
redis.call("HMSET", key, "tokens", newTokens, "timestamp", now)
redis.call("EXPIRE", key, 60) -- 60秒过期
return 1 -- 允许
else
redis.call("HMSET", key, "tokens", newTokens, "timestamp", now)
redis.call("EXPIRE", key, 60)
return 0 -- 拒绝
end// Java 调用
@Component
public class RedisRateLimiter {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private final DefaultRedisScript<Long> script = new DefaultRedisScript<>();
@PostConstruct
public void init() {
script.setScriptSource(new ResourceScriptSource(
new ClassPathResource("rate_limiter.lua")));
script.setResultType(Long.class);
}
public boolean tryAcquire(String key, long capacity, long refillRate, int tokens) {
Long result = redisTemplate.execute(
script,
Collections.singletonList(key),
String.valueOf(capacity),
String.valueOf(refillRate),
String.valueOf(System.currentTimeMillis()),
String.valueOf(tokens)
);
return result != null && result == 1;
}
// 使用
public boolean checkApiLimit(String userId) {
return tryAcquire("rate:user:" + userId, 100, 100, 1);
}
}3.2 Redis 固定窗口限流
@Component
public class RedisFixedWindowLimiter {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public boolean tryAcquire(String key, int limit, int windowSeconds) {
// 使用 INCR + EXPIRE 实现固定窗口
String luaScript = """
local count = redis.call('INCR', KEYS[1])
if count == 1 then
redis.call('EXPIRE', KEYS[1], ARGV[1])
end
return count
""";
DefaultRedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
Long count = redisTemplate.execute(
script,
Collections.singletonList(key),
String.valueOf(windowSeconds)
);
return count != null && count <= limit;
}
// 使用:每分钟最多 60 次
public boolean checkApiLimit(String userId) {
String key = "rate:api:" + userId + ":" +
LocalDate.now() + ":" +
(System.currentTimeMillis() / 60000);
return tryAcquire(key, 60, 60);
}
}四、Sentinel 限流
4.1 Sentinel 介绍
Sentinel(阿里开源):
以流量为切入点,提供限流、熔断、降级、系统保护
核心概念:
Resource:资源(接口、方法)
Rule:规则(限流规则、熔断规则)
Slot Chain:处理链(限流 → 熔断 → 降级)
Flow Slot:限流(令牌桶/滑动窗口/匀速排队)4.2 限流规则配置
import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
// 1. 配置限流规则
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule();
rule.setResource("queryOrder"); // 资源名
rule.setGrade(RuleConstant.FLOW_GRADE_QPS); // QPS 限流
rule.setCount(100); // 阈值 100 QPS
rule.setLimitApp("default"); // 来源
rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT); // 直接拒绝
// rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_WARM_UP); // 预热
// rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER); // 匀速排队
rules.add(rule);
FlowRuleManager.loadRules(rules);
// 2. 使用
try (Entry entry = SphU.entry("queryOrder")) {
// 业务逻辑
queryOrderFromDB();
} catch (BlockException e) {
// 被限流
return Result.error("系统繁忙,请稍后重试");
}4.3 Spring Cloud 集成
# application.yml
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8080 # Sentinel Dashboard
port: 8719 # 通信端口
filter:
enabled: true # 自动拦截所有 URL
# 限流规则(Nacos 动态配置)
management:
endpoints:
web:
exposure:
include: sentinel// 注解方式
@SentinelResource(
value = "queryOrder",
blockHandler = "queryOrderBlockHandler",
fallback = "queryOrderFallback"
)
public Order queryOrder(String orderId) {
return orderService.getById(orderId);
}
// 限流处理
public Order queryOrderBlockHandler(String orderId, BlockException ex) {
log.warn("queryOrder 被限流: {}", orderId);
return Order.defaultOrder();
}
// 降级处理
public Order queryOrderFallback(String orderId, Throwable ex) {
log.error("queryOrder 异常: {}", orderId, ex);
return Order.defaultOrder();
}五、多级限流架构
客户端
│
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
┌──────────────────────────────────────┐
│ API Gateway (Nginx) │ 第1级:IP限流
│ limit_req_zone │ 防爬虫/CC攻击
└──────────────────────────────────────┘
│
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
┌──────────────────────────────────────┐
│ Spring Cloud Gateway │ 第2级:用户限流
│ Sentinel / RateLimiter │ 按用户ID/IP限流
└──────────────────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ Application Service │ 第3级:接口限流
│ @SentinelResource │ 按接口/资源限流
└──────────────────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ Redis 分布式限流 │ 第4级:全局限流
│ Lua 脚本 │ 全局限流兜底
└──────────────────────────────────────┘Nginx 限流配置
# IP 限流:每个 IP 10 QPS
limit_req_zone $binary_remote_addr zone=ip_limit:10m rate=10r/s;
# 连接数限流:每个 IP 最多 50 个并发连接
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
server {
location /api/ {
limit_req zone=ip_limit burst=20 nodelay;
limit_conn conn_limit 50;
limit_req_status 429;
proxy_pass http://backend;
}
}六、限流后的处理策略
6.1 四种策略
// 1. 直接拒绝(快速失败)
return Result.error(429, "请求过于频繁,请稍后重试");
// 2. 排队等待(匀速通过)
// 适合消息消费场景
try {
RateLimiter limiter = RateLimiter.create(100);
limiter.acquire(); // 阻塞等待
processMessage(msg);
} catch (Exception e) {
// 超时处理
}
// 3. 降级(返回默认值或缓存)
@SentinelResource(fallback = "getCacheOrder")
public Order getOrder(String id) {
return orderService.getById(id);
}
public Order getCacheOrder(String id) {
return redisCache.get("order:" + id, Order.class);
}
// 4. 告警 + 记录
if (!limiter.tryAcquire()) {
metrics.increment("rate_limit.rejected");
alertService.send("接口限流: " + resourceName);
return Result.error(429, "系统繁忙");
}七、面试要点
Q:令牌桶和漏桶有什么区别?
令牌桶允许突发流量(桶里积攒的令牌可以瞬间消费),漏桶强制匀速输出(流量平滑)。令牌桶适合 API 限流(允许偶尔突发),漏桶适合保护下游服务(平滑流量)。
Q:滑动窗口怎么解决固定窗口的临界问题?
固定窗口在窗口边界可能放过 2 倍流量(0.9s 放 100 + 1.1s 放 100 = 200/0.2s)。滑动窗口不按固定边界,而是统计当前时刻往前 N 秒的请求数,时刻滑动,没有固定边界,不会出现临界突发。
Q:Sentinel 和 Hystrix 的区别?
| 维度 | Sentinel | Hystrix |
|---|---|---|
| 限流 | 支持(多种算法) | 不支持 |
| 熔断 | 支持 | 支持 |
| 降级 | 支持 | 支持 |
| 控制台 | 实时监控+动态配置 | Dashboard(功能弱) |
| 维护 | 阿里维护 | 停止维护 |
八、总结
限流方案选择:
- 单机限流:Guava RateLimiter(令牌桶),简单高效
- 分布式限流:Redis + Lua(令牌桶),全局精准
- 微服务限流:Sentinel(多种算法 + 动态规则 + 监控)
- 入口限流:Nginx limit_req(IP 级别,防爬虫)
记住:令牌桶允许突发,漏桶强制匀速,滑动窗口无临界问题,多级限流是最佳实践。