给函数接口加限流保护,最直接有效的方式就是用令牌桶:它允许突发流量、平滑限速,在性能和可靠性之间取得平衡,且实现成本低。
令牌桶为什么适合函数接口
函数接口和传统Web API不同,它往往承载着更细粒度的业务逻辑调用,你不可能让每个函数都裸奔,尤其是涉及数据库读写、第三方支付、短信发送这类场景。限流不是为了让服务变慢,而是为了避免服务被拖垮。
接口限流的本质是挡掉超出承载力的请求
业内专家指出,绝大多数接口故障并非代码逻辑错误,而是流量洪峰超出了系统设计容量,函数接口被高频调用时,CPU、内存、数据库连接池都会被快速消耗,如果不做任何保护,一次突发的刷单请求就能把整个服务打挂。
令牌桶的工作机制其实很朴素:系统以一个固定速率往桶里放令牌,每个请求进来先取一枚令牌,取到了就放行,桶空了就拒绝或排队,这个机制天然适合函数接口,因为函数调用频率通常有波动,令牌桶允许短时间的积压和突发,不会像固定窗口那样一刀切。
函数接口限流的核心指标
在落地之前,先想清楚三个问题:
- QPS上限:这个接口正常情况下能扛多少并发。
- 峰值容忍度:允许瞬时的请求突发多大,持续多久。
- 超限策略:是直接拒绝、排队等待,还是降级返回默认值。
这三个问题对应令牌桶的三个核心参数:速率(rate)、容量(burst)、超限处理逻辑,参数定完之后,代码实现就是一个数据结构的事。
令牌桶限流和漏桶限流怎么选
很多人在选型时纠结于令牌桶和漏桶的区别,实际落地时两者的差距并不像理论看起来那么大。
| 对比维度 | 令牌桶算法 | 漏桶算法 |
|---|---|---|
| 允许突发流量 | 支持,桶容量决定突发上限 | 不支持,流出速率恒定 |
| 实现复杂度 | 低,一个计数器加时间戳即可 | 低,但需要队列配合 |
| 适用场景 | 函数接口、API网关、微服务 | 平滑流出场景,如消息队列消费 |
| 对流量波动的响应 | 允许突刺,随之平滑 | 完全削峰填谷 |
行业共识认为,函数接口场景下令牌桶的收益更高,原因很简单:

用户请求天然带有随机性,业务上允许某些时刻瞬间涌来一批请求,只要总体速率可控就行,漏桶会把所有请求强制排队,虽然系统最安全,但用户体验会明显变差你排队等待的结果是接口响应延迟飙升。
如果非要说选型的边界,记住这句话:业务需要瞬时处理一批请求,选令牌桶;业务要求绝对均匀的输出速率,选漏桶,大多数函数接口属于前者。
接口限流方案怎么落地
确定用令牌桶之后,接下来就是代码实现,这里不推荐从零手写算法,成熟的语言库已经帮你处理好了边界情况,以Go语言的 golang.org/x/time/rate 为例,它是官方维护的令牌桶实现,性能很好。
单机场景下最简配置
import "golang.org/x/time/rate"
var limiter = rate.NewLimiter(rate.Limit(100), 200)
func ProtectFunc(ctx context.Context, req Request) error {
if !limiter.Allow() {
return errors.New("请求过于频繁,请稍后再试")
}
// 业务逻辑...
return process(ctx, req)
}
上面这段代码,每秒生成100个令牌,桶容量200,意味着接口正常情况下每秒最多处理100个请求,但允许瞬时200个请求同时进来,第一个参数是速率,第二个参数是桶的大小,这个写法对于内部工具函数、定时任务回调、单实例部署的服务完全够用。
分布式场景下的令牌桶怎么处理
单机版本的缺点是令牌桶在内存里,多实例部署时各自独立,无法形成全局限流,这时需要把令牌桶搬到 Redis 上,用 Lua 脚本 保证原子性,伪代码如下:
-- 令牌桶的 Redis Lua 实现核心逻辑
local key = KEYS[1]
local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
local tokens = redis.call("GET", key .. ":tokens")
if not tokens then tokens = capacity end
local lastRefill = redis.call("GET", key .. ":ts")
if not lastRefill then lastRefill = now end
local delta = math.max(0, now - lastRefill)
tokens = math.min(capacity, tokens + delta rate)
if tokens >= requested then
redis.call("SET", key .. ":tokens", tokens - requested)
redis.call("SET", key .. ":ts", now)
return 1
else
return 0
end
这段脚本的特点是:判断和扣减在 Redis 内部原子完成

