1. 前言
限流在做服务端开发时应该并不少见,最先映入脑海的应该有淘宝 双11
,过年回家通过 12306
买火车票的场景等。
这里简单列举一种业务场景:用户的账号密码登录,为防止恶意程序尝试暴力破解密码,频繁的调用登录请求,可以添加一系列的防护措施,如:添加验证码校验,设置时间段内可尝试错误请求的次数等。
2. go 实现
借助 go-cache
开源仓库来做缓存控制。
package main
import (
"fmt"
"github.com/patrickmn/go-cache"
"time"
)
// 使用
func main() {
// 设置一分钟内可以用户a可以尝试10次
c := NewCache(10, time.Minute, time.Second)
key := "a"
go func() {
ticker := time.NewTicker(time.Millisecond * 200)
for {
select {
case <-ticker.C:
fmt.Println("add")
c.Add(key)
}
}
}()
for {
if c.Limit(key) {
fmt.Println("end")
return
}
}
}
type Cache struct {
c *cache.Cache
countLimit int
}
// cleanupInterval 定时清理缓存的时间
func NewCache(countLimit int, timeLimit, cleanupInterval time.Duration) *Cache {
res := new(Cache)
res.c = cache.New(timeLimit, cleanupInterval)
res.countLimit = countLimit
return res
}
func (this *Cache) Limit(key string) bool {
count, ok := this.c.Get(key)
if !ok {
return false
}
curCount := count.(int)
if curCount >= this.countLimit {
return true
}
return false
}
func (this *Cache) Add(key string) {
count, expiration, ok := this.c.GetWithExpiration(key)
if !ok {
this.c.SetDefault(key, 1)
return
}
// 这里的过期时间重新算一下
duration := expiration.Sub(time.Now())
if duration <= 0 {
return
}
curCount := count.(int)
this.c.Set(key, curCount+1, duration)
return
}
上述代码输出 10
次 add
后输出 end
结束程序。
3. 限流策略
限流策略有很多,这里介绍几种常见的,还是要根据实际情况进行选择。
3.1 固定时间窗口限流
很粗糙,两个时间窗口内,若前一次突发流量在最末尾,后一次突发流量在最前端,还是有可能造成系统瘫痪的。
3.2 滑动时间窗口
对固定时间窗口算法的一种改进,流量经过滑动时间窗口算法整形之后,可以保证任意时间窗口内,都不会超过最大允许的限流值,从流量曲线上来看会更加平滑,可以部分解决上面提到的临界突发流量问题。
3.3 令牌桶
- 接口限制 t 秒内最大访问次数为 n,则每隔 t/n 秒会放一个 token 到桶中;
- 桶中最多可以存放 b 个 token,如果 token 到达时令牌桶已经满了,那么这个 token 会被丢弃;
- 接口请求会先从令牌桶中取 token,拿到 token 则处理接口请求,拿不到 token 则执行限流。
匀速生成令牌,直到达到桶的最大值,每来个请求就拿走一个,拿不到的就舍弃。
3.4 漏桶
一共能容纳多少,进出都做记录,桶满了就不再给进来了。
4. 如何配置合理的限流规则
限流规则包含三个部分:时间粒度,接口粒度,最大限流值。
限流的调整,热更新(开启/关闭限流,调整限流规则,更换限流算法)。
5. 总结
本文主要以一个业务来简单的描述了一下限流(固定时间窗口),并附带了一个 go
版本的实现。
限流方式很多,还是要找到最适合本身业务的限流方式,难的不是实现,而是怎么去限流,怎么设计。
6. 参考
2022/10/18
更新
Add
方法 和 Limit
方法合并。
package main
import (
"fmt"
"github.com/patrickmn/go-cache"
"time"
)
func main() {
c := NewCache(10, time.Minute, time.Minute)
key := "a"
ticker := time.NewTicker(time.Millisecond * 200)
i := 0
for {
select {
case <-ticker.C:
i++
if c.Limit1(key) {
fmt.Println("end:", i)
return
}
}
}
}
func (this *Cache) Limit1(key string) bool {
count, expiration, ok := this.c.GetWithExpiration(key)
if !ok {
this.c.SetDefault(key, 1)
return false
}
duration := expiration.Sub(time.Now())
if duration <= 0 {
// 过期了重新设置值
this.c.SetDefault(key, 1)
fmt.Println("过期了")
return false
}
curCount := count.(int)
if curCount >= this.countLimit {
return true
}
this.c.Set(key, curCount+1, duration)
return false
}
输出结果:end:11