一、为什么网关层需要限流?
在微服务架构中,网关是所有流量的入口。如果没有限流保护,一个突发流量高峰就可能导致下游服务雪崩。
先看一个真实案例:
某电商平台大促期间,由于未在网关层做限流,秒杀接口的流量在1秒内激增了100倍,直接打垮了商品服务和订单服务,最终导致30分钟的服务不可用。
网关限流的核心价值:
| 价值 | 说明 |
|---|---|
| 保护下游服务 | 防止突发流量冲垮业务服务 |
| 资源隔离 | 保障核心API的可用性,牺牲非核心API |
| 成本控制 | 云环境下,限流能有效控制资源消耗成本 |
| 防刷治理 | 拦截恶意刷接口的请求 |
二、限流算法选型:为什么选择令牌桶?
在实现限流之前,先了解主流的限流算法及其优缺点:
2.1 计数器算法(固定窗口)
text
时间窗口(1s) 时间窗口(1s) 时间窗口(1s)
│ 请求100 │ │ 请求100 │ │ 请求100 │
└─────────┘ └─────────┘ └─────────┘
最简单的实现,统计每个时间窗口内的请求数。
❌ 问题:存在"突刺现象"。窗口切换瞬间,可能涌入2倍阈值的请求(窗口末尾100个 + 窗口开头100个 = 200个)。
2.2 滑动窗口算法
text
时间轴 → ┌──┬──┬──┬──┬──┐
│10│10│10│10│10│ ← 每个小格代表一个子窗口
└──┴──┴──┴──┴──┘
←──── 滑动窗口 ────→
将固定窗口切分为多个子窗口,随着时间滑动,统计窗口内的总请求数。
✅ 解决了固定窗口的突刺问题,但实现复杂度稍高,且需要存储每个子窗口的计数。
2.3 令牌桶算法(重点)
text
┌──────────────┐
请求 ──▶ │ 桶容量 = 20 │ ──▶ 放行(有令牌时)
│ 速率 = 10/s │ ──▶ 拒绝(无令牌时)
└──────────────┘
▲
│ 以固定速率(10/s)生成令牌
这是Spring Cloud Gateway RequestRateLimiter 采用的算法,也是生产环境最推荐的方式。
核心参数:
replenishRate:令牌生成速率,即允许的每秒请求数(QPS)burstCapacity:桶容量,允许的突发请求数
为什么选择令牌桶?
✅ 允许合理突发(桶内积累的令牌可应对短时流量洪峰)
✅ 平滑限流,不会出现"一刀切"的拒绝
✅ 适合互联网业务的流量特征(有波峰波谷)
三、方案一:Redis + RequestRateLimiter(最常用)
这是Spring Cloud Gateway官方提供的限流方案,基于Redis实现分布式限流,适合网关集群部署的场景。
3.1 整体架构
text
┌─────────────────────────────────────────────────────┐
│ Spring Cloud Gateway │
│ ┌───────────┐ ┌──────────────┐ ┌──────────┐ │
│ │ 请求进入 │───▶│ KeyResolver │───▶│ 限流判断 │ │
│ └───────────┘ │ (确定限流维度)│ │ 令牌桶 │ │
│ └──────────────┘ └────┬─────┘ │
└─────────────────────────────────────────────┼───────┘
│
▼
┌─────────────────┐
│ Redis集群 │
│ (存储令牌计数) │
└─────────────────┘
3.2 完整实现步骤
Step 1:添加依赖
xml
<!-- gateway 核心依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- Redis 响应式驱动(必需) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
Step 2:配置Redis连接
yml
spring:
redis:
cluster:
nodes:
- 192.168.1.101:6379
- 192.168.1.102:6379
- 192.168.1.103:6379
password: your_password
timeout: 3000ms
lettuce:
pool:
max-active: 100
max-idle: 20
min-idle: 5
Step 3:定义限流维度(KeyResolver)
这是限流策略的核心,决定了"按什么来限流"。
java
@Configuration
public class KeyResolverConfig {
/**
* 方案一:按客户端IP限流
* 适合:开放API、防刷场景
*/
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> {
String ip = exchange.getRequest().getRemoteAddress()
.orElseThrow(() -> new RuntimeException("无法获取IP"))
.getAddress().getHostAddress();
return Mono.just("ip:" + ip);
};
}
/**
* 方案二:按用户ID限流
* 适合:需要区分用户等级的付费API
*/
@Bean
public KeyResolver userKeyResolver() {
return exchange -> {
// 从请求Header中获取用户Token
String token = exchange.getRequest().getHeaders()
.getFirst("Authorization");
String userId = extractUserIdFromToken(token);
return Mono.just("user:" + userId);
};
}
/**
* 方案三:按API路径限流
* 适合:不同接口不同限流阈值
*/
@Bean
public KeyResolver pathKeyResolver() {
return exchange -> {
String path = exchange.getRequest().getURI().getPath();
return Mono.just("path:" + path);
};
}
/**
* 方案四:组合维度(推荐)
* 格式:IP + API路径,实现更精细的限流
*/
@Bean
public KeyResolver compositeKeyResolver() {
return exchange -> {
String ip = exchange.getRequest().getRemoteAddress()
.orElseThrow(() -> new RuntimeException("无法获取IP"))
.getAddress().getHostAddress();
String path = exchange.getRequest().getURI().getPath();
// 组合Key,例如:192.168.1.100:/api/order
return Mono.just("composite:" + ip + ":" + path);
};
}
}
Step 4:配置路由限流规则
yml
spring:
cloud:
gateway:
routes:
# 核心业务接口:限流宽松
- id: core-service
uri: lb://core-service
predicates:
- Path=/api/order/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 100 # 每秒100个请求
redis-rate-limiter.burstCapacity: 200 # 突发容量200
key-resolver: "#{@compositeKeyResolver}"
# 非核心接口:限流严格
- id: non-core-service
uri: lb://non-core-service
predicates:
- Path=/api/log/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10 # 每秒10个请求
redis-rate-limiter.burstCapacity: 20
key-resolver: "#{@ipKeyResolver}"
3.3 源码深度解析
RequestRateLimiter过滤器的工作原理:
java
// Gateway内部核心逻辑(简化版)
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1. 通过KeyResolver获取限流Key
Mono<String> keyMono = keyResolver.resolve(exchange);
// 2. 调用Redis Lua脚本进行令牌桶判断
return keyMono.flatMap(key -> {
// 执行的Lua脚本逻辑:
// - 获取当前时间窗口
// - 计算已使用的令牌数
// - 判断是否还有令牌
// - 返回是否放行
return redisTemplate.execute(script, keys, args);
}).flatMap(response -> {
if (response.isAllowed()) {
return chain.filter(exchange); // 放行
} else {
return onError(exchange); // 限流拒绝
}
});
}
核心Lua脚本逻辑(Redis中执行的原子操作):
lua
-- 令牌桶算法在Redis中的实现(简化)
local tokens_key = KEYS[1] -- 剩余令牌数
local timestamp_key = KEYS[2] -- 上次更新时间
local rate = tonumber(ARGV[1]) -- 令牌生成速率
local capacity = tonumber(ARGV[2]) -- 桶容量
local now = tonumber(ARGV[3]) -- 当前时间
-- 计算应该新增的令牌数
local last_tokens = tonumber(redis.call('get', tokens_key) or capacity)
local last_refreshed = tonumber(redis.call('get', timestamp_key) or now)
local delta = math.max(0, now - last_refreshed)
local filled_tokens = math.min(capacity, last_tokens + (delta * rate / 1000))
-- 判断是否有足够令牌
local allowed = filled_tokens >= 1
local new_tokens = filled_tokens
if allowed then
new_tokens = filled_tokens - 1
end
-- 更新Redis
redis.call('setex', tokens_key, 10, new_tokens)
redis.call('setex', timestamp_key, 10, now)
return { allowed, new_tokens }
💡 设计要点 :整个判断过程在Redis中通过Lua脚本完成,保证了原子性,避免了分布式环境下的并发问题。
四、方案二:集成 Sentinel(动态限流)
当限流规则需要频繁调整 、需要可视化监控 、或需要熔断降级时,Sentinel是更好的选择。
4.1 Sentinel vs Redis限流对比
| 对比维度 | Redis RequestRateLimiter | Sentinel |
|---|---|---|
| 规则变更 | 修改配置重启网关 | 控制台动态推送,实时生效 |
| 限流算法 | 令牌桶 | 多种(令牌桶/漏桶/滑动窗口) |
| 熔断降级 | ❌ 不支持 | ✅ 支持(慢调用/异常比例) |
| 监控面板 | ❌ 无 | ✅ 丰富的Dashboard |
| 系统自适应保护 | ❌ 不支持 | ✅ 支持(根据CPU/内存自适应) |
| 学习成本 | 低 | 中 |
4.2 集成步骤
Step 1:添加依赖
xml
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
Step 2:配置Sentinel控制台
yml
spring:
cloud:
sentinel:
transport:
dashboard: 192.168.1.100:8080 # Sentinel控制台地址
port: 8719 # 与控制台通信的端口
eager: true # 提前初始化,懒加载可能导致首次请求异常
Step 3:在Sentinel控制台配置限流规则
访问 http://192.168.1.100:8080,在"网关流控"页面配置:
text
资源名: /api/order/**
阈值类型: QPS
单机阈值: 100
流控效果: 快速失败
Step 4:定制限流响应
java
@Component
public class SentinelGatewayConfig {
@PostConstruct
public void init() {
// 自定义限流返回结果
GatewayCallbackManager.setBlockHandler((serverWebExchange, throwable) -> {
ServerHttpResponse response = serverWebExchange.getResponse();
response.setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
// 返回JSON格式错误信息
Map<String, Object> result = new HashMap<>();
result.put("code", 429);
result.put("msg", "请求过于频繁,请稍后重试");
result.put("timestamp", System.currentTimeMillis());
DataBuffer buffer = response.bufferFactory()
.wrap(JSON.toJSONString(result).getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(buffer));
});
}
}
4.3 高级特性:熔断降级
Sentinel不仅能限流,还能做熔断降级。当某个API错误率过高时,自动熔断保护下游。
在控制台配置熔断规则:
text
资源名: /api/order/**
熔断策略: 慢调用比例
最大RT: 200ms
比例阈值: 0.5
熔断时长: 10s
最小请求数: 5
含义:当5个请求中有50%的响应时间超过200ms时,触发熔断,后续请求直接走降级逻辑,10秒后恢复探测。
五、方案三:自定义限流过滤器(灵活定制)
如果官方方案无法满足业务需求(比如需要自定义限流算法、需要从数据库读取规则等),可以自己实现限流过滤器。
5.1 实现一个自定义限流过滤器
java
@Component
@Slf4j
public class CustomRateLimiterFilter implements GlobalFilter, Ordered {
@Autowired
private StringRedisTemplate redisTemplate;
// 使用滑动窗口实现限流
private static final String LUA_SCRIPT =
"local key = KEYS[1] " +
"local window = tonumber(ARGV[1]) " +
"local limit = tonumber(ARGV[2]) " +
"local now = tonumber(ARGV[3]) " +
"local window_start = now - window * 1000 " +
"redis.call('ZREMRANGEBYSCORE', key, 0, window_start) " +
"local current = redis.call('ZCARD', key) " +
"if current < limit then " +
" redis.call('ZADD', key, now, now .. '_' .. math.random()) " +
" redis.call('EXPIRE', key, window + 1) " +
" return 1 " +
"else " +
" return 0 " +
"end";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1. 获取限流Key
String ip = exchange.getRequest().getRemoteAddress()
.orElseThrow(() -> new RuntimeException("无法获取IP"))
.getAddress().getHostAddress();
String key = "ratelimit:custom:" + ip;
// 2. 执行限流判断(滑动窗口)
Long windowSeconds = 1L; // 1秒窗口
Long maxRequests = 10L; // 最多10个请求
Long result = redisTemplate.execute(
new DefaultRedisScript<>(LUA_SCRIPT, Long.class),
Collections.singletonList(key),
windowSeconds.toString(),
maxRequests.toString(),
String.valueOf(System.currentTimeMillis())
);
// 3. 判断是否放行
if (result == null || result == 0) {
// 限流拒绝
log.warn("请求被限流,IP: {}", ip);
exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
return exchange.getResponse().setComplete();
}
// 4. 放行请求
return chain.filter(exchange);
}
@Override
public int getOrder() {
// 确保在业务路由之前执行
return -1;
}
}
5.2 动态加载限流配置(从配置中心)
java
@Component
@RefreshScope // 支持Spring Cloud Config动态刷新
public class DynamicRateLimiterConfig {
@Value("${rate-limiter.default.qps:100}")
private Integer defaultQps;
// 也可以从数据库或Nacos读取更细致的配置
public Integer getQpsForPath(String path) {
// 从缓存或数据库获取该路径的限流阈值
return defaultQps;
}
}
六、生产环境最佳实践
6.1 限流配置建议
| 场景 | replenishRate | burstCapacity | 说明 |
|---|---|---|---|
| 核心写接口(下单/支付) | 50 | 100 | 严格限流,保护数据一致性 |
| 核心读接口(商品详情) | 500 | 800 | 适当放宽,允许高并发读 |
| 非核心接口(日志上报) | 20 | 30 | 严格限流,避免浪费资源 |
| 开放API(无需鉴权) | 10 | 20 | 最严格,防止恶意刷接口 |
6.2 限流响应处理
被限流的请求,应该返回明确的错误信息,而不是让用户感到困惑。
java
@Component
public class RateLimiterErrorHandler {
@Bean
public GlobalFilter rateLimiterErrorFilter() {
return (exchange, chain) -> {
// 检查是否被限流标记
if (exchange.getResponse().getStatusCode() == HttpStatus.TOO_MANY_REQUESTS) {
exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
Map<String, Object> error = new HashMap<>();
error.put("code", 429);
error.put("message", "请求太频繁,请休息一下再试");
error.put("retryAfter", 1); // 建议1秒后重试
String body = JSON.toJSONString(error);
DataBuffer buffer = exchange.getResponse().bufferFactory()
.wrap(body.getBytes(StandardCharsets.UTF_8));
return exchange.getResponse().writeWith(Mono.just(buffer));
}
return chain.filter(exchange);
};
}
}
6.3 监控告警
限流不是为了"拒绝用户",而是为了让系统更稳定。因此需要建立监控:
java
@Component
public class RateLimiterMetrics {
@Autowired
private MeterRegistry meterRegistry;
private Counter rateLimitCounter;
@PostConstruct
public void init() {
rateLimitCounter = Counter.builder("gateway.ratelimit.rejected")
.description("被限流拒绝的请求数")
.register(meterRegistry);
}
public void recordReject(String key, String path) {
rateLimitCounter.increment();
// 按路径维度记录
Counter.builder("gateway.ratelimit.rejected.bypath")
.tag("path", path)
.register(meterRegistry)
.increment();
}
}
告警规则建议:
- 限流拒绝率 > 5% → 告警,可能业务突增
- 限流拒绝率 > 20% → 严重告警,可能系统容量不足或遭受攻击
6.4 灰度发布时的注意事项
灰度期间,新旧版本流量分配不均,可能导致限流阈值不准确。
解决方案:
yml
# 对灰度流量使用独立的限流Key
spring:
cloud:
gateway:
routes:
- id: gray-service
uri: lb://gray-service
predicates:
- Path=/api/**
- Header=X-Gray-Tag, gray # 灰度流量标记
filters:
- name: RequestRateLimiter
args:
# 灰度流量单独限流,互不影响
key-resolver: "#{@grayKeyResolver}"
redis-rate-limiter.replenishRate: 20
redis-rate-limiter.burstCapacity: 40
七、三种方案对比总结
| 维度 | Redis RequestRateLimiter | Sentinel | 自定义过滤器 |
|---|---|---|---|
| 实现难度 | ⭐⭐ 简单 | ⭐⭐⭐ 中等 | ⭐⭐⭐⭐ 复杂 |
| 分布式支持 | ✅ 原生支持 | ✅ 原生支持 | ❌ 需要自己实现 |
| 动态规则 | ❌ 需要重启 | ✅ 实时生效 | ✅ 需要自己实现 |
| 熔断降级 | ❌ 不支持 | ✅ 完善支持 | ❌ 需要自己实现 |
| 监控能力 | ❌ 较弱 | ✅ 强大的Dashboard | ❌ 需要自己实现 |
| 生态集成 | Spring Cloud原生 | 阿里系生态 | 无 |
| 推荐场景 | 大部分通用场景 | 需要动态规则/熔断能力 | 极端定制需求 |
个人推荐:
- 新项目快速上线 → 直接用 Redis RequestRateLimiter,够用且简单
- 已有Sentinel基础设施 → 使用Sentinel,充分利用其治理能力
- 特殊业务需求(如按时间段调整阈值)→ 自定义过滤器 + 配置中心
八、总结
网关限流是微服务架构的第一道安全防线。本文从算法原理出发,到三种落地方案,再到生产实践细节,希望能帮助你构建一个高可用的限流系统。
核心要点回顾:
- 算法选型:令牌桶算法能同时兼顾限流精度和突发流量处理
- 存储选型:Redis是实现分布式限流的首选,Lua脚本保证原子性
- 维度选择:按IP、用户、API路径、组合维度,根据业务场景灵活选择
- 监控告警:限流不是终点,建立完善的监控体系才能持续优化
- 渐进式演进:从简单的Redis限流起步,复杂后逐步演进到Sentinel
限流不是目的,而是手段。最终目标是在保证系统稳定的前提下,尽可能多地服务合法用户。