分布式限流
问题
分布式系统中如何实现限流?常见的限流算法有哪些?如何在网关层和应用层做限流?
答案
为什么需要限流
限流是保护系统的重要手段,防止突发流量(恶意攻击、秒杀场景、下游故障)压垮服务。
四种限流算法
1. 固定窗口计数器
将时间划分为固定窗口(如 1 秒),每个窗口内计数,超过阈值则拒绝。
| 窗口 1 (0~1s) | 窗口 2 (1~2s) |
| count: 100/100 | count: 50/100 |
缺点:临界问题。窗口 1 尾部 100 请求 + 窗口 2 头部 100 请求 = 1 秒内 200 请求(阈值翻倍)。
2. 滑动窗口计数器
将窗口细分为多个小格子,滑动计算总数,解决临界问题。
Redis 滑动窗口限流
public boolean isAllowed(String key, int maxCount, int windowSeconds) {
long now = System.currentTimeMillis();
long windowStart = now - windowSeconds * 1000L;
String member = now + ":" + UUID.randomUUID();
// Lua 脚本保证原子性
String script = """
redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, ARGV[1])
redis.call('ZADD', KEYS[1], ARGV[2], ARGV[3])
local count = redis.call('ZCARD', KEYS[1])
redis.call('EXPIRE', KEYS[1], ARGV[4])
return count
""";
Long count = redis.execute(new DefaultRedisScript<>(script, Long.class),
List.of(key),
String.valueOf(windowStart), String.valueOf(now), member,
String.valueOf(windowSeconds));
return count != null && count <= maxCount;
}
3. 漏桶算法(Leaky Bucket)
请求进入桶中,以固定速率流出处理。桶满则拒绝新请求。
特点:输出速率恒定,适合平滑流量。缺点是无法应对突发流量。
4. 令牌桶算法(Token Bucket)
以固定速率向桶中放入令牌,请求需获取令牌才能处理。桶满时令牌溢出丢弃。
特点:允许一定程度的突发流量(桶中积攒的令牌)。Guava RateLimiter 和 Sentinel 都使用令牌桶。
算法对比
| 算法 | 突发流量 | 平滑度 | 实现复杂度 | 适用场景 |
|---|---|---|---|---|
| 固定窗口 | 有临界问题 | 差 | 简单 | 简单限流 |
| 滑动窗口 | 较好 | 好 | 中等 | API 限流 |
| 漏桶 | 不允许 | 最好 | 中等 | 流量整形 |
| 令牌桶 | 允许 | 好 | 中等 | 最常用 |
Guava RateLimiter
单机令牌桶限流
// 每秒生成 100 个令牌
RateLimiter rateLimiter = RateLimiter.create(100);
public void handleRequest() {
if (rateLimiter.tryAcquire(1, 500, TimeUnit.MILLISECONDS)) {
// 获取令牌成功,处理请求
doBusiness();
} else {
// 限流,返回 429
throw new RateLimitException("请求过于频繁");
}
}
Guava RateLimiter 只适用于单机
Guava RateLimiter 基于 JVM 内存,集群部署时每个实例独立限流。分布式限流需要 Redis 或 Sentinel。
Sentinel 分布式限流
Sentinel 限流规则
// 定义限流规则
FlowRule rule = new FlowRule();
rule.setResource("createOrder");
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setCount(100); // QPS 阈值
rule.setClusterMode(true); // 开启集群限流
FlowRuleManager.loadRules(Collections.singletonList(rule));
// 使用 @SentinelResource 注解
@SentinelResource(value = "createOrder",
blockHandler = "createOrderBlockHandler")
public Order createOrder(OrderDTO dto) {
return orderService.create(dto);
}
public Order createOrderBlockHandler(OrderDTO dto, BlockException e) {
throw new BusinessException("系统繁忙,请稍后再试");
}
网关层限流
Spring Cloud Gateway 限流
spring:
cloud:
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/orders/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 100 # 每秒 100 个令牌
redis-rate-limiter.burstCapacity: 200 # 桶容量 200
key-resolver: "#{@userKeyResolver}" # 按用户限流
常见面试问题
Q1: 令牌桶和漏桶的区别?
答案:
| 维度 | 令牌桶 | 漏桶 |
|---|---|---|
| 突发流量 | 允许(消耗桶中积累的令牌) | 不允许(固定速率输出) |
| 核心控制 | 令牌的生成速率 | 请求的流出速率 |
| 用途 | 限流(保护服务) | 流量整形(平滑输出) |
令牌桶更灵活,大多数限流场景优先选择。
Q2: 单机限流和分布式限流怎么选?
答案:
- 单机限流:Guava RateLimiter 或 Sentinel 本地模式。简单,但每个实例独立计算
- 分布式限流:Redis + Lua 或 Sentinel 集群模式。全局统一计数,但有网络开销
建议:网关层做分布式限流(全局流量控制),应用层做单机限流(保护本机资源)。
Q3: 限流后请求怎么处理?
答案:
- 直接拒绝:返回 HTTP 429 Too Many Requests
- 排队等待:将请求放入队列,匀速处理(漏桶思想)
- 降级:返回兜底数据或缓存数据
- 重试引导:返回
Retry-AfterHeader,告知客户端何时重试