直接用 time.tick 实现 token 桶会出错,因其无法动态调整填充速率、高并发下 token 计算存在竞态、不支持预支和精确截止时间,导致限流精度漂移;正确做法是每次请求时按时间差动态补 token,并用互斥锁保护状态。
为什么直接用 time.tick 实现 token 桶会出错
因为 time.tick 无法动态调整填充速率,且在高并发下多个 goroutine 同时调用时,桶内 token 计算容易出现竞态——比如两个请求几乎同时触发填充逻辑,都读到旧的 token 数,各自加 1 后写回,实际只增加 1 而非 2。
更关键的是,它不支持“预支”(burst)和“精确截止时间”控制,导致限流精度漂移。真实场景中,你看到的 qps 波动远超配置值,尤其在流量突增时完全失效。
正确做法是:每次请求来临时,先按**距上次请求的时间差**计算应补充的 token 数,再判断是否足够扣除。这要求记录上一次填充时间点,并用原子操作或互斥锁保护状态。
- 用
sync.Mutex保护tokens和lastFillTime,比原子类型更直观、不易漏字段 - 填充逻辑必须放在
Allow方法内,而不是靠后台 goroutine 定期刷——后者在低频请求下会囤积大量 token,失去“平滑限流”意义 - 注意浮点运算误差:
rate * elapsed.Seconds()可能略小于理论值,建议用math.Floor截断,避免因浮点累积导致 token 溢出
如何让 TokenBucketLimiter 支持突发流量(burst)
Token 桶的核心弹性就来自 burst 容量。如果你把最大容量(capacity)设得太小,比如和 rate 相同,那就退化成漏桶;设得太大,又可能在空闲期攒太多 token,瞬间击穿下游。
典型配置是:capacity = rate * 2 或固定值如 100,取决于你的服务容忍度。例如 API 允许平均 10 QPS、但可接受短时 20 QPS 冲击,那就设 capacity = 20,rate = 10.0(单位:token/秒)。
- 初始化时直接将
tokens设为capacity,允许首次请求立即通过 - 每次填充后要和
capacity取min,防止因系统时间跳变(如 NTP 校准)导致误补过多 - 不要在
Allow中返回剩余 token 数——外部若据此做重试策略,会放大抖动;只返回 bool 即可
Allow 方法里的时间计算为什么必须用 time.Now().Sub(l.lastFillTime)
因为 token 补充不是离散事件,而是连续过程。假设 rate 是 5 token/秒,两次请求间隔 0.3 秒,就该补 5 * 0.3 = 1.5 个 token,向下取整得 1 个。如果硬切为每 200ms 补 1 个,那在 0.3 秒时只能补 1 个(正确),但在 0.5 秒时仍只补 1 个(错误,应补 2 个)。
关键代码片段:
elapsed := time.Now().Sub(l.lastFillTime) newTokens := l.rate * elapsed.Seconds() l.tokens = math.Min(l.capacity, l.tokens+math.Floor(newTokens)) l.lastFillTime = time.Now()
- 必须用
Sub而非手动减 Unix 时间戳——后者在跨纳秒/秒边界或时钟回拨时结果不可靠 -
elapsed.Seconds()返回 float64,务必用math.Floor,不能用int()强转,否则负数截断行为不一致 - 更新
lastFillTime必须在所有计算完成后,否则并发调用时一个 goroutine 可能用到被提前更新的时间点,造成 token 少补
生产环境要注意 rate 的单位和配置热更新
Rate 如果写死为 10.0,单位其实是 “token per second”,但你的接口可能是按分钟计费(如 600 次/分钟),这时别换算成 10.0 就完事——要确认监控指标(如 Prometheus 的 http_requests_total)是否也按秒聚合,否则告警阈值对不上。
- Rate 建议从配置中心加载(如 etcd/Viper),并监听变更事件,调用
Reset(rate, capacity)重建内部状态,而不是改字段——避免中间状态不一致 - 不要用
time.Duration表达 rate(比如写100 * time.Millisecond),这是常见误解:rate 是标量,不是周期 - 压测时发现限流器本身 CPU 占用高?检查是否在
Allow里频繁调用time.Now()——它在某些内核版本下有微秒级开销,可考虑用runtime.nanotime()替代(需自行转秒),但优先优化锁竞争
最易被忽略的一点:token 桶不保证请求处理时间,只控制进入速率。如果后端处理变慢,排队请求仍在增长,此时需要配合超时控制或熔断器,光靠限流器挡不住雪崩。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











