常用的限流算法有一下4中实现方式:
- 令牌桶
- 漏桶
- 计数器
- 滑动窗口
令牌桶
令牌桶以恒定的速度向桶里加入令牌,桶满了则不再加入令牌。当服务收到请求时尝试从桶中取出一个令牌,如果可以获取到令牌,则继续执行后续的业务,否则返回超限错误码或对应的错误页面。
特点:令牌桶可以支持突发流量
代码实现:
package main
import (
"fmt"
"sync"
"time"
)
// TokenBucket 令牌桶
type TokenBucket struct {
cap int // 容量
rate int // 速率(个/秒)
tokens int // 当前令牌数
lastCheck time.Time // 上次检查时间
lock sync.Mutex // lock 避免并发问题
}
// NewTokenBucket 构造令牌桶
func NewTokenBucket(cap, rate int) *TokenBucket {
return &TokenBucket{
cap: cap,
rate: rate,
tokens: cap,
lastCheck: time.Now(),
}
}
// Allow
func (tb *TokenBucket) Allow() bool {
tb.lock.Lock()
defer tb.lock.Unlock()
now := time.Now()
// 当前令牌数等于 时间间隔 * rate + 剩余的令牌数
// 超过容量则设置为容量
tb.tokens += int(now.Sub(tb.lastCheck).Seconds() * float64(tb.rate))
if tb.tokens > tb.cap {
tb.tokens = tb.cap
}
tb.lastCheck = now
if tb.tokens > 0 {
tb.tokens--
return true
}
return false
}
func main() {
tb := NewTokenBucket(10, 2)
for i := 0; i < 20; i++ {
if tb.Allow() {
fmt.Println("allow", i)
} else {
fmt.Println("forbid")
}
}
}
漏桶
漏桶的容量是固定的。当有请求到来时先放到桶中,处理请求的worker以固定的速度从木桶中取出请求进行执行。如果桶已经满了,返回超限错误码或对应的错误页面
特点:流量最均匀的限流实现方式,一般用于流量“整形”
package main
import (
"fmt"
"sync"
"time"
)
type LeakyBucket struct {
cap int // 容量
rate int // 速率(r/s)
cur int // 当前容量
lastCheck time.Time // 上次check时间
lock sync.Mutex
}
func NewLeakyBucket(cap, rate int) *LeakyBucket {
return &LeakyBucket{
cap: cap,
rate: rate,
cur: 0,
lastCheck: time.Now(),
}
}
func (lb *LeakyBucket) Allow() bool {
lb.lock.Lock()
defer lb.lock.Unlock()
now := time.Now()
// 更新桶中当前数量
lb.cur -= int(now.Sub(lb.lastCheck).Seconds() * float64(lb.rate))
if lb.cur < 0 {
lb.cur = 0
}
lb.lastCheck = now
if lb.cur > lb.cap {
return false
}
lb.cur++
return true
}
func main() {
lb := NewLeakyBucket(10, 2)
for i := 0; i < 20; i++ {
if lb.Allow() {
fmt.Println("allow", i)
} else {
fmt.Println("forbid")
}
}
}
计数器
计数器限流算法指 在固定窗口内对请求进行计数,与阀值进行比较,超过阀值则返回超频错误或对应的页面。到达时间临界点,计数清零
特点:简单 问题:临界点可能会出现阀值2倍的并发请求打到服务
package main
import (
"fmt"
"sync"
"time"
)
// CountLimiter 计数器
// 可以增加 cycle 参数来指定计数周期
type CountLimiter struct {
limit int // 限制的最大请求数
count int // 当前请求数
lastCheck time.Time // 上次校验时间
lock sync.Mutex
}
func NewCountLimiter(limit int) *CountLimiter {
return &CountLimiter{
limit: limit,
lastCheck: time.Now(),
}
}
func (cl *CountLimiter) Allow() bool {
cl.lock.Lock()
defer cl.lock.Unlock()
now := time.Now()
if now.Sub(cl.lastCheck).Seconds() > 1 {
cl.count = 0
}
cl.lastCheck = time.Now()
if cl.count < cl.limit {
cl.count++
return true
}
return false
}
func main() {
cl := NewCountLimiter(10)
for i := 0; i < 20; i++ {
if cl.Allow() {
fmt.Println("allow", i)
} else {
fmt.Println("forbid")
}
}
}
滑动窗口
滑动窗口算法将一个大的时间窗口分成多个小窗口,每次大窗口向后滑动一个小窗口,并保证大的窗口内流量不会超出最大值,这种实现比固定窗口的流量曲线更加平滑。
对于滑动时间窗口,我们可以把1s的时间窗口划分成10个小窗口(time slot), 每个time slot统计某个100ms的请求数量。每经过100ms,有一个新的time slot加入窗口,早于当前时间1s的time slot出窗口。窗口内最多维护10个time slot
package main
import (
"fmt"
"sync"
"time"
)
type SlidingWindowCounter struct {
windowSize int // 窗口数
windows []int // 每个窗口的请求数
interval time.Duration // 每个窗口时间间隔
lastCheck time.Time // 上次检查时间
currentWindowId int // 当前窗格id
currentCount int // 当前请求数
maxReq int // 请求阀值
lock sync.Mutex
}
func NewSlidingWindowCounter(maxReq, windowSize int) *SlidingWindowCounter {
return &SlidingWindowCounter{
windowSize: windowSize,
windows: make([]int, windowSize),
interval: time.Second / time.Duration(windowSize),
lastCheck: time.Now(),
currentWindowId: 0,
maxReq: maxReq,
}
}
func (swc *SlidingWindowCounter) Allow() bool {
swc.lock.Lock()
defer swc.lock.Unlock()
// 新的小窗口
if time.Since(swc.lastCheck) > swc.interval {
swc.currentCount -= swc.windows[swc.currentWindowId]
swc.currentWindowId++
// 环形buff
swc.currentWindowId = swc.currentWindowId % swc.windowSize
swc.windows[swc.currentWindowId] = 0
swc.lastCheck = time.Now()
}
swc.windows[swc.currentWindowId]++
swc.currentCount++
return swc.currentCount <= swc.maxReq
}
func main() {
swc := NewSlidingWindowCounter(4, 4)
for i := 0; i < 20; i++ {
if swc.Allow() {
fmt.Println("allow", swc.maxReq, swc.currentCount)
} else {
fmt.Println("forbid", swc.maxReq, swc.currentCount)
}
}
}
总结:
- 令牌桶算法:面对大量请求时具有柔性,且请求处理速度可以随请求速度而变
- 漏桶算法:面对大量请求时具有柔性,但请求处理速度恒定;
- 计数器算法:存在窗口临界问题
- 滑动窗口算法:解决了临界问题,但面对大量请求时不具有柔性;