漏桶算法不能用time.ticker定时补桶,而应按需补桶:记录上次更新时间,用单调时钟(如runtime.nanotime)计算时间差动态填充token,配合atomic.value原子更新tokens和last状态,避免时钟回拨与锁竞争。

漏桶算法用 time.Ticker 实现会出错
直接拿 time.Ticker 每秒发一个 token,看似简单,但无法应对突发流量——它只保证“平均速率”,不控制瞬时并发。真实场景下,客户端可能在 10ms 内连发 5 个请求,而 Ticker 还没 tick 第二次,桶就空了,结果全放行,完全失去限流意义。
正确做法是记录上一次放行/填充时间,按时间差动态计算当前可用 token 数量,即“按需补桶”,不是“定时补桶”。
- 用
sync.Mutex或atomic保护桶状态,避免并发读写冲突 - 每次请求到来时,先更新当前 token 数:
current = min(capacity, previous + rate * (now - lastRefill)) - 判断
current >= 1,成立则扣减 1 并返回 true,否则拒绝 - 注意浮点运算误差,建议统一用纳秒 +
int64计算,避免float64累积漂移
用 golang.org/x/time/rate 的 Limiter 要小心 AllowN 的时间窗口
rate.Limiter 底层就是漏桶(严格说是“平滑令牌桶”),但它默认行为容易误导:调用 AllowN(now, n) 时,它检查的是“从 now 往前推 burst 容量所需时间”是否已过,而不是维护一个持续流动的桶。
这意味着如果连续高频调用 AllowN(t, 1),且 t 是单调递增的系统时间,它能正确限流;但若传入的 t 是固定值(比如误用 time.Now().Unix() 截断到秒),会导致窗口错位、放行过多。
- 永远用
time.Now()原始值传给AllowN或ReserveN,不要截断或缓存 - 需要阻塞等待时,用
ReserveN+Delay,别自己 sleep,否则精度丢失 -
burst参数设太大会让突发流量穿透,设太小又容易误伤正常毛刺,建议设为rate * 1~2 秒(如 rate=100qps,burst=150)
自定义漏桶组件必须处理时钟回拨
服务器时间被 NTP 校正或虚拟机休眠恢复后,time.Now() 可能突然倒退几秒甚至几分钟。此时按时间差补 token 会算出负增量,导致 token 数暴增,限流彻底失效。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
安全做法是只接受单调递增的时间戳,丢弃回拨部分:
- 用
runtime.nanotime()替代time.Now().UnixNano(),它是单调时钟,不受系统时间调整影响 - 若必须用
time.Time,需在结构体里缓存上次合法时间,并做if now.After(last) { ... }判断 - 日志中记录时钟回拨事件(如
log.Warn("clock stepped back", "delta", delta)),方便事后排查限流异常
高并发下 atomic 比 mutex 更适合漏桶状态更新
漏桶核心状态只有两个字段:tokens(当前数量)和 last(上次更新时间)。用 sync.Mutex 加锁虽然安全,但在 QPS 过万时,锁竞争会明显拖慢吞吐。
atomic 可以无锁更新这两个字段,前提是把它们打包进一个 struct 并用 atomic.Value 或 unsafe + atomic.CompareAndSwapUint64 手动管理(推荐前者)。
- 定义状态结构体:
type state struct { tokens int64; last int64 } - 用
atomic.Value.Store()和.Load()原子替换整个状态,避免 ABA 问题 - 避免在
atomic.Value中存指针指向堆对象,防止 GC 压力;直接存值更轻量 - 注意:Go 1.19+ 支持
atomic.Int64,但双字段仍需封装,atomic.Value是目前最稳的选择
漏桶真正难的不是公式,而是怎么在系统时钟不准、并发争抢、精度要求高的现实条件下,让 tokens 这个数字始终可信。多数线上问题都出在时间源和状态更新路径上,不是算法本身。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










