跳到主要内容

分布式限流

问题

分布式系统中如何实现限流?常见的限流算法有哪些?如何在网关层和应用层做限流?

答案

为什么需要限流

限流是保护系统的重要手段,防止突发流量(恶意攻击、秒杀场景、下游故障)压垮服务。

四种限流算法

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 RateLimiterSentinel 都使用令牌桶。

算法对比

算法突发流量平滑度实现复杂度适用场景
固定窗口有临界问题简单简单限流
滑动窗口较好中等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: 限流后请求怎么处理?

答案

  1. 直接拒绝:返回 HTTP 429 Too Many Requests
  2. 排队等待:将请求放入队列,匀速处理(漏桶思想)
  3. 降级:返回兜底数据或缓存数据
  4. 重试引导:返回 Retry-After Header,告知客户端何时重试

相关链接