github.com/golang/time

Golang官方的事件频率限制器,可以用来限制事件发生的频率,平时项目中可以用来做网关层的限流中间件,也可以用来做请求后台资源限制的中间件。

令牌桶频率限制算法

令牌桶算法是比较常见的限流算法之一,大概描述如下:
1)、所有的请求在处理之前都需要拿到一个可用的令牌才会被处理;
2)、根据限流大小,设置按照一定的速率往桶里添加令牌;
3)、桶设置最大的放置令牌限制,当桶满时、新添加的令牌就被丢弃或者拒绝;
4)、请求达到后首先要获取令牌桶中的令牌,拿着令牌才可以进行其他的业务逻辑,处理完业务逻辑之后,将令牌直接删除;
5)、令牌桶有最低限额,当桶中的令牌达到最低限额的时候,请求处理完之后将不会删除令牌,以此保证足够的限流。

使用示例

// 初始化一个限制器,一般在主协程初始化,这样所有协程共享总的事件限制
limiter := NewLimiter(1000, 1000)

// 具体的每一个协程触发,保证协程进行有效消耗token
if !limiter.Allow() {
	// 如果被限制,则返回403,或者其他业务状态
}
复制代码

源码分析

数据结构

type Limiter struct {
	mu     sync.Mutex
	limit  Limit
	burst  int
	tokens float64
	// last is the last time the limiter's tokens field was updated
	last time.Time
	// lastEvent is the latest time of a rate-limited event (past or future)
	lastEvent time.Time
}
复制代码

mu:主要用来保证并发安全,防止出现竞争访问同一模块数据;
limit:速率,可以用来表示我们常见的TPS、QPS;在限制器是表示生成token的速率;
burst:桶的容量,可以表示最大的并发数量;
tokens:当前桶中token的数量;
last:则代表最早的请求消耗token的时间,每次请求会修改,每次计算token容量都会进行修正;

核心算法函数

整个令牌桶事件频率控制算法就在以下函数实现的;
下面保留了主要的计算逻辑,如何计算当前桶中token的容量?如何计算token消耗的数量?以及相关的临界值校验。


func (lim *Limiter) reserveN(now time.Time, n int, maxFutureReserve time.Duration) Reservation {
	lim.mu.Lock()
	...
	// 当前时间、开始时间、token数量
	now, last, tokens := lim.advance(now)
	// 计算请求带来的消耗
	// Calculate the remaining number of tokens resulting from the request.
	tokens -= float64(n)
	...
	// 当前请求是否可以通过
	ok := n <= lim.burst && waitDuration <= maxFutureReserve
	...
	if ok {
		lim.last = now
		lim.tokens = tokens
		...
	} else {
		lim.last = last
	}
}
复制代码

主要流程:

1、计算当前时间,限流器有效的最早时间,当前桶中token的容量
2、计算当前的消耗后的token数量
3、最后更新限流器相关的数据

注意事项

1、首先是限制器最早有效时间,如果当前时间更早,则进行更新
2、计算时间消耗,如果时间大于允许最大消耗的时间(通过当前桶中的token数量计算),则进行重置
3、计算当前桶中token的数量=已经过去的时间*速率;修正数据,如果大于桶的容量,则重置为桶的最大容量

总结

golang/time包还有其他Reserve(),Wait(ctx context.Context)等函数;Wait()主要是通过上下文和时间计数器实现可以等待的功能,一般如果仅是用来做限流功能,使用Allow()函数即可满需求。