选令牌桶更适合Web场景,因其TakeAvailable()非阻塞;漏桶则强制同步等待,易导致请求hang住且不支持直接拒绝。

选漏桶还是令牌桶,取决于你能不能容忍突发流量——如果接口要扛秒杀、抢券这类短时洪峰,必须用令牌桶;如果只是稳稳守住后端数据库连接池或下游服务的吞吐上限,漏桶更可控、更难被绕过。
漏桶算法在 Gin 中的实际行为:Take() 会阻塞,且无法跳过等待
使用 go.uber.org/ratelimit 的 New() 创建漏桶限流器后,每次调用 Take() 都会返回「下一次允许处理请求的时间点」。它不是返回布尔值或令牌数,而是强制同步等待——哪怕你只差 1ms,也要卡住当前 goroutine 直到那个时刻。
- 这意味着:HTTP 请求会真实地 hang 住,响应延迟 = 等待时间 + 处理时间,
ab -c 100压测时能看到大量请求耗时陡增 - 它不支持「拒绝」语义:你想丢弃请求,得自己判断
Take().Sub(time.Now()) > 0,再手动c.Abort(),否则默认就是等 - 桶容量隐式固定(由速率倒推),不能显式设“最多积压 N 个请求”,容易因堆积过多导致内存缓慢上涨
令牌桶算法在 Gin 中的实际行为:TakeAvailable() 是非阻塞试探,更适合 Web 场景
用 github.com/juju/ratelimit 初始化的令牌桶,核心是 TakeAvailable() —— 它立刻返回能取走的令牌数,不等、不锁、不挂起 goroutine。
- 典型用法:
if bucket.TakeAvailable(1) - 桶初始满(
NewBucketWithQuantum(fillInterval, cap, quantum)中cap就是初始令牌数),所以冷启动瞬间能扛住 burst -
quantum参数控制单次填充量(比如每 100ms 放 10 个令牌,比每 10ms 放 1 个更省系统调用开销),但注意:它不影响总速率,只影响填充粒度
两个库的并发安全与资源开销差异
go.uber.org/ratelimit 的漏桶实现是无状态的:每个 Limiter 实例内部只存一个 last 时间戳和速率,Take() 计算纯靠数学,几乎零内存占用,适合全局单例复用。
- 但它的设计假设是「所有请求走同一个限流器」,若你在中间件里为每个路由新建一个
New(10),就失去了速率控制意义——因为每个实例都独立计时 -
github.com/juju/ratelimit的令牌桶带内部 mutex,TakeAvailable()是线程安全的,但高并发下会有轻微锁竞争;不过它支持 per-route 或 per-user 桶(比如用sync.Map按c.ClientIP()分桶),灵活性更高 - 别直接把
juju/ratelimit.Bucket放进 gin.Context.Value() —— 它不是线程安全的跨 goroutine 传递对象,应在中间件闭包里捕获并复用
真正容易被忽略的坑:时间精度与测试偏差
两种算法都依赖系统时钟,但 go.uber.org/ratelimit 默认用 time.Now(),在容器或虚拟机里可能因时钟漂移导致实际速率偏高;而 juju/ratelimit 内部用 runtime.nanotime(),更稳定但无法感知 wall clock 变化(比如 NTP 调整)。
- 本地用
ab测试时,go.uber.org/ratelimit.New(10)表示「每 100ms 一个请求」,但如果你发 10 个并发,前几个可能瞬间通过,后面全卡住——这不是 bug,是漏桶本意:它只保平均速率,不保瞬时并发 - 用
juju/ratelimit设NewBucket(time.Second, 10),你以为是“每秒 10 个”,实际是“桶容量 10,每秒补满”,如果第一秒初一口气来 10 个请求,全过;第二秒初再来 10 个,也全过——两秒共 20 个,但峰值并发是 10,这才是令牌桶的弹性


















