第11章 限流
第7章介绍过秒杀系统的架构方案,其中涉及了限流的相关内容。这一章就着重讲解限流的相关知识。

11.1 业务场景:如何保障服务器承受亿级流量
在某次秒杀活动中:
- 总计有 100 个特价商品
- 每个商品的价格都非常低
- 活动计划于 10 月 10 日晚上 10:10:00 开启
服务架构:
客户端 → Nginx → 网关层(Spring Cloud Zuul) → 后台服务
公司预测到秒杀开始那一瞬间会有海量用户涌入,致使系统无法处理所有请求。为保障服务器承受住大流量,只能通过限流的方式将部分流量放入后台服务。
熔断 vs 限流
| 机制 | 发生位置 | 说明 |
|---|---|---|
| 熔断 | 服务调用方 | 发现下游服务有问题时,在一段时间内不再调用 |
| 限流 | 服务被调用方(主要在网关层) | 把超出处理能力的请求抛弃,保证能处理的请求正常 |
业务需求:在某个层级通过限流的方式将秒杀活动的交易 TPS 控制在 100 笔/秒。
11.2 限流算法

11.2.1 固定时间窗口计数算法
假设需求是后台服务每 5 秒钟处理 500 个请求:
| 时间段 | 请求数 | 是否超标 |
|---|---|---|
| 1~5秒 | 200+300=500 | 否 |
| 6~10秒 | 499+1=500 | 否 |
看起来没问题?计算一下 5~9秒 这个区间:
- 300 + 499 = 799 个请求
- 超标了 299 个,服务器支撑不住!
结论:固定时间窗口计数算法在现实中并不实用。
11.2.2 滑动时间窗口计数算法
假设每秒处理 100 个请求,每 100 毫秒设置一个时间区间:
- 每 10 个时间区间合并计算请求总数
- 超出最大数量时把多余请求抛弃
- 进入下一个区间时,窗口向前滑动
优点:大大减少请求数超出阈值且检测不出来的概率
问题:库存 100 个商品,TPS 控制在 100 笔/秒,可能在第一个 100 毫秒请求就超过 100 个。
什么人能在 100 毫秒内完成点击购买、下单、提交订单的整个流程?只有机器人!
11.2.3 漏桶算法

实现步骤:
- 任意请求进来后直接进入漏桶排队
- 以特定的速度处理漏桶队列里面的请求
- 超出漏桶负载范围的新请求直接抛弃
把输出速度设置为 100 个/秒(每 10 毫秒处理一次请求),桶的大小设置为 100。
问题:漏桶算法是先进先出原则,最终被处理的还是前面 100 个请求(机器人的请求)。
11.2.4 令牌桶算法

实现步骤:
- 按照特定速度产生令牌(Token)并存放在令牌桶中
- 如果令牌桶满了,新的令牌将不再产生
- 新请求需要消耗桶中的一个令牌
- 如果桶中没有令牌,进入队列等待
- 如果等待队列满了,新请求直接被抛弃
配置方案:
- 令牌产生速度:100 个/秒
- 等待队列:0(拿不到令牌直接抛弃)
- 令牌桶数量:10(保证最多只有 10 个机器人抢到商品)
11.3 方案实现
11.3.1 使用令牌桶还是漏桶模式
| 算法 | 特点 | 适用场景 |
|---|---|---|
| 漏桶 | 处理速度恒定,无法应对突发流量 | 平稳流量 |
| 令牌桶 | 可以把令牌桶装满,应对突发流量 | 秒杀等突发场景(选择) |
11.3.2 在 Nginx 还是网关层实现限流
| 层级 | 优点 | 缺点 |
|---|---|---|
| Nginx | 性能好 | 基于漏桶算法,对 Lua 不熟悉 |
| 网关层 | 令牌桶算法,团队熟悉 Java,可动态配置 | 性能略低于 Nginx |
选择:在 Java 网关层做限流。
11.3.3 分布式限流还是统一限流
| 方式 | 实现 | 优缺点 |
|---|---|---|
| 统一限流 | 令牌桶数据存放在 Redis | Redis 崩溃则限流失效 |
| 分布式限流 | 每个节点有自己的令牌桶 | 部分节点失效其他节点仍可工作(选择) |
分布式限流的影响:
- 部分节点失效时,后台处理 100 个请求的时间拉长
- 对业务影响不大
11.3.4 使用哪个开源技术
使用开源库 Google-Guava 中 RateLimiter 的相关类来实现限流:
@Component
public class RateLimitFilter extends ZuulFilter {
// 每秒允许10个请求(100/10台服务器)
private RateLimiter rateLimiter = RateLimiter.create(10,
100, TimeUnit.MILLISECONDS); // 100ms后令牌桶满
@Override
public Object run() {
// 超时时间为0,拿不到直接抛弃
if (!rateLimiter.tryAcquire(0, TimeUnit.MILLISECONDS)) {
throw new RateLimitException("请求被限流");
}
return null;
}
}
配置说明:
| 参数 | 值 | 说明 |
|---|---|---|
| permitsPerSecond | 100/10=10 | 每秒产生10个令牌 |
| warmupPeriod | 100毫秒 | 令牌桶大小为1(10台服务器共10个) |
| tryAcquire超时 | 0 | 拿不到令牌直接抛弃 |
11.4 限流方案的注意事项
11.4.1 限流返回给客户端的错误代码
为了给用户带来好的体验:
- 限流后被抛弃的请求返回一个特制的 HTTP CODE
- 客户端展示专门的信息给用户
示例提示:
很遗憾,商品已经秒光,您可以关注下次的秒杀活动。
第二次秒杀活动增加的提示:
您可以在10分钟后过来,有些秒杀成功但没有在10分钟内付款的用户,
他们锁定的商品会被释放出来。
11.4.2 实时监控
对限流日志随时做好记录并实时统计:
- 有助于实时监控限流情况
- 一旦出现意外,可以及时处理
11.4.3 实时配置
在配置中心实现对令牌桶的动态管理 + 实时设置,方便管理其他限流场景。
11.4.4 秒杀以外的场景限流配置
在平时的限流场景中,TPS 或 QPS 需要根据实际的压力测试结果来计算,从而进行限流的正确配置。
11.5 小结

四种限流算法对比
| 算法 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 固定窗口 | 固定时间段计数 | 简单 | 边界突刺问题 |
| 滑动窗口 | 滑动时间段计数 | 精确度高 | 可能被机器人抢占 |
| 漏桶 | 恒定速率处理 | 平滑流量 | 无法应对突发流量 |
| 令牌桶 | 按速率产生令牌 | 可应对突发流量 | 实现稍复杂 |
方案选择总结
| 决策点 | 选择 | 原因 |
|---|---|---|
| 算法 | 令牌桶 | 可应对突发流量 |
| 实现层 | Java网关层 | 团队熟悉,可动态配置 |
| 限流方式 | 分布式 | 部分节点失效仍可工作 |
| 开源库 | Guava RateLimiter | 基于令牌桶,使用简单 |
面试常见问题
- 在秒杀架构中怎么保证不超卖?
- 熔断是基于什么条件触发的?这个条件的数据又是怎么收集的?
- 限流和熔断有什么不同?你了解几种限流算法?用过哪种?为什么?
- 项目中熔断(限流)的参数在上线后调整过吗?是根据什么调整的?
微服务的常见场景就介绍完了。接下来将进入第4部分——微服务进阶场景实战。