算法
原理:
(图片来自网络)
- 有一个存放令牌的桶,桶的容量一般即为qps大小
- 有单独生成令牌的线程,以一定速率(和qps有关)往桶里生产令牌,如果桶满则停止生产
- 当有请求过来,首先从桶里获取令牌,如果能获得则继续执行后续业务逻辑,如果无法获取则超时等待或直接返回限流错误
特点:
- 令牌桶本身没有丢弃和优先级策略。
- 桶的容量有限制,所以每个时间段内的流量可控
- 请求处理非匀速
-
实现相对简单
适用场景:
-
因为桶本身会预留一定的空闲令牌,适合流量有突增的场景
代码实现
通过令牌桶的思想可知,我们大概要做以下逻辑
- 有一个桶的实体,可以设置桶的容量,当前剩余的令牌数量等
- 会有一个单独的线程匀速生成令牌
-
提供一个获取令牌的接口,返回获取成功或者失败
以下代码参考kitex qps限流实现
- 令牌桶
type tokenBucketRateLimiter struct {
limit int32 // 容量
tokens int32 // 当前token数
interval time.Duration // 多长时间生成一次token
once int32 // 每次生成多少token
}
- 初始化
func NewTokenBucket(limit int, opts ...Option) RateLimiter {
l := &tokenBucketRateLimiter{
limit: int32(limit),
}
for _, opt := range opts {
opt.apply(l)
}
l.calcOnce()
go l.createTokens()
return l
}
// 设置生成token的间隔时间
func WithInterval(interval time.Duration) Option {
return Option{apply: func(limiter RateLimiter) {
l := limiter.(*tokenBucketRateLimiter)
l.interval = interval
}}
}
间隔时间的设置,主要根据系统每次请求的耗时来决定,如果qps耗时比较低,间隔时间可以适当 变端
- 根据生产token的间隔时间和qps数来计算出每次生成多少token
func (l *tokenBucketRateLimiter) calcOnce() {
if l.interval > time.Second || l.interval == 0 {
l.interval = 100 * time.Millisecond
}
once := int32(float64(l.limit) / (fixedWindowTime.Seconds() / l.interval.Seconds()))
if once < 1 {
once = 1
}
l.once = once
l.tokens = once
}
- 新起一个协程生成token
func (l *tokenBucketRateLimiter) _createTokens() {
if atomic.LoadInt32(&l.tokens) > l.limit {
return
}
cur := atomic.LoadInt32(&l.tokens)
if cur+l.once > l.limit {
atomic.StoreInt32(&l.tokens, l.limit)
} else {
atomic.StoreInt32(&l.tokens, l.once)
}
}
- 获取token
func (l *tokenBucketRateLimiter) Take() bool {
if atomic.LoadInt32(&l.tokens) <= 0 {
return false
}
return atomic.AddInt32(&l.tokens, -1) >= 0
}
完整代码:
总结
- 令牌桶相对来说实现简单,且能比较好的应付突发流量
- 令牌桶方式同样适用于分布式限流,可以部署专门负责生产、发放令牌桶的服务