,不会有并发扣减超卖的问题,实现上需要注意,每次请求都会消耗一次 Redis 的读写,如果接口本身 QPS 极高,Redis 的往返延迟会成为瓶颈,这时候可以考虑给本地做一个缓存副本,每隔几秒同步一次令牌数,牺牲一点精确度换取性能。
路由网关和代码层限流区别
这里有个选择问题:限流逻辑放在网关层还是业务代码里?
- 网关层限流(如 Nginx 的 limit_req、云厂商的 API 网关):统一管控,不侵入业务代码,但对单个业务函数的针对性控制较弱。
- 代码层限流(在函数入口手动加逻辑):灵活精准,能针对不同函数配置不同的速率,但需要每个函数都加一遍。
实际项目里通常结合使用,网关层做粗粒度的全局保护,代码层做细粒度的业务限流,比如支付回调接口需要严格控制频次,就在函数入口加一个 100 QPS 的令牌桶;而普通的查询函数,可能只在网关层做全局 QPS 限制就够了。
配置限流参数的实践经验
参数设多少合适?这个没有标准答案,但有一套可以遵循的推演路径。
从接口的支撑能力倒推
先压测你的函数接口,拿到单实例的吞吐上限,假设压测结果是单实例 500 QPS,部署了 3 个副本,理论集群上限 1500 QPS。限流阈值不要设到 1500,建议预留 20% 的缓冲,也就是 1200 QPS 左右,为什么留余量?因为压测数据是理想状态,真实请求有参数大小差异、有外部依赖耗时波动,不一定跑得满。
桶容量的大小怎么定
桶容量决定瞬时突发能力,普通业务接口建议桶容量设定为速率的 1-2 倍,比如速率 100 QPS,桶容量 150-200,如果业务存在批量操作场景,比如导入文件时一次性调用大量接口,桶容量可以适当放大到速率的 3-5 倍。
这里给一组参考配置:
| 接口类型 | 速率(QPS) | 桶容量 | 超限处理 |
|---|---|---|---|
| 普通查询接口 | 500 | 800 | 直接拒绝 |
| 下单接口 | 200 | 300 | 排队 200ms |
| 支付回调 | 100 | 150 | 直接拒绝 |
| 批量导入 | 50 | 250 | 延迟执行 |
限流之后怎么让用户感知到
限流不只是代码层面的事,还需要让调用方明确知道触发了限制,常用的做法是返回标准 HTTP 状态码

429 Too Many Requests,并在响应头里带上 Retry-After 字段,告诉调用方多久之后可以重试,很多开源客户端库看到 429 会自动退避重试,这样做能显著减少重复请求对系统的二次压力。
限流效果监控与常见问题解答
限流上线后不是一劳永逸,需要观察实际触发次数和决策是否合理,建议在限流器上加一个计数器,记录每分钟被拒绝的请求数,如果这个数字长期为 0,说明限流阈值设置过高;如果长期超过总请求的 20%,说明阈值太紧,需要评估扩容或者调整参数。
做限流保护时容易疏忽的地方
- 没有处理
Allow()返回 false 时的降级逻辑,直接抛异常导致调用方报错。 - 限流器创建在函数内部,每次调用都新建一个实例,导致限流完全失效。
- 忽略了限流器本身的异常处理,Redis 不可用时直接 panic 导致服务雪崩。
限流保护常见问题解答
问:令牌桶限流放本地内存好还是放 Redis 好?
单实例部署且进程不频繁重启,用本地内存性能最高,延迟在纳秒级别;多实例共享流量且需要统一限制,用 Redis 方案,虽然每次请求多了一次网络开销,但换来了全局一致性,折中方案是本地内存做初步拦截,Redis 做二次精确控制。
问:限流和熔断有什么区别?
限流针对的是请求速率,不管系统健康与否,超过阈值就拦截;熔断针对的是下游依赖的健康状态,当下游错误率达到阈值时直接短路,不再发起请求,两者可以配合使用:限流保护上游入口,熔断保护下游依赖。
问:调大桶容量会不会导致系统压力瞬间过大?
理论上会,桶容量决定了允许的突发上线,但实际压力取决于填满桶的速度,即使桶容量设得很大,只要令牌生成速率还是原来的值,被放行的总请求量不会超过生成速率加上存量令牌的总和,所以调大桶容量不会导致无限的突发流量,它只是把之前积累的令牌释放出来,如果当下游系统已经接近瓶颈,把桶容量调小是更稳妥的选择。
收束
函数接口的限流本质上是给系统装一个安全阀,令牌桶算法用最小的代价实现了这个能力,定好速率、容量,配上合理的超限策略,再做好监控和参数调优,就能在绝大多数场景下起到保护作用。