当流量洪峰突然袭来时,你的服务能否稳如泰山?在十三Tech 的后端架构中,限流是保障服务可用性的第一道防线。它确保系统在自身处理能力范围内运行,避免因资源耗尽而导致级联故障。本文将深入剖析四种最常用的限流算法——令牌桶、漏桶、计数器和滑动窗口,并给出可直接落地的 Go 实现。
令牌桶算法
令牌桶以恒定速率向桶中添加令牌,桶满则丢弃多余令牌。当请求到来时,尝试从桶中取出一个令牌,取到则放行,否则拒绝。
核心特点:支持一定程度的突发流量,具有柔性。
Go 实现
package main
import (
"fmt"
"sync"
"time"
)
// TokenBucket 令牌桶
type TokenBucket struct {
cap int // 桶容量
rate int // 令牌产生速率(个/秒)
tokens int // 当前令牌数
lastCheck time.Time // 上次检查时间
lock sync.Mutex // 保证并发安全
}
// 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()
// 根据时间间隔补充令牌
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")
}
}
}
漏桶算法
漏桶的容量固定,请求先进入桶中,处理以恒定速率从桶中取出执行。如果桶已满,新请求被拒绝。
核心特点:流量最均匀,常用于流量"整形"。
Go 实现
package main
import (
"fmt"
"sync"
"time"
)
// LeakyBucket 漏桶
type LeakyBucket struct {
cap int // 桶容量
rate int // 漏出速率(个/秒)
cur int // 当前水量
lastCheck time.Time // 上次检查时间
lock sync.Mutex
}
func NewLeakyBucket(cap, rate int) *LeakyBucket {
return &LeakyBucket{
cap: cap,
rate: rate,
cur: 0,
lastCheck: time.Now(),
}
}
// Allow 尝试将请求放入漏桶
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")
}
}
}
计数器算法
计数器算法在固定时间窗口内统计请求数,与阈值比较,超过则拒绝。到达时间临界点后计数清零。
核心特点:实现简单,但存在临界突发问题。
Go 实现
package main
import (
"fmt"
"sync"
"time"
)
// CountLimiter 计数器限流器
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(),
}
}
// Allow 判断是否允许通过
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 = 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")
}
}
}
滑动窗口算法
滑动窗口将大的时间窗口划分为多个小窗口(time slot),每次向后滑动一个小窗口,保证大窗口内的总流量不超过阈值。流量曲线比固定窗口更平滑。
Go 实现
package main
import (
"fmt"
"sync"
"time"
)
// SlidingWindowCounter 滑动窗口计数器
type SlidingWindowCounter struct {
windowSize int // 小窗口数量
windows []int // 每个小窗口的请求数
interval time.Duration // 每个小窗口的时间间隔
lastCheck time.Time // 上次检查时间
currentWindowId int // 当前窗口索引
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,
}
}
// Allow 判断是否允许通过
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++
// 环形缓冲区
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)
}
}
}
算法对比与选型建议
| 算法 | 突发流量支持 | 流量平滑度 | 实现复杂度 | 适用场景 |
|---|---|---|---|---|
| 令牌桶 | 支持 | 较好 | 中 | 允许突发、限制平均速率 |
| 漏桶 | 不支持 | 最好 | 中 | 严格限流、流量整形 |
| 计数器 | 不支持 | 差 | 低 | 简单场景、粗粒度限流 |
| 滑动窗口 | 不支持 | 好 | 高 | 精准限流、避免临界问题 |
在十三Tech 的实践中,我们的选型策略是:
- API 网关层:令牌桶或滑动窗口,兼顾突发能力和精度
- 内部服务调用:漏桶,保证流量均匀,避免下游被打爆
- 简单阈值保护:计数器,快速实现,适合非核心路径
- 核心交易链路:滑动窗口 + 令牌桶组合,多重防护
总结
限流是构建高可用系统的必备技能。令牌桶柔性最强,漏桶最均匀,计数器最简单,滑动窗口最精准。没有最好的算法,只有最适合场景的算法。在十三Tech 的架构设计中,我们通常会在网关层、服务层、数据库访问层分别部署不同策略的限流器,形成多层防御体系。理解这些算法的本质,能帮助你在面对不同的流量特征时做出正确的技术选型。