# 什么是限流?限流算法有哪些?怎么实现的?

9 min read
Table of Contents

什么是限流?限流算法有哪些?怎么实现的?

限流面试标准答案

【核心定义】

限流是一种通过控制单位时间内系统处理请求的数量,来保护系统免受过载冲击的流量控制技术。

【关键要点】

  1. 目的与价值:防止突发流量压垮系统,保障核心服务稳定,实现平滑削峰填谷
  2. 核心指标:QPS(每秒查询数)、并发连接数、网络带宽等
  3. 应用场景:API网关、微服务接口、数据库连接池、消息队列消费等
  4. 实现层级:可在网关层、应用层、中间件层等多层次实施

【深度推导/细节】

主流限流算法实现原理

1. 计数器算法(固定窗口)

核心逻辑:

  • Step 1:设定时间窗口T(如1秒)和最大请求数N
  • Step 2:每个请求到达时,检查当前窗口计数是否超限
  • Step 3:窗口结束时重置计数器

临界问题:

  • 边界突刺:在窗口切换瞬间可能承受2N的请求冲击
  • 示例:设N=100,在0.9s-1.0s接收100请求,1.0s-1.1s又接收100请求,实际0.9s-1.1s这0.2秒内承受了200请求

2. 滑动窗口算法

优化方案:

  • 将大窗口划分为多个小时间片(如1秒分为10个100ms片)
  • Step 1:请求到达时,计算当前时间所在的小窗口
  • Step 2:统计最近N个小窗口的总请求数
  • Step 3:通过滑动统计消除固定窗口的边界问题

数据结构实现:

// 环形数组存储各时间片计数
long[] timeSlices = new long[sliceCount];
int currentIndex = (currentTime / sliceDuration) % sliceCount;

3. 漏桶算法

核心机制:

  • Step 1:请求以任意速率进入漏桶
  • Step 2:漏桶以恒定速率流出请求(如每秒10个)
  • Step 3:桶满时新请求被拒绝或排队

数据结构:

class LeakyBucket {
long capacity; // 桶容量
long rate; // 流出速率(请求/秒)
long waterLevel; // 当前水位
long lastTime; // 上次漏水时间
}

4. 令牌桶算法(最常用)

核心流程:

  • Step 1:系统以恒定速率向桶中添加令牌
  • Step 2:请求到达时从桶中获取令牌
  • Step 3:获取成功则放行,桶空则拒绝或等待

关键参数设计合理性:

  • 令牌生成速率r:决定系统平均处理能力
  • 桶容量b:允许的突发流量上限
  • 设计公式:突发流量 ≤ b + r × t(t为突发持续时间)

Guava RateLimiter实现要点:

// SmoothBursty实现(允许突发)
class SmoothRateLimiter {
double storedPermits; // 当前令牌数
double maxPermits; // 最大令牌数
long nextFreeTicketMicros; // 下次可用时间
}

【关联/对比】

算法对比矩阵

算法优点缺点适用场景
计数器实现简单,内存消耗小边界突刺问题对精度要求不高的简单场景
滑动窗口精度较高,平滑限流实现复杂,内存占用多需要精确控制的API限流
漏桶输出速率绝对恒定无法应对突发流量需要恒定输出速率的场景
令牌桶允许突发,灵活性高实现相对复杂绝大多数互联网应用场景

与熔断降级的区别

  • 限流:控制流量入口,预防过载
  • 熔断:故障时快速失败,避免雪崩
  • 降级:牺牲非核心功能,保障核心链路

【线程安全实现】

高并发下的原子操作

// 使用AtomicLong实现计数器
public class AtomicCounterLimiter {
private AtomicLong counter = new AtomicLong(0);
private long limit;
private long windowMillis;
public boolean tryAcquire() {
long currentTime = System.currentTimeMillis();
long windowKey = currentTime / windowMillis;
long oldKey = lastWindowKey.getAndSet(windowKey);
if (oldKey != windowKey) {
counter.set(0);
}
return counter.incrementAndGet() <= limit;
}
}

Redis分布式限流方案

-- Lua脚本保证原子性
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local current = redis.call('get', key) or 0
if current + 1 > limit then
return 0
else
redis.call('INCR', key)
redis.call('EXPIRE', key, 1)
return 1
end

【版本差异与演进】

Java生态发展

  • 早期:自定义实现计数器/漏桶
  • Guava 18+:RateLimiter提供生产级令牌桶
  • Spring Cloud Gateway:集成RedisRateLimiter
  • Sentinel 1.8+:支持热点参数限流、集群限流

云原生时代

  • Envoy:基于令牌桶的全局/本地限流
  • Istio:服务网格层级的全局限流
  • Kong:插件化限流策略

『面试官追问』

Q1:如何选择限流算法的具体参数?

回答要点:

  1. 压测基准:通过压测获取系统单机最大QPS
  2. 安全系数:通常取压测值的70%-80%作为限流阈值
  3. 令牌桶容量:根据业务可接受的突发持续时间计算
    • 公式:突发容量 = 平均QPS × 允许突发时间
  4. 动态调整:结合监控指标实现动态限流

Q2:分布式限流如何保证一致性?

解决方案对比:

  1. Redis中心化方案:简单但存在单点瓶颈
  2. 令牌分发方案:中心节点分发令牌,复杂度高
  3. 滑动窗口+一致性哈希:每个节点负责部分key的限流
  4. 本地限流+定期同步:折中方案,允许短暂不一致

Q3:限流后的请求如何处理?

分级处理策略:

  1. 快速失败:直接返回”系统繁忙”(默认)
  2. 排队等待:进入队列,设置超时时间
  3. 降级返回:返回缓存数据或默认值
  4. 优先级队列:VIP用户请求优先处理
  5. 预热机制:冷启动时逐步增加限流阈值

Q4:如何验证限流效果?

监控指标体系:

  1. 基础指标:限流触发次数、拒绝请求数、通过QPS
  2. 质量指标:请求平均延迟、错误率、超时率
  3. 系统指标:CPU使用率、内存占用、GC频率
  4. 业务指标:订单成功率、支付成功率等

Q5:令牌桶算法中,突发流量是如何实现的?

技术细节:

// Guava RateLimiter的突发实现
void reserveEarliestAvailable(int requiredPermits, long nowMicros) {
// 1. 首先使用存储的令牌
double storedPermitsToSpend = min(requiredPermits, this.storedPermits);
double freshPermits = requiredPermits - storedPermitsToSpend;
// 2. 计算需要等待的时间
long waitMicros = (long)(freshPermits * stableIntervalMicros);
// 3. 允许预支未来的令牌(实现突发)
this.nextFreeTicketMicros = nextFreeTicketMicros + waitMicros;
this.storedPermits -= storedPermitsToSpend;
}

关键设计:通过nextFreeTicketMicros记录”债务”,后续请求需要等待更长时间,实现了”先突发后偿还”的效果。


总结要点:限流是系统稳定的第一道防线,令牌桶算法因其灵活性成为业界主流,实际选择需结合业务场景、性能要求和运维成本综合考虑。分布式环境下需特别注意一致性和性能的平衡。

My avatar

文章看完了,不打算去别的篇章转转吗?👀 感谢你的耐心阅读!如果你有问题想探讨,或者发现了文中的 Bug,请务必通过页脚的链接联系我


More Posts

# 什么是设计模式?请简述其作用。

7 min read

【核心定义】 设计模式是一套被反复使用、经过分类编目的、用于解决软件设计中特定问题的可复用解决方案,它代表了最佳实践,是面向对象软件设计的经验总结。

Read

Comments