令牌桶比计数器更适合真实接口,因其允许突发流量缓冲、无临界问题;计数器在窗口边界易导致两倍流量冲击,仅适用于精度要求不高的场景。

Go语言限流不是“学完语法再补”的可选项,而是写第一个HTTP服务时就必须嵌入的防护逻辑——不加限流的接口在压测或上线当天就可能被爬虫打挂。
为什么令牌桶比计数器更适合真实接口
计数器限流(FixedWindow)看似简单,但存在临界漏洞:比如窗口是1秒、阈值100,请求在第0.999秒来99个,下一秒开头又来99个,两秒内实际处理198次,却只触发一次清零。而令牌桶(TokenBucket)天然支持突发流量缓冲,桶容量决定峰值容忍度,生成速率决定长期吞吐上限。
- 令牌桶适合API网关、用户登录、支付回调等有短时峰值但需保障可用性的场景
- 计数器只适合内部健康检查、低频配置刷新等对精度要求不高的临时限流
- Go标准库
time.Ticker或手动计算时间差都能实现令牌补充,但必须用sync.Mutex保护tokenNum和lastTime字段,否则高并发下会漏判
TokenBucket.Allow()里最容易错的三处细节
很多开发者照抄示例代码后发现限流失效,问题往往出在时间计算和边界处理上:
在 Go 中使用 google/wire 实现编译时依赖注入——wire.NewSet、wire.Build、wire.Bind(接口→实现)、wire.Struct、wire.Value、wire.Interface
-
elapsed := now.Sub(tb.lastTime)必须用time.Duration做除法,不能直接转int秒数——否则1.9秒只补1个令牌,丢失0.9秒的额度 -
tb.tokens = min(tb.capacity, tb.tokens + addTokens)这行必须存在,否则桶会因浮点误差或时钟跳变超额填充 -
tb.lastTime = now必须放在补充令牌之后、消费之前,否则两次调用间隔小于1秒时,第二次会重复计算同一段时长
单机限流和分布式限流的根本区别在哪
单机限流(如TokenBucket内存实现)只管当前进程,QPS 10万没问题;分布式限流(如基于Redis+Lua)要协调多实例,引入网络延迟和序列化开销,实测QPS掉到2万左右。选型关键看部署形态:
- K8s单Pod多副本?必须用分布式,否则每个副本各自计数,总流量翻倍超标
- Serverless函数或单体二进制部署?用内存限流更轻量,
go run启动即生效 - 混部场景(部分服务上云、部分在IDC)?优先统一用Redis方案,避免限流策略割裂
真正难的不是写对那20行TokenBucket代码,而是想清楚你的服务到底跑在几个物理节点上、流量入口有几个、失败时该返回429还是排队——这些决策比算法本身影响更大。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










