最近项目需要对新增做限流,就使用到了go官方提供的以令牌桶算法实现的限流器的使用,找了许多博文资料,自己亲测成功有效。主要导官方工具包
"golang.org/x/time/rate"
一:先附上代码
1.抽出关键代码以工具包形式避免代码耦合度过高
package limiter
import (
"golang.org/x/time/rate"
"sync"
"time"
)
type Limiters struct {
limiters *sync.Map
}
type Limiter struct {
limiter *rate.Limiter
lastGet time.Time //上一次获取token的时间
key string
}
var GlobalLimiters = &Limiters{
limiters: &sync.Map{},
}
var once = sync.Once{}
func NewLimiter(r rate.Limit, b int, key string) *Limiter {
once.Do(func() {
go GlobalLimiters.clearLimiter()
})
keyLimiter := GlobalLimiters.getLimiter(r, b, key)
return keyLimiter
}
func (l *Limiter) Allow() bool {
l.lastGet = time.Now()
return l.limiter.Allow()
}
// r:往桶里放Token的速率 b:令牌桶的大小 key:可对某id\ip做限制
func (ls *Limiters) getLimiter(r rate.Limit, b int, key string) *Limiter {
limiter, ok := ls.limiters.Load(key)
if ok {
return limiter.(*Limiter)
}
l := &Limiter{
limiter: rate.NewLimiter(r, b),
lastGet: time.Now(),
key: key,
}
ls.limiters.Store(key, l)
return l
}
//清除过期的限流器
func (ls *Limiters) clearLimiter() {
for {
time.Sleep(1 * time.Minute)
ls.limiters.Range(func(key, value interface{}) bool {
//超过1分钟
if time.Now().Unix()-value.(*Limiter).lastGet.Unix() > 60 {
ls.limiters.Delete(key)
}
return true
})
}
}
2.在需要进行限流的接口使用
// createDataBank 注册新数据文件
func CreateFootPrint(c *gin.Context) {
appG := app.Gin{C: c}
// 10ms放一个token,桶容量100
limit := limiter.NewLimiter(rate.Every(10*time.Millisecond), 100, "")
// 令牌桶限流器--防止大量请求
if !limit.Allow() {
appG.Response(models.ErrorFrequently, "写入失败", "您的访问过于频繁,请稍后再试")
return
}
// 省略业务代码
........
appG.Response(models.Success, "成功", response)
}
自测方法,建立一个limiter_test测试限流器是否管用
package limiter
import (
"golang.org/x/time/rate"
"testing"
"time"
)
func TestMqProducer(t *testing.T) {
l := NewLimiter(rate.Every(time.Millisecond * 31), 1, "152****86")
for i := 0; i < 10; i++ {
if l.Allow() {
t.Log("success")
} else {
t.Log("您的访问过于频繁")
}
time.Sleep(time.Millisecond * 20)
}
}
二、原理和参数记录
time/rate
包的Limiter
类型对限流器进行了定义,所有限流功能都是通过基于Limiter
类型实现的,其内部结构如下
type Limiter struct {
mu sync.Mutex
limit Limit
burst int // 令牌桶的大小
tokens float64
last time.Time // 上次更新tokens的时间
lastEvent time.Time // 上次发生限速器事件的时间(通过或者限制都是限速器事件)
}
其主要字段的作用是:
-
limit:
limit
字段表示往桶里放Token的速率,它的类型是Limit,是int64的类型别名。设置limit
时既可以用数字指定每秒向桶中放多少个Token,也可以指定向桶中放Token的时间间隔,其实指定了每秒放Token的个数后就能计算出放每个Token的时间间隔了。 -
burst: 令牌桶的大小。
-
tokens: 桶中的令牌。
-
last: 上次往桶中放 Token 的时间。
-
lastEvent:上次发生限速器事件的时间(通过或者限制都是限速器事件)
可以看到在 timer/rate
的限流器实现中,并没有单独维护一个 Timer 和队列去真的每隔一段时间向桶中放令牌,而是仅仅通过计数的方式表示桶中剩余的令牌。每次消费取 Token 之前会先根据上次更新令牌数的时间差更新桶中Token数
这个池子一开始容量为b,装满b个令牌,然后每秒往里面填充r个令牌。
由于令牌池中最多有b个令牌,所以一次最多只能允许b个事件发生,一个事件花费掉一个令牌。
可以使用以下方法构造一个限流器对象:
limiter := rate.NewLimiter(10, 100);
这里有两个参数:
第一个参数是 r Limit,设置的是限流器Limiter的limit字段,代表每秒可以向 Token 桶中产生多少 token。Limit 实际上是 float64 的别名。
第二个参数是 b int,b 代表 Token 桶的容量大小,也就是设置的限流器 Limiter 的burst字段。
对于以上例子来说,其构造出的限流器的令牌桶大小为 100, 以每秒 10 个 Token 的速率向桶中放置 Token,除了给r Limit
参数直接指定每秒产生的 Token 个数外,还可以用 Every 方法来指定向桶中放置 Token 的间隔,例如:
limit := rate.Every(100 * time.Millisecond);
limiter := rate.NewLimiter(limit, 100);
以上就表示每 100ms 往桶中放一个 Token。本质上也是一秒钟往桶里放 10 个
Limiter有三个主要的方法 Allow、Reserve和Wait,最常用的是Wait和Allow方法
这三个方法每调用一次都会消耗一个令牌,这三个方法的区别在于没有令牌时,他们的处理方式不同
Allow: 如果没有令牌,则直接返回false
Reserve:如果没有令牌,则返回一个reservation,
Wait:如果没有令牌,则等待直到获取一个令牌或者其上下文被取消。
一些好的博文参考