令牌桶算法与Golang官方包的实现
概念
什么叫令牌桶算法?它有点像疫情期间你去公园玩,为了保证在公园里的人不超过一定的数量,你得在门口大爷那取号子,拿到号子才能进去玩,没号子你就不能进去。当然这例子有点不是太准确,感兴趣的话可以看下维基百科下的解释:https://en.wikipedia.org/wiki/Token_bucket
下面一幅图简单解释了令牌桶的工作流程:
为了统一,下面均将令牌称之为token
。
作用
限流。是指限制到达系统的并发请求数,当达到限制条件则可以拒绝请求,可以起到保护下游服务,防止服务过载等作用。
使用
新建限流器方法:
1 | func NewLimiter(r Limit, b int) *Limiter { |
实例化一个限流器:
1 | package main |
上述初始化的意思是:构造了一个容量为50的的桶,并且以每秒10个token
的速率往桶里放。
此外,官方还提供了Every
方法来设置向桶里放token
的时间间隔:
1 | // 每100ms放一个token,也就是每秒10个 |
Limiter主要用到这几个方法:
Wait(ctx context.Context) (err error)
WaitN(ctx context.Context, n int) (err error)
Allow() bool
AllowN(now time.Time, n int) bool
Reserve() *Reservation
ReserveN(now time.Time, n int) *Reservation
其中,Wait
/Allow
/Reserve
分别是WaitN(ctx, 1)
/AllowN(time.Now(), 1)
/ReserveN(time.Now(), 1)
的简化形式
WaitN
方法代表当桶内的token
数量小于N时,则等待一段时间(超时时间可以通过context.withTimeout
设置)。如果token
数量充足,则从桶中消费掉这N个token
,则直接返回。AllowN
方法代表截止到某一时刻,判断桶中的token
数量至少为N个,如果满足,则从桶中消费掉这N个token
,返回true
;否则直接返回false
。ReserveN
方法调用完成后,会返回一个*Reservation
对象,你可以继续使用该对象的Delay
方法或Cancel
方法。
实际使用中,可以根据不同的场景使用不同的方法。比如:AllowN
方法可以用在频繁访问场景中,超过一定的速率则直接拒绝访问。
当然你也可以动态调整限流器的速率和桶大小,使用如下方法:
SetLimit(newLimit Limit)
SetBurst(newBurst int)
源码分析
官方限流器并没有通过队列来实现桶的逻辑,下面我们通过源码来看一下。
限流器的定义:
1 | type Limiter struct { |
这里有几个字段解释一下:
limit
:其实就是float64
的别名。它代表token
入桶的频率,即每秒可以塞几个token
到桶里面。burst
:token
桶的大小tokens
:桶中剩余的token
数量last
: 上一次取走token
的时间lastEvent
: 上一次发生限流时间的时间
WaitN
、AllowN
、ReserveN
这三个方法最终都调用了reserveN
和advance
方法,下面我们来看下这两个方法,我已将主要的注释标注上去了。
1 | func (lim *Limiter) advance(now time.Time) (newNow time.Time, newLast time.Time, newTokens float64) { |
可以看到,advance
方法的作用是: 计算出当一个请求进来的时刻,当前可用的token
数量,并返回请求时间和上一次token
取走的时间
1 | func (lim *Limiter) reserveN(now time.Time, n int, maxFutureReserve time.Duration) Reservation { |
reserveN
方法的作用是:判断这个请求能否获得想要数量的token(n)
,并更新这次请求后Limiter
实例的状态。
从上面的分析可以看到,官方的限流器设计还是很精巧的,结合官方库下的测试用例看得话效果更好。(^o^)/~