常见的限流算法
漏桶算法
漏桶法的关键点在于漏桶始终按照固定的速率运行,但是它并不能很好的处理有大量突发请求的场景,毕竟在某些场景下我们可能需要提高系统的处理效率,而不是一味的按照固定速率处理请求。
关于漏桶的实现,uber团队有一个开源的github.com/uber-go/ratelimit库。 这个库的使用方法比较简单,Take()
方法会返回漏桶下一次滴水的时间。
package main
import (
"fmt"
"time"
"go.uber.org/ratelimit"
)
func main() {
r1 := ratelimit.New(10) // (10表示1s/10,即每100ms(0.1s)运行一次;同理,1表示1s/1,即每秒运行一次)
prev := time.Now()
for i := 0; i < 10; i++ {
now := r1.Take()
fmt.Println(i, now.Sub(prev))
prev = now
}
}
令牌桶算法
令牌桶(Token bucket)其实和漏桶的原理类似,令牌桶按固定的速率往桶里放入令牌,并且只要能从桶里取出令牌就能通过,令牌桶支持突发流量的快速处理。
对于从桶里取不到令牌的场景,我们可以选择等待也可以直接拒绝并返回。
对于令牌桶的Go语言实现,大家可以参照github.com/juju/ratelimit库。这个库支持多种令牌桶模式,并且使用起来也比较简单。
创建令牌桶的方法:
// 创建指定填充速率和容量大小的令牌桶
func NewBucket(fillInterval time.Duration, capacity int64) *Bucket
// 创建指定填充速率、容量大小和每次填充的令牌数的令牌桶
func NewBucketWithQuantum(fillInterval time.Duration, capacity, quantum int64) *Bucket
// 创建填充速度为指定速率和容量大小的令牌桶
// NewBucketWithRate(0.1, 200) 表示每秒填充20个令牌
func NewBucketWithRate(rate float64, capacity int64) *Bucket
取出令牌的方法如下:
// 取token(非阻塞)
func (tb *Bucket) Take(count int64) time.Duration
func (tb *Bucket) TakeAvailable(count int64) int64
// 最多等maxWait时间取token
func (tb *Bucket) TakeMaxDuration(count int64, maxWait time.Duration) (time.Duration, bool)
// 取token(阻塞)
func (tb *Bucket) Wait(count int64)
func (tb *Bucket) WaitMaxDuration(count int64, maxWait time.Duration) bool
虽说是令牌桶,但是我们没有必要真的去生成令牌放到桶里,我们只需要每次来取令牌的时候计算一下,当前是否有足够的令牌就可以了,具体的计算方式可以总结为下面的公式:
当前令牌数 = 上一次剩余的令牌数 + (本次取令牌的时刻-上一次取令牌的时刻)/放置令牌的时间间隔 * 每次放置的令牌数
我们推荐使用令牌桶算法。
gin中使用限流中间件
import (
"github.com/gin-gonic/gin"
"github.com/juju/ratelimit"
"net/http"
"time"
)
func RateLimit() gin.HandlerFunc {
// 每100ms填充一个令牌,令牌桶容量为10000
bucket := ratelimit.NewBucket(time.Microsecond*100, int64(10000))
return func(c *gin.Context) {
// 如果获取不到令牌就中断本次请求返回
if bucket.TakeAvailable(1) < 1 {
c.JSON(http.StatusOK, gin.H{
"msg": "rate limit...",
})
c.Abort()
return
}
}
}
我们可以看到这个中间件里面使用了c.Abort
这一个代码,所以接下来我们来讲解一下c.Abort
。
c.Next和c.Abort
Next()
:在当前中间件中调用c.Next()
时会中断当前中间件中后续的逻辑转而执行后续的中间件和handlers
,等它们全部执行完之后再回来执行当前中间件的后续代码。
结论
c.Next()
只针对当前的中间件,并不影响其他中间件。如果在中间件函数的非结尾调用Next()
方法当前中间件剩余代码会被暂停执行,会先去执行后续中间件及handlers
,等这些handlers
全部执行完以后程序控制权会回到当前中间件继续执行剩余代码;- 如果想中断剩余中间件以及
handlers
应该使用Abort
方法,但需要注意当前中间件的剩余代码会继续执行。 - 如果想提前中止当前中间件的执行应该使用
return
退出而不是Next()
方法;