从计数器到令牌桶:三种限流方案的落地与取舍
第 5 篇 · 共100篇|用代码丈量成长 —— 坚持写下去,就是最好的成长。
在工作中,很多人第一次接触限流,往往是在被接口“打爆”之后。我自己也是。比如有一次调用阿里的消息机器人接口,它明文规定——一分钟最多只能调用 20 次。
当时我就想:这怎么做?后来才发现,限流其实分层有讲究,从最简单的计数器,一直到灵活的令牌桶,每一步都是在解决上一个方案的问题,同时实现的难度也是越来越高。
一、计数器限流
计数器是最容易上手的一种。说白了,就是设定一个时间段,比如一分钟,只要这段时间里请求数超过上限,就拒绝。
比如在项目里用计数器限流,可以给登录接口加一个防刷保护。最直接的方式:每个用户在一定时间内只能请求几次,用 Redis 计数就行。给每个用户生成一个 key,每次请求就加 1,超过阈值就拒绝。配合过期时间,就能控制“一分钟内最多几次”的效果。但很快就发现问题了:窗口边界效应。
比如在第 59 秒来了 100 个请求,第 60 秒又来了 100 个,总量其实是两倍流量,但系统却没察觉。这时会觉得,这个算法太死板了,只能在窗口内做统计,完全不关心流量分布。
因此:它适合那些节奏比较稳定的场景,比如登录尝试次数、验证码发送频率控制等。简单、快、好理解,面对突发流量,它完全没办法。

二、漏桶限流
后来看到了“漏桶”算法。它的思路像一个“限速阀”:所有请求都得先进桶,再按固定速率流出。桶满了,新来的就直接丢掉。这时候我会觉得,这才算“限流”。
比如消息队列消费端试这个算法。因为消费太快,导致下游数据库压力飙升。用了漏桶之后,输出变得平滑,后台服务也不再被压垮。它解决了系统“稳定输出”的问题。
但我们再考虑下它自身的问题。比如遇到短时间突发流量,“桶一满,直接丢请求“。再加上实现上要维护队列,排队太长时延迟也会变大。此刻,我们会发现,它虽然稳,但不够灵活。因此我们能看出,漏桶适合那种必须保证输出平稳的系统,比如网络带宽整形、队列消费速率控制这类场景。它让系统“慢下来”,但不至于“崩掉”。

三、令牌桶限流
最后要谈论的就是“令牌桶”,它的思路很巧妙:系统不断往桶里放“令牌”,请求必须拿到令牌才能通过。桶能装的令牌有限,如果之前攒了一些,就能应对突发高峰。这对于系统而言意味着——系统能短暂地“爆发”,但不会长期超载。
比如电商秒杀、活动抢购这类场景,流量总是瞬间飙升,硬限流太死板。这个时候,我们可以使用令牌桶机制,根据接口重要性动态调节令牌生成速率。
它能满足更高要求的场景,它的问题是代码实现更复杂,需要考虑更多的逻辑问题


四、怎么实践
谈了一些理论,再谈谈怎么进行实践,下面提两个实践的方式
1. Guava RateLimiter
Guava 自带的 RateLimiter,逻辑非常直白:系统以固定的速率生成令牌,请求想要通过,就得先拿到令牌。拿不到就得等,或者直接被拒绝。
RateLimiter limiter = RateLimiter.create(100);
if (!limiter.tryAcquire()) {return "请求太快啦,请稍后再试";
}它优点就是简单、轻量,本地项目中加个限流几乎没负担。但也正因为简单,RateLimiter 只适合单机使用——如果是多台机器,每台都会自己计算速率,这样整体流量仍可能超出预期。
2. Sentinel
阿里巴巴的 Sentinel 是更常见的企业级限流方案。它提供了控制台,运维人员可以直接在界面上动态调整规则。比如说一个商品详情接口,可以配置“同一个商品 ID 每分钟最多访问多少次”,这样就能很好地控制热点流量,防止把系统拖垮。在分布式场景下,还可以通过 Token Server 统一管理限流规则,实现全局控制。
@SentinelResource(value = "getOrder", blockHandler = "handleBlock")
public String getOrder() {return "正常请求";
}public String handleBlock(BlockException ex) {return "被限流啦,请稍后再试";
}如果项目需要 分布式限流 或者 统一管理流量,那就选择Sentinel。
五、内容总结
这三种限流算法包括两个实践基本能覆盖日常开发里大多数的业务场景。实际工作中,根据实际的场景去选择不同的策略,而不是去追求最牛的算”。限流的目的从来不是为了拒绝请求,而是让系统在高压下依然稳得住。有一个原则——系统不崩,就是底线。能把负载和性能平衡好,让服务稳定地活下去。如果没有使用过,可以自己部署试试看。
你的阅读与同行,让路途更有意义
愿我们一路向前,成为更好的自己
