# 什么是限流?限流算法有哪些?怎么实现的?
9 min read
Table of Contents
什么是限流?限流算法有哪些?怎么实现的?
限流面试标准答案
【核心定义】
限流是一种通过控制单位时间内系统处理请求的数量,来保护系统免受过载冲击的流量控制技术。
【关键要点】
- 目的与价值:防止突发流量压垮系统,保障核心服务稳定,实现平滑削峰填谷
- 核心指标:QPS(每秒查询数)、并发连接数、网络带宽等
- 应用场景:API网关、微服务接口、数据库连接池、消息队列消费等
- 实现层级:可在网关层、应用层、中间件层等多层次实施
【深度推导/细节】
主流限流算法实现原理
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 0else redis.call('INCR', key) redis.call('EXPIRE', key, 1) return 1end【版本差异与演进】
Java生态发展
- 早期:自定义实现计数器/漏桶
- Guava 18+:RateLimiter提供生产级令牌桶
- Spring Cloud Gateway:集成RedisRateLimiter
- Sentinel 1.8+:支持热点参数限流、集群限流
云原生时代
- Envoy:基于令牌桶的全局/本地限流
- Istio:服务网格层级的全局限流
- Kong:插件化限流策略
『面试官追问』
Q1:如何选择限流算法的具体参数?
回答要点:
- 压测基准:通过压测获取系统单机最大QPS
- 安全系数:通常取压测值的70%-80%作为限流阈值
- 令牌桶容量:根据业务可接受的突发持续时间计算
- 公式:突发容量 = 平均QPS × 允许突发时间
- 动态调整:结合监控指标实现动态限流
Q2:分布式限流如何保证一致性?
解决方案对比:
- Redis中心化方案:简单但存在单点瓶颈
- 令牌分发方案:中心节点分发令牌,复杂度高
- 滑动窗口+一致性哈希:每个节点负责部分key的限流
- 本地限流+定期同步:折中方案,允许短暂不一致
Q3:限流后的请求如何处理?
分级处理策略:
- 快速失败:直接返回”系统繁忙”(默认)
- 排队等待:进入队列,设置超时时间
- 降级返回:返回缓存数据或默认值
- 优先级队列:VIP用户请求优先处理
- 预热机制:冷启动时逐步增加限流阈值
Q4:如何验证限流效果?
监控指标体系:
- 基础指标:限流触发次数、拒绝请求数、通过QPS
- 质量指标:请求平均延迟、错误率、超时率
- 系统指标:CPU使用率、内存占用、GC频率
- 业务指标:订单成功率、支付成功率等
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记录”债务”,后续请求需要等待更长时间,实现了”先突发后偿还”的效果。
总结要点:限流是系统稳定的第一道防线,令牌桶算法因其灵活性成为业界主流,实际选择需结合业务场景、性能要求和运维成本综合考虑。分布式环境下需特别注意一致性和性能的平衡。