在之前的文章里,我们聊过了秒杀系统的整体架构方案,其中也提到了限流------这个让人又爱又恨的技术环节。因为篇幅所限,我们当时只能浅尝辄止,没来得及展开细说。
那么这一章,我们就来把限流这个问题掰开揉碎,讲个明白。放心,内容依然保持专业与深度,并且尽量说得清楚易懂。
老规矩,为了便于理解,我们还是从一个实际的业务场景开始说起。
1 业务场景:如何让服务器在亿级流量冲击下"活下去"
业务场景:如何让服务器在亿级流量冲击下"活下去"
想象这样一个场景:某次秒杀活动,只有100件特价商品,价格低到让人心动。活动时间定在10月10日晚上10点10分0秒------这时间点选得挺有仪式感,但对系统来说,可能就像一场精心策划的"流量海啸"。
当时的服务架构大致如下图所示:所有用户请求首先到达Nginx层,然后被转发到网关层(基于Java的Spring Cloud Zuul),最后才进入后台业务服务(同样是Java)。这条链路听起来清晰,但秒杀开始瞬间,预测将有无序涌入的海量用户请求------多到系统根本处理不完。

怎么办呢?答案就是限流。为了保证服务器不被冲垮,我们只能理智地选择"放行一部分,拒绝大部分"。这听起来有点残酷,但却是高并发场景下的常见生存策略。
说到这里,容易混淆的两个概念------限流 和熔断------值得简单区分一下。虽然它们都是保护系统的手段,但发生的位置和目的不同:
- 熔断 通常发生在服务调用方。举例来说,如果服务A多次调用服务B都失败,判断服务B已不可用,那么服务A就会主动"熔断",暂时停止调用,避免资源浪费并给服务B恢复的时间。
- 限流 则主要发生在服务被调用方 ,尤其在网关层实施。例如,一个电商后台每秒只能处理10万请求,如果瞬间涌来100万请求,系统可能直接丢弃其中90万,只处理剩余的10万。这个比例在秒杀场景下并不夸张------有时甚至舍弃99%的流量都是可以接受的,只为确保系统不崩溃、部分用户能顺利完成交易。
回到我们的具体业务需求:这次秒杀只有100件商品,也就是说,最终成功的交易只有100笔。我们希望把这些交易控制在大约一秒内完成,即将交易TPS(每秒事务数)限制在100笔/秒。
那么问题来了:如何在系统的某一层(比如网关)实现这样的精确控制?这就引出了我们接下来要深入探讨的限流常用算法。
2 限流算法
在明确了需要将交易TPS控制在100笔/秒的目标后,我们来看看实现限流有哪些常用的算法。每种算法各有特点,其适用性也需结合具体场景来判断。
2.1 固定时间窗口计数算法
这是一种最直观的思路。例如,若要求每5秒处理不超过500个请求,我们就以5秒为一个固定窗口进行计数。
原理与缺陷:
该算法看似能满足需求,但存在一个典型的边界临界问题。假设请求分布如下:
- 第1-4秒:200个请求
- 第5秒:300个请求
- 第6-9秒:499个请求
- 第10秒:1个请求
单独统计第1-5秒和第6-10秒这两个窗口,请求数均为500,未超出阈值。然而,如果我们观察第5-9秒这个跨窗口的区间,请求总数高达300 + 499 = 799个,这已远超系统负载能力。
因此,固定时间窗口算法由于存在统计盲区,在实际高并发场景中通常不适用。
2.2 滑动时间窗口计数算法
为改善固定窗口的缺陷,滑动时间窗口算法应运而生。假设需求是每秒处理100个请求,我们可以将1秒划分为10个100毫秒的子窗口。
工作原理:
系统持续统计当前时间点往前回溯1秒(即最近10个子窗口)内的请求总数。时间每推进100毫秒,窗口就"滑动"一次,丢弃最老的那个子窗口的计数,并入最新的一个子窗口计数,然后重新计算总和。

优势与局限:
这种方法极大地降低了单位时间内流量超标却无法检测到的概率。当然,精度取决于子窗口的粒度:划分为10毫秒会更精准,但计算开销也更大。
然而,在秒杀场景下,该算法依然面临核心挑战: 即便将限流设置为100/秒,也可能出现请求全部集中在第一个100毫秒内到达的情况。这意味着商品在100毫秒内就被"秒光"。能在此极短时间内完成操作的几乎只有自动化脚本("机器人"),这与希望真实用户参与的商业初衷相悖,并不是我们想要的结果。
再看看其他算法。
2.3 漏桶算法
漏桶算法的实现思路如图所示。

