Gateway 限流,RequestRateLimiter 与 Redis
电商系统大促,流量是平时的 10 倍。如果不对网关限流,后端服务会被瞬时流量打垮。
Gateway 内置的限流过滤器,配合 Redis,让你轻松实现分布式限流。
为什么要限流
场景一:突发流量
正常流量: 100 QPS ───────────────────────
│
实际流量: 1000 QPS ───────
│ ← 服务扛不住
▼
服务崩溃,响应超时场景二:恶意请求
正常用户: ████████████████████
攻击者: ████████████████████████████████████████
10% 的请求占用 90% 的资源场景三:资源争抢
用户 A: 大量请求占用连接池
用户 B: 等待超时
用户 C: 页面打不开
用户 D: 愤怒投诉限流,就是给系统装上「保险丝」,保护后端服务不被瞬时流量打垮。
限流算法
1. 计数器算法
最简单但有「突刺问题」:
时间窗口: 1 分钟
限流阈值: 100 请求
00:00 - 00:30 → 60 请求
00:30 - 00:31 → 40 请求(窗口重置)
00:31 - 01:00 → 0 请求(浪费后半段容量)2. 滑动窗口算法
解决突刺问题:
将时间窗口切分为多个小窗口
[5s|5s|5s|5s|5s|5s|5s|5s|5s|5s|5s|5s] = 1 分钟
当前窗口请求数 = 最近 12 个小窗口的和3. 令牌桶算法(Gateway 采用)
允许一定程度的突发流量:
┌─────────────────────────────────────┐
│ 令牌桶 │
│ │
│ ┌───┐ │
│ │ T │ ──► 取出令牌 ──► 处理请求 │
│ │ T │ │
│ │ T │ 按固定速率放入令牌 │
│ │ │ │
│ └───┘ │
└─────────────────────────────────────┘
特点:
- 桶满时令牌溢出(不会累积)
- 允许一定突发(只要桶里有令牌)4. 漏桶算法
严格控制请求速率:
┌─────────────────────────────────────┐
│ 漏桶 │
│ │
│ 请求 ──► 进桶 ──► 按固定速率漏出 │
│ │ │
│ ▼ │
│ 超过容量则拒绝 │
└─────────────────────────────────────┘
特点:
- 严格平稳输出
- 不允许突发Gateway 限流快速开始
1. 引入依赖
xml
<dependencies>
<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>
</dependencies>2. 启用限流
yaml
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/users/**
filters:
- name: RequestRateLimiter
args:
# 每秒允许的请求数
redis-rate-limiter.replenishRate: 100
# 桶容量(最大突发流量)
redis-rate-limiter.burstCapacity: 2003. 配置 Redis
yaml
spring:
redis:
host: localhost
port: 6379
password:
database: 0
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0限流维度
基于请求路径
yaml
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 100
redis-rate-limiter.burstCapacity: 200
# 默认基于请求路径限流基于用户(通过参数指定)
yaml
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 100
redis-rate-limiter.burstCapacity: 200
key-resolver: '#{@userKeyResolver}' # 自定义 Key 解析器java
@Configuration
public class RateLimiterConfig {
// 基于用户 ID 限流
@Bean
public KeyResolver userKeyResolver() {
return exchange -> Mono.just(
exchange.getRequest().getHeaders().getFirst("X-User-Id")
);
}
}基于 IP 地址
java
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(
exchange.getRequest().getRemoteAddress()
.getAddress()
.getHostAddress()
);
}基于接口路径
java
@Bean
public KeyResolver pathKeyResolver() {
return exchange -> Mono.just(
exchange.getRequest().getPath().value()
);
}组合维度
java
@Bean
public KeyResolver userPathKeyResolver() {
return exchange -> {
String userId = exchange.getRequest().getHeaders().getFirst("X-User-Id");
String path = exchange.getRequest().getPath().value();
return Mono.just(userId + "_" + path);
};
}自定义限流器
实现 RateLimiter 接口
java
@Component
public class CustomRateLimiter implements RateLimiter<Object> {
private final RedisTemplate<String, String> redisTemplate;
public CustomRateLimiter(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public Mono<ServerWebExchange> isAllowed(String routeId, String id) {
// 1. 获取限流配置
Config config = getConfig().get(routeId);
if (config == null) {
return Mono.just(true); // 无配置,不限流
}
// 2. 生成 Redis Key
String key = "rate_limit:" + routeId + ":" + id;
// 3. 原子操作限流
return redisTemplate.execute(
new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisOperations operations) throws DataAccessException {
// Lua 脚本保证原子性
return true; // 实际实现见下方
}
}
);
}
}Lua 脚本实现
Gateway 使用 Lua 脚本实现令牌桶:
lua
-- token_bucket.lua
local key = KEYS[1] -- 限流 key
local rate = tonumber(ARGV[1]) -- 补充速率
local capacity = tonumber(ARGV[2]) -- 桶容量
local now = tonumber(ARGV[3]) -- 当前时间戳
local requested = tonumber(ARGV[4]) -- 请求数量
-- 获取当前令牌数
local last_tokens = tonumber(redis.call('get', key) or capacity)
-- 获取上次补充时间
local last_refreshed = tonumber(redis.call('get', key .. ':timestamp') or now)
-- 计算需要补充的令牌
local delta = math.max(0, (now - last_refreshed) * rate)
local tokens = math.min(capacity, last_tokens + delta)
-- 检查是否允许
if tokens >= requested then
tokens = tokens - requested
redis.call('set', key, tokens)
redis.call('set', key .. ':timestamp', now)
return {1, tokens} -- 允许
else
return {0, tokens} -- 拒绝
end不同场景的限流配置
场景一:普通接口
yaml
spring:
cloud:
gateway:
routes:
- id: normal-api
uri: lb://normal-service
predicates:
- Path=/api/normal/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 1000
redis-rate-limiter.burstCapacity: 2000场景二:敏感接口(更严格的限流)
yaml
spring:
cloud:
gateway:
routes:
- id: sensitive-api
uri: lb://sensitive-service
predicates:
- Path=/api/sensitive/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20场景三:登录接口(防暴力破解)
yaml
spring:
cloud:
gateway:
routes:
- id: login-api
uri: lb://auth-service
predicates:
- Path=/api/auth/login
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 5
redis-rate-limiter.burstCapacity: 10
key-resolver: '#{@ipKeyResolver}' # 基于 IP限流后的响应
默认行为
默认返回 429 Too Many Requests。
自定义响应
yaml
spring:
cloud:
gateway:
default-filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 100
redis-rate-limiter.burstCapacity: 200
# 自定义 Response Writerjava
@Component
public class RateLimitResponseWriter implements GatewayFilter, ErrorWebExceptionHandler {
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
if (ex instanceof RateLimitException) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
response.getHeaders().add("Content-Type", "application/json");
String body = "{\"code\":429,\"msg\":\"访问太频繁,请稍后再试\"}";
DataBuffer buffer = response.bufferFactory().wrap(body.getBytes());
return response.writeWith(Mono.just(buffer));
}
return Mono.error(ex);
}
}分布式限流 vs 本地限流
本地限流(Token Bucket)
┌────────────┐
│ Gateway 1 │ ← 本地限流 100 QPS
│ TokenBucket│
└────────────┘
┌────────────┐
│ Gateway 2 │ ← 本地限流 100 QPS
│ TokenBucket│
└────────────┘
总限流: 200 QPS(不符合预期)分布式限流(Redis)
┌────────────┐
│ Redis │ ← 全局限流 100 QPS
│ TokenBucket│
└────────────┘
▲
│
┌─────┴─────┐
│ Gateway 1 │
└───────────┘
▲
│
┌─────┴─────┐
│ Gateway 2 │
└───────────┘
总限流: 100 QPS(符合预期)Gateway 的 Redis 限流是分布式限流,多个 Gateway 实例共享一个限流计数器。
面试高频问题
Q:Gateway 限流用的什么算法?
A:Gateway 使用令牌桶算法,通过 Redis + Lua 脚本实现。令牌桶允许一定程度的突发流量,同时保证平均速率不超过设定值。
Q:限流 Key 怎么确定?
A:通过 KeyResolver 接口自定义。常见的 Key 包括:用户 ID、IP 地址、请求路径、服务名等。可以组合多个维度。
Q:replenishRate 和 burstCapacity 有什么区别?
A:replenishRate 是令牌补充速率(每秒),burstCapacity 是桶容量。burstCapacity 必须 >= replenishRate。burstCapacity 决定了允许的最大突发流量。
Q:单机限流和分布式限流有什么区别?
A:单机限流每个 Gateway 实例独立限流,多实例总流量可能超出预期。分布式限流使用 Redis 作为共享计数器,保证全局限流。Gateway + Redis 是分布式限流。
总结
Gateway 限流提供了完整的限流能力:
- 令牌桶算法:允许突发流量,平稳处理请求
- Redis 存储:分布式限流,多实例共享限流额度
- 多维度 Key:支持用户、IP、路径等多种限流维度
- Lua 脚本:保证限流检查的原子性
限流是保护系统的高考手段。配合熔断降级,可以让系统在极端流量下依然保持可用。
