限流(Rate Limiting)的核心目标是在流量超过系统承载能力时拒绝或延迟部分请求,以保护后端服务不被打垮。但同样一个限流阈值,在不同算法和组件中表现差异很大:有的算法在窗口边界会瞬时放行两倍流量,有的配置会在突发流量下误伤正常用户。理解这些差异,是做好限流配置的前提。

一、限流算法选型:从固定窗口到令牌桶
最简单的限流方式是固定窗口计数器。例如把一分钟作为一个窗口,每个窗口内最多允许100次请求。这种实现非常轻量,用Redis的INCR配合EXPIRE即可完成。但固定窗口有一个明显缺陷:如果在第59秒和第61秒各涌入100个请求,虽然每个窗口都没有超限,但在一秒内实际放行了接近200个请求,后端可能瞬间过载。这个现象被称为窗口边界突发。
滑动窗口对固定窗口做了改进。它不按自然时间切分窗口,而是始终检查当前时间往前推一个窗口长度内的请求数。比如限制最近60秒最多100次请求,就需要记录每次请求的时间戳,并清理过期记录。滑动窗口更平滑,但存储和计算成本更高。在请求量很大时,使用Sorted Set维护时间戳会占用较多内存。
令牌桶是另一类常用算法。系统以固定速率向桶中放入令牌,桶有最大容量。请求到达时需要先获取令牌,获取不到则拒绝或等待。令牌桶允许一定程度的突发流量:如果桶已经积累了大量令牌,短时间内的突发请求可以一次性消耗这些令牌,但长期平均速率仍受补充速率控制。漏桶则相反,它强制请求以固定速率流出,突发请求会被缓存或丢弃,输出速率恒定。对API网关来说,令牌桶比漏桶更灵活,因为后端服务通常能够承受一定程度的短时突发。
import com.google.common.util.concurrent.RateLimiter;
public class TokenBucketDemo {
public static void main(String[] args) {
RateLimiter limiter = RateLimiter.create(50.0);
for (int i = 0; i < 200; i++) {
if (limiter.tryAcquire(100, java.util.concurrent.TimeUnit.MILLISECONDS)) {
System.out.println("放行请求 " + i);
} else {
System.out.println("拒绝请求 " + i);
}
}
}
}
上面的示例创建了一个每秒补充50个令牌的限流器,并尝试在100毫秒内获取令牌。这种配置适合对突发请求有一定容忍度的场景。如果希望更严格控制峰值,可以将超时时间调得更短,甚至直接使用acquire阻塞等待。
二、主流组件中的限流配置实例
Nginx的ngx_http_limit_req_module模块经常用于入口层限流。它基于漏桶算法实现,其中rate定义平均速率,burst定义允许的突发排队容量。配置中如果没有nodelay,超出rate但未超过burst的请求会被延迟处理;加上nodelay后,这些突发请求会立即放行,但超出burst的部分会被拒绝。
http {
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=50r/s;
server {
location /api/ {
limit_req zone=api_limit burst=100 nodelay;
proxy_pass http://backend;
}
}
}
上面的配置以客户端IP为维度限制平均每秒50个请求,突发队列容量为100。burst设成0时,所有超过速率的请求都会被拒绝。如果去掉nodelay,则100个突发请求会以每秒50个的速率排队处理,客户端可能因为排队时间过长而超时。需要根据接口延迟容忍度来决定是否保留nodelay。
Spring Cloud Gateway提供了RequestRateLimiter过滤器,底层基于Redis执行令牌桶限流。它需要配置redis-rate-limiter.replenishRate和burstCapacity,并指定KeyResolver来定义限流维度。下面是一个按IP限流的YAML配置。
spring:
cloud:
gateway:
routes:
- id: api_route
uri: http://backend-service
predicates:
- Path=/api/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
key-resolver: "#{@ipKeyResolver}"
replenishRate表示每秒向令牌桶中补充的令牌数,burstCapacity表示桶的最大容量。如果实际流量经常超过replenishRate但低于burstCapacity,说明配置中留有过大的突发空间,需要适当降低burstCapacity。反之,如果大量请求被拒绝,则要确认是正常峰值还是攻击流量。
如果不想依赖网关,也可以在应用内使用Redis和Lua实现滑动窗口限流。下面这段Lua脚本通过Sorted Set记录请求时间戳,并原子地判断当前窗口内请求数是否超限。
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
local count = redis.call('ZCARD', key)
if count < limit then
redis.call('ZADD', key, now, now .. '-' .. math.random())
redis.call('PEXPIRE', key, window)
return 1
else
return 0
end
这种方案可以精确控制最近一段时间内的请求总量,返回1表示放行,返回0表示拒绝。但由于每个请求都要向Redis写入时间戳,在高并发场景下会带来明显的网络和内存开销。针对热点接口,可以在本地进程内先做粗粒度限流,再进入Redis做精确校验。
三、限流参数调优与误伤排查
限流配置上线后最常见的问题是误伤正常请求。一个典型的排查方向是观察拒绝率与业务指标的关系。例如Nginx返回503的比例突然升高,但后端CPU和内存并没有达到瓶颈,这时很可能是限流阈值设置过低,或者限流维度选择不当。如果按IP限流,公司内部出口IP或运营商NAT可能会把大量用户聚合成一个IP,导致个别IP被错误限制。
限流配置Rate Limiting令牌桶算法修改时间:2026-08-27 00:34:13