实现步骤:
- 所有请求进入漏桶的队列中等待。
- 漏桶以恒定的速率(例如,对应100个/秒,即每10毫秒处理一个)向外处理请求。
- 若桶内队列已满,则新到达的请求会被直接丢弃。
场景分析:
在我们的例子中,若设置桶的大小(队列容量)为100,输出速率为100/秒,那么系统将严格按"先进先出"原则处理前100个请求。这同样会导致商品被瞬间涌入的最早一批请求(很可能来自机器人)抢购一空。若将桶容量设为1,虽能加剧排队、稀释瞬时压力,但仍未从根本上解决机器人优势问题。
再说一下令牌桶算法。
2.4 令牌桶算法
令牌桶算法提供了更灵活的调控能力,其原理如图所示。

- 系统以恒定速率(如100个/秒)生成令牌,并放入容量固定的令牌桶中。
- 每个请求到达时,必须获取并消耗一个令牌才能被处理。
- 若桶中有令牌,则请求立即消耗一个并通行。
- 若桶中无令牌,请求可进入队列等待(如果配置了队列),或直接被拒绝。
- 当等待队列也满时,新请求将被丢弃。
针对秒杀的优化方案:
回到每秒100笔交易的限制。我们可以将令牌生成速率设为100个/秒,同时关键地将令牌桶本身的容量设置成一个较小的值(例如10) ,并将排队队列长度设为0。
这样一来:
- 系统每秒最多只能发放100个令牌(即处理100个请求)。
- 由于桶容量只有10,这意味着在任意瞬时,最多只有10个"库存"令牌可供立即使用。即使秒杀前已提前生成了一些令牌,其数量也被限制在10个以内。
因此,在秒杀开启的瞬间,最多只有约10个请求能立即获得令牌并完成交易 ,其余请求必须等待下一个令牌生成周期(10毫秒后)。这在一定程度上打散了瞬时峰值,增加了真实用户成功的机会。
3 方案实现
3.1 使用令牌桶还是漏桶模式
尽管两者都能满足"每秒100笔"的基本限流需求,但我们需要的是一个能适应多场景的通用方案。
漏桶算法以恒定速率处理请求,其行为类似于一个**"流量整形器"** ,即使系统有空闲资源,也无法加速处理之前累积的请求。
令牌桶算法则因其可以累积令牌的特性,在算法行为上天然允许在阈值范围内应对短时突发流量 ,能更好地利用系统空闲期所预留的处理能力。
因此,从算法机制上看,令牌桶更适合需要一定突发容忍度的通用场景。这种特性使其更适用于更广泛的业务场景。因此,项目组最终选择了灵活性更高的令牌桶算法。当然,无论是令牌桶还是漏桶, 其速率和容量的具体参数值都需要根据实际运维监控数据进行配置和调整。
3.2 在Nginx中实现限流还是在网关层中实现限流
在我们的架构中,流量依次经过Nginx和网关层。将限流组件部署在哪一层,需要仔细权衡。
最终决定在Java网关层(Spring Cloud Zuul) 实现限流,主要基于两点考虑:
- 算法匹配度: Nginx自带的限流模块(如
ngx_http_limit_req_module)主要基于漏桶算法,与我们选定的令牌桶算法不符。 - 技术栈与运维便利性: 实现动态配置管理(如通过管理界面实时调整限流参数)通常需要结合Nginx与Lua。鉴于团队对Java更为熟悉,而对Lua了解有限,选择在Java网关层开发,更利于团队的自主掌控和后续运维。
3.3 使用分布式限流还是统一限流
网关层本身通常是多节点部署的,这就引出了下一个问题:限流状态如何维护?
- 统一限流: 所有网关节点共享一个中心化的令牌桶(例如将令牌计数存放在Redis中)。这种方式看似精确,但引入了单点风险和性能瓶颈。在秒杀瞬间,所有网关节点都需要频繁访问Redis以争夺令牌,极易将Redis压垮,一旦Redis故障,全局限流即刻失效。
- 分布式限流: 每个网关节点独立维护自己的令牌桶。这需要我们预先将总TPS(100笔/秒)平均分配到每个节点。例如,若有10个网关节点,则每个节点的限流阈值设置为10笔/秒。
项目组经过权衡,选择了分布式限流。原因如下:
- 可靠性更高: 避免了Redis这个中心化组件的单点故障风险。即使部分网关节点失效,剩余节点仍能基于自身的令牌桶正常工作。
- 性能更优: 限流决策在各自内存中进行,无远程调用开销,速度极快。
- 带来的影响可接受: 其代价是,在部分节点失效时,总吞吐量会暂时下降(例如从100笔/秒降至50笔/秒),导致处理完所有请求的时间可能拉长(如从1秒变为2秒)。这在我们的业务容错范围内是可以接受的。
3.4 使用哪个开源技术
在Java技术栈中,项目组选择了Google Guava库中的RateLimiter类 来实现令牌桶算法。它是一个广泛验证、简单易用的内存式限流器。
在网关的Zuul过滤器中,我们这样配置和使用它:
- 核心参数配置 :
permitsPerSecond(令牌生成速率):设为 10 。这是由总TPS(100)除以网关节点数(10)计算得出,即每个节点每秒生成10个令牌。warmupPeriod(预热期):设为 100毫秒 。这间接决定了单个节点令牌桶的容量。在预热模式下,此配置意味着桶容量约为1。结合10个节点,全局的"瞬时可用令牌"总量约为10,有效防止了机器人瞬间刷光库存。
- 获取令牌策略 :
- 调用
tryAcquire(0, TimeUnit.SECONDS)方法。超时时间设置为0,意味着请求若无法立即获取到令牌,将直接被快速失败(Fast-Fail),不进入等待队列,这符合秒杀场景高并发、即时反馈的要求。
- 调用
通过以上四个层次的决策,我们构建了一个基于令牌桶算法、部署于Java网关层、采用分布式部署模式,并借助Guava RateLimiter实现的具体限流方案。该方案在确保系统不被冲垮的同时,也兼顾了灵活性、可靠性与业务目标。
4 限流方案的注意事项
将限流方案部署上线只是第一步,要使其稳健、易用并能应对多场景,还需要关注以下几个工程细节。
4.1 限流 - 设计友好的限流响应
限流的目的不是粗暴地拒绝用户,而是为了保障系统整体可用。因此,被限流请求的返回信息至关重要。
一个良好的实践是,为被限流的请求返回一个特定的HTTP状态码(例如 429 Too Many Requests),而非通用的服务器错误码(如500)。这样,客户端(如App或前端)就能准确识别这是"流量限制"而非"系统故障",从而展示更具引导性的友好提示。
例如,可以提示:"很遗憾,商品已秒光,欢迎关注下次活动。"在后续的活动中,我们甚至升级了提示:"部分订单可能因未及时支付而释放库存,请您10分钟后再来试试看。"这种设计显著提升了用户体验。。
4.2 实时监控
"看不见就无法管理"。必须对限流操作进行完整的日志记录,并建立实时监控仪表盘。这能帮助运维人员即时掌握:
- 限流是否已触发?触发的频率和规模如何?
- 被拒绝的请求量是否在预期范围内?
- 系统整体流量与限流阈值的关系。
一旦监控指标出现异常(如限流突然失效或过早触发),团队可以快速介入排查,避免小问题演变为线上故障。
4.3 实时配置
限流参数(如阈值、桶大小)不应是硬编码在代码中的。一个成熟的系统需要将配置外化,并支持动态调整 。
理想情况下,运维人员应能通过统一的配置中心,在不重启服务的情况下,实时修改不同API接口的限流规则。这种灵活性对于应对突发流量、进行业务调整或灰度发布都至关重要。
4.4 秒杀以外的场景限流配置
本次秒杀场景的目标值(100 TPS)是明确的业务结果。但在大多数日常限流场景中(如某个查询接口),正确的阈值需要通过压力测试来科学确定。我们需要根据系统的实际承载能力、业务优先级和SLA要求,来设定合理的QPS(每秒查询数)或TPS限制。切忌凭感觉配置。
5 小结
至此,我们完成了从业务场景分析、算法原理对比到具体方案落地和优化建议的完整闭环。这个基于令牌桶的分布式网关层限流方案,在实践中成功护航了秒杀活动。
核心回顾 :限流的原理(特别是滑动时间窗口与令牌桶算法)是理解所有工具的基础。一旦掌握了原理,使用任何现成库(如Guava RateLimiter)都会变得简单直接。
给开发者的提示 :限流与熔断是高并发系统面试中的高频考点。为了帮助你更好地准备,下面列举几个与之相关的典型问题,它们能很好地考察你对微服务稳定性的理解深度:
- 防超卖设计 :在秒杀架构中,除了限流,如何通过其他机制(如库存扣减)保证商品不会超卖?
- 熔断机制 :服务熔断的触发条件具体是什么?这些指标(如错误率、延迟)是如何被采集和计算的?
- 原理辨析 :限流和熔断的核心区别是什么?你了解几种限流算法?在项目中为何选择特定的一种?
- 运维与调优 :上线后是否调整过熔断或限流的参数?调整的依据是什么(监控数据、压测结果)?又如何验证调整后的效果?
关于微服务核心场景的讨论就到这里。在进入下一部分的"进阶场景实战"之前,我们需要先直面现实------微服务在带来巨大灵活性的同时,也引入了一系列新的挑战与痛点。下一章,我们将系统地梳理这些痛点,为后续的解决方案奠定基础。