一、引言
限流是高并发系统的核心保障手段。面对突发流量,如何选择合适的限流方案?
今天我要对比三种主流限流方案:Guava RateLimiter(应用级令牌桶)、Sentinel(分布式滑动窗口)、Nginx limit_req(网关层漏桶)。通过真实压测数据,告诉你哪种方案最适合你的场景。
二、限流算法原理
2.1 三种算法对比
| 算法 | 核心思想 | 突发处理 | 适用场景 |
|---|---|---|---|
| 令牌桶 | 固定速率生成令牌,请求消耗令牌 | 允许突发(桶内累积令牌) | API 限流、削峰填谷 |
| 漏桶 | 请求入队列,固定速率流出 | 平滑输出,拒绝突发 | 网关限流、流量整形 |
| 滑动窗口 | 时间窗口内计数,动态调整 | 精确控制,平滑过渡 | 分布式限流、热点参数 |
2.2 算法图示
令牌桶算法 (Token Bucket)
┌──────────────────────────┐
│ 令牌桶 │
│ ┌──────────────────┐ │
│ │ ● ● ● ● ● ● ● ● │ │
│ │ (令牌) │ │
│ └──────────────────┘ │
│ ↓ 生成速率 = 1000/s │
│ │
│ 请求 → 取令牌 → 通过/拒绝 │
└──────────────────────────┘
漏桶算法 (Leaky Bucket)
┌──────────────────────────┐
│ 请求队列 │
│ ┌──────────────────┐ │
│ │ 请求1 → 请求2 → │ │
│ │ 请求3 → 请求4 → │ │
│ └──────────────────┘ │
│ ↓ 流出速率 = 1000/s │
│ │
│ 队列满 → 拒绝请求 │
└──────────────────────────┘
滑动窗口算法 (Sliding Window)
┌─────────────────────────────────────────────┐
│ 时间轴: 0 1 2 3 4 5 6 │
│ ↓ ↓ ↓ ↓ ↓ ↓ ↓ │
│ 窗口: └─────┘ │
│ └─────┘ │
│ └─────┘ │
│ └─────┘ │
│ └─────┘ │
│ └─────┘ │
│ 每个窗口计数,超过阈值拒绝 │
└─────────────────────────────────────────────┘
三、方案一:Guava RateLimiter
3.1 核心特性
- 算法:令牌桶(Token Bucket)
- 模式:SmoothBursty(允许突发)、SmoothWarmingUp(预热)
- 分布式支持:不支持(单机)
- 重启影响:计数清零,需重新累积
3.2 使用示例
import com.google.common.util.concurrent.RateLimiter;
public class RateLimiterService {
// SmoothBursty 模式:允许突发
private final RateLimiter burstyLimiter = RateLimiter.create(1000.0);
// SmoothWarmingUp 模式:预热期 3 秒
private final RateLimiter warmingUpLimiter = RateLimiter.create(1000.0, 3, TimeUnit.SECONDS);
public boolean tryAcquire() {
// 非阻塞:尝试获取令牌,立即返回结果
return burstyLimiter.tryAcquire();
}
public boolean tryAcquireWithTimeout(long timeout, TimeUnit unit) {
// 带超时的非阻塞获取
return burstyLimiter.tryAcquire(timeout, unit);
}
public void acquire() {
// 阻塞:等待直到获取到令牌
burstyLimiter.acquire();
}
}
3.3 在 Controller 中使用
@RestController
@RequestMapping("/api")
public class RateLimiterController {
@Autowired
private RateLimiterService rateLimiterService;
@GetMapping("/resource")
public Result<String> getResource() {
if (!rateLimiterService.tryAcquire()) {
return Result.fail(429, "请求过于频繁,请稍后重试");
}
// 业务逻辑
return Result.success("success");
}
}
3.4 优缺点
| 优点 | 缺点 |
|---|---|
| 轻量级,零依赖 | 不支持分布式 |
| API 简洁,易于集成 | 进程重启后计数清零 |
| 支持突发和预热模式 | 无可视化控制台 |
四、方案二:Sentinel
4.1 核心特性
- 算法:滑动窗口(Sliding Window)
- 模式:单机限流、集群限流(需 Token Server)
- 分布式支持:支持(需部署集群)
- 额外功能:热点参数限流、熔断降级、控制台可视化
4.2 引入依赖
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>1.8.6</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-web-servlet</artifactId>
<version>1.8.6</version>
</dependency>
<!-- 控制台 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-transport-simple-http</artifactId>
<version>1.8.6</version>
</dependency>
4.3 基本配置
@Configuration
public class SentinelConfig {
@Bean
public FilterRegistrationBean<SentinelFilter> sentinelFilter() {
FilterRegistrationBean<SentinelFilter> bean = new FilterRegistrationBean<>();
bean.setFilter(new SentinelFilter());
bean.addUrlPatterns("/*");
return bean;
}
@PostConstruct
public void initRules() {
// 限流规则:QPS 阈值 1000
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule();
rule.setResource("api-resource");
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setCount(1000);
rule.setStrategy(RuleConstant.STRATEGY_DIRECT);
rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT);
rules.add(rule);
FlowRuleManager.loadRules(rules);
}
}
4.4 在代码中使用
@RestController
@RequestMapping("/api")
public class SentinelController {
@GetMapping("/resource")
public Result<String> getResource() {
// 资源名
String resource = "api-resource";
// 尝试获取权限
Entry entry = null;
try {
entry = SphU.entry(resource);
// 业务逻辑
return Result.success("success");
} catch (BlockException e) {
return Result.fail(429, "请求过于频繁,请稍后重试");
} finally {
if (entry != null) {
entry.exit();
}
}
}
}
4.5 热点参数限流
@PostConstruct
public void initHotspotRules() {
List<ParamFlowRule> rules = new ArrayList<>();
ParamFlowRule rule = new ParamFlowRule();
rule.setResource("api-resource");
rule.setParamIdx(0); // 第一个参数
rule.setCount(100); // 每个参数值 QPS 阈值
rules.add(rule);
ParamFlowRuleManager.loadRules(rules);
}
4.6 集群限流配置
// 客户端配置
@Configuration
public class SentinelClusterConfig {
@PostConstruct
public void initClusterClient() {
ClusterClientConfig config = new ClusterClientConfig();
config.setRequestTimeout(2000);
ClusterClientConfigManager.applyNewConfig(config);
// 配置 Token Server 地址
ClusterClientAssignManager.assignAddress("10.0.0.100:8719");
}
}
⚠️ 注意:集群限流需要部署独立的 Token Server,增加运维复杂度。
4.7 优缺点
| 优点 | 缺点 |
|---|---|
| 强大的控制台可视化 | 引入额外依赖和复杂度 |
| 支持集群限流 | 集群模式需部署 Token Server |
| 热点参数限流 | 学习成本较高 |
| 熔断降级一体化 | - |
五、方案三:Nginx limit_req
5.1 核心特性
- 算法:漏桶(Leaky Bucket)
- 模式:网关层限流
- 分布式支持:天然支持(多 Nginx 实例需共享状态)
- 代码侵入:零侵入
5.2 基本配置
http {
# 定义限流区域
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=1000r/s;
server {
listen 80;
location /api/ {
# 应用限流规则
limit_req zone=mylimit;
# 转发到后端
proxy_pass http://backend;
}
}
}
5.3 关键参数详解
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=1000r/s;
# 参数说明:
# $binary_remote_addr:客户端 IP(二进制格式,节省内存)
# zone=mylimit:10m:区域名称,内存大小 10MB
# rate=1000r/s:限流速率 1000 请求/秒
location /api/ {
# burst=200:允许 200 个突发请求排队
# nodelay:突发请求立即处理,不排队等待
limit_req zone=mylimit burst=200 nodelay;
}
| 参数 | 作用 |
|---|---|
burst=N | 允许 N 个请求在队列中等待 |
nodelay | 队列中的请求立即处理,不延迟 |
zone | 指定限流区域 |
5.4 自定义限流响应
limit_req_status 429;
location /api/ {
limit_req zone=mylimit;
error_page 429 = @rate_limit;
}
location @rate_limit {
return 429 '{"code": 429, "message": "请求过于频繁"}';
}
5.5 基于路径的细粒度限流
http {
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=1000r/s;
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=100r/s;
limit_req_zone $binary_remote_addr zone=search_limit:10m rate=500r/s;
server {
location /api/ {
limit_req zone=api_limit;
proxy_pass http://backend;
}
location /api/login {
limit_req zone=login_limit;
proxy_pass http://backend;
}
location /api/search {
limit_req zone=search_limit;
proxy_pass http://backend;
}
}
}
5.6 分布式限流方案
⚠️ 注意:标准 Nginx 的
limit_req是单机的,多实例部署时每个 Nginx 独立计数,无法实现全局限流。
方案一:使用 OpenResty + Redis
OpenResty 提供了 resty.limit.req 模块,可以基于 Redis 实现分布式限流:
-- lua/rate_limit.lua
local limit_req = require "resty.limit.req"
-- 使用 Redis 作为共享存储
local lim, err = limit_req.new("my_limit_store", 1000, 200)
if not lim then
ngx.log(ngx.ERR, "failed to instantiate limit.req: ", err)
return ngx.exit(500)
end
local key = ngx.var.binary_remote_addr
local delay, err = lim:incoming(key, true)
if not delay then
if err == "rejected" then
return ngx.exit(429)
end
ngx.log(ngx.ERR, "failed to limit req: ", err)
return ngx.exit(500)
end
if delay >= 0.001 then
ngx.sleep(delay)
end
http {
lua_shared_dict my_limit_store 10m;
server {
location /api/ {
access_by_lua_file lua/rate_limit.lua;
proxy_pass http://backend;
}
}
}
方案二:使用 Redis + Lua 脚本
另一种方式是直接在 Nginx 中调用 Redis Lua 脚本实现分布式限流,详见第八节。
5.7 优缺点
| 优点 | 缺点 |
|---|---|
| 零代码侵入 | 细粒度控制弱 |
| 性能极高 | 不支持热点参数限流 |
| 配置灵活 | 分布式限流需额外模块 |
| 网关层统一管理 | 无应用级监控 |
六、压测数据全公开
6.1 测试环境
| 项目 | 配置 |
|---|---|
| 硬件 | 4 核 CPU / 8GB 内存 |
| JMeter | 500 并发线程 |
| 目标 QPS | 1000 |
| 测试时长 | 60 秒 |
| 后端服务 | Spring Boot 应用 |
6.2 独立方案压测结果
| 指标 | Guava RateLimiter | Sentinel | Nginx |
|---|---|---|---|
| 实际 QPS | 998 | 995 | 992 |
| P99 响应时间 | 2ms | 5ms | 1ms |
| P999 响应时间 | 5ms | 12ms | 3ms |
| CPU 使用率 | 30% | 45% | 15% |
| 内存占用 | +50MB | +150MB | +20MB |
| 拒绝率 | 0.2% | 0.5% | 0.8% |
6.3 分层限流压测结果
架构:JMeter → Nginx → Spring Boot (Sentinel)
测试结果:
┌─────────────────────────────────────────────────────┐
│ 指标 │ 值 │
├─────────────────────────────────────────────────────┤
│ 实际 QPS │ 996 │
│ P99 响应时间 │ 3ms │
│ P999 响应时间 │ 8ms │
│ Nginx CPU 使用率 │ 10% │
│ 应用 CPU 使用率 │ 35% │
│ 拒绝率 │ 0.4% │
└─────────────────────────────────────────────────────┘
流量分布:
┌─────────────────────────────────────────────────────┐
│ 入口流量 │ 500 并发 │
│ Nginx 转发到应用 │ 996 QPS │
│ Nginx 直接拒绝 │ 4 QPS (0.4%) │
│ 应用层 Sentinel 拒绝 │ 0 │
└─────────────────────────────────────────────────────┘
6.4 突发场景测试
测试场景:前 10 秒 500 并发,第 11 秒突增至 1000 并发
结果对比:
┌─────────────────────────────────────────────────────┐
│ 方案 │ 突发处理方式 │
├─────────────────────────────────────────────────────┤
│ Guava RateLimiter │ 允许突发(令牌桶累积) │
│ │ 峰值 QPS: 1500 │
├─────────────────────────────────────────────────────┤
│ Sentinel │ 平滑过渡(滑动窗口) │
│ │ 峰值 QPS: 1100 │
├─────────────────────────────────────────────────────┤
│ Nginx │ 拒绝超出部分 │
│ │ 峰值 QPS: 1000 │
│ │ 拒绝率: 33% │
└─────────────────────────────────────────────────────┘
七、方案对比总结
7.1 核心对比
| 维度 | Guava RateLimiter | Sentinel | Nginx |
|---|---|---|---|
| 算法 | 令牌桶 | 滑动窗口 | 漏桶 |
| 分布式 | ❌ | ✅(需集群) | ⚠️(需额外模块) |
| 可视化 | ❌ | ✅ | ❌ |
| 热点参数 | ❌ | ✅ | ❌ |
| 代码侵入 | 中 | 中 | 无 |
| 运维复杂度 | 低 | 高 | 中 |
| 性能开销 | 低 | 中 | 极低 |
| 重启影响 | 计数清零 | 规则持久化 | 无 |
7.2 推荐决策树
┌──────────────────────┐
│ 限流需求分析 │
└──────────┬───────────┘
│
┌────────────────┼────────────────┐
│ │ │
┌─────────▼─────────┐ ┌────▼─────┐ ┌───────▼───────┐
│ 单机应用/简单场景 │ │ 分布式系统 │ │ 微服务架构 │
│ 无复杂需求 │ │ 需要统一控制 │ │ 需要细粒度 │
└─────────┬─────────┘ └────┬─────┘ └───────┬───────┘
│ │ │
┌─────────▼─────────┐ ┌────▼─────┐ ┌───────▼───────┐
│ Guava RateLimiter │ │ Sentinel │ │ Nginx + │
│ │ │ (集群模式)│ │ Sentinel │
└───────────────────┘ └──────────┘ └───────────────┘
7.3 中小团队最佳实践
架构:
┌─────────────────────────────────────────────────────┐
│ JMeter / 用户 │
│ ↓ │
│ Nginx 网关 │
│ ┌─────────┴─────────┐ │
│ ↓ ↓ │
│ 全局限流 路径级限流 │
│ rate=5000r/s rate=1000r/s │
│ └─────────┬─────────┘ │
│ ↓ │
│ Spring Boot 应用 │
│ ┌─────────┴─────────┐ │
│ ↓ ↓ │
│ Sentinel Sentinel │
│ 核心接口限流 热点参数限流 │
│ rate=500r/s rate=50r/s │
└─────────────────────────────────────────────────────┘
配置策略:
┌─────────────────────────────────────────────────────┐
│ Nginx 层:粗粒度限流(全局 + 路径级) │
│ Sentinel 层:细粒度限流(接口级 + 参数级) │
│ Guava 层:局部限流(内部方法级) │
└─────────────────────────────────────────────────────┘
八、额外方案:Redis + Lua
8.1 为什么需要这个方案
当你需要分布式限流但又不想引入 Sentinel 的复杂度时,Redis + Lua 是一个轻量的选择。
8.2 实现原理
-- 限流脚本
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local current = redis.call('get', key)
if current and tonumber(current) >= limit then
return 0
end
current = redis.call('incr', key)
if tonumber(current) == 1 then
redis.call('expire', key, window)
end
return 1
8.3 Java 调用示例
@Service
public class RedisRateLimiter {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String LUA_SCRIPT =
"local key = KEYS[1] local limit = tonumber(ARGV[1]) " +
"local window = tonumber(ARGV[2]) local current = redis.call('get', key) " +
"if current and tonumber(current) >= limit then return 0 end " +
"current = redis.call('incr', key) if tonumber(current) == 1 then " +
"redis.call('expire', key, window) end return 1";
private final DefaultRedisScript<Long> script = new DefaultRedisScript<>();
@PostConstruct
public void init() {
script.setScriptText(LUA_SCRIPT);
script.setResultType(Long.class);
}
public boolean tryAcquire(String key, int limit, int windowSeconds) {
Long result = redisTemplate.execute(
script,
Collections.singletonList(key),
String.valueOf(limit),
String.valueOf(windowSeconds)
);
return result != null && result == 1;
}
}
8.4 优缺点
| 优点 | 缺点 |
|---|---|
| 轻量级,无额外依赖 | 无可视化控制台 |
| 天然分布式 | 需要自己实现熔断降级 |
| 灵活可控 | 需要维护 Lua 脚本 |
九、总结
9.1 方案选择指南
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 单机应用 | Guava RateLimiter | 轻量、简单 |
| 分布式系统 | Sentinel 集群模式 | 可视化、功能全面 |
| 微服务架构 | Nginx + Sentinel | 分层防护、各司其职 |
| 不想引入重依赖 | Redis + Lua | 轻量、分布式 |
9.2 中小团队最佳实践
推荐组合:Nginx 网关层 + Sentinel 核心接口精控
理由:
1. Nginx 承担大部分限流压力,性能极高
2. Sentinel 负责核心接口的精细化控制
3. 运维成本可控,不需要部署复杂的集群
4. 可以逐步升级:先 Nginx,再 Sentinel,最后考虑集群
9.3 核心原则
- 分层限流:网关层粗粒度,应用层细粒度
- 算法选择:令牌桶适合允许突发,漏桶适合平滑流量,滑动窗口适合精确控制
- 监控优先:限流必须有监控和告警
- 优雅降级:拒绝请求时返回友好提示
💡 互动话题:你在项目中使用哪种限流方案?有没有遇到过什么坑?欢迎在评论区分享你的经验!
