在高并发系统中,接口限流是保护服务稳定性的重要手段。本文深入讲解三种经典限流算法——令牌桶(Token Bucket)、漏桶(Leaky Bucket)和滑动窗口(Sliding Window),并提供完整的Go语言实现。
一、为什么需要限流?
在分布式系统中,突发流量可能导致服务过载甚至崩溃。限流(Rate Limiting)的核心目标是:在有限资源下,控制请求的处理速率,保障服务稳定运行。常见的应用场景包括:
- 防止恶意攻击或爬虫过度消耗资源
- 保护下游服务不被突发流量击垮
- 实现API的QoS分级,保障核心接口可用性
- 防止数据库连接池耗尽
二、固定窗口计数器(Fixed Window Counter)
这是最简单的限流算法:在固定时间窗口内统计请求数,超过阈值则拒绝请求。
package main
import (
"fmt"
"sync"
"time"
)
// FixedWindowLimiter 固定窗口限流器
type FixedWindowLimiter struct {
rate int // 窗口内允许的最大请求数
interval time.Duration // 窗口大小
count int // 当前窗口请求计数
windowEnd time.Time // 窗口结束时间
mu sync.Mutex
}
func NewFixedWindowLimiter(rate int, interval time.Duration) *FixedWindowLimiter {
return &FixedWindowLimiter{
rate: rate,
interval: interval,
windowEnd: time.Now().Add(interval),
}
}
func (f *FixedWindowLimiter) Allow() bool {
f.mu.Lock()
defer f.mu.Unlock()
now := time.Now()
// 如果当前时间超过窗口结束时间,重置窗口
if now.After(f.windowEnd) {
f.count = 0
f.windowEnd = now.Add(f.interval)
}
if f.count >= f.rate {
return false
}
f.count++
return true
}
func main() {
limiter := NewFixedWindowLimiter(5, time.Second)
for i := 0; i < 10>
缺陷:固定窗口存在"临界问题"——在窗口边界处可能出现两倍的限流阈值。例如:1秒允许100请求,第0.9秒来了100个请求,第1.0秒重置后又来了100个请求,实际上0.2秒内通过了200个请求。
三、滑动窗口(Sliding Window)
滑动窗口算法将时间划分为多个小窗口,随着时间推移,窗口向前滑动。通过统计多个小窗口的请求总数来判断是否限流,有效解决了固定窗口的临界问题。
package main
import (
"container/list"
"fmt"
"sync"
"time"
)
// SlidingWindowLimiter 滑动窗口限流器
type SlidingWindowLimiter struct {
rate int // 允许的最大请求数
windowSize time.Duration // 滑动窗口总大小
interval time.Duration // 每个子窗口大小
requests *list.List // 存储请求时间戳
mu sync.Mutex
}
func NewSlidingWindowLimiter(rate int, windowSize time.Duration, subWindows int) *SlidingWindowLimiter {
return &SlidingWindowLimiter{
rate: rate,
windowSize: windowSize,
interval: windowSize / time.Duration(subWindows),
requests: list.New(),
}
}
func (s *SlidingWindowLimiter) Allow() bool {
s.mu.Lock()
defer s.mu.Unlock()
now := time.Now()
// 移除窗口外的所有请求记录
cutoff := now.Add(-s.windowSize)
for s.requests.Len() > 0 {
front := s.requests.Front()
if front.Value.(time.Time).Before(cutoff) {
s.requests.Remove(front)
} else {
break
}
}
// 检查当前窗口内的请求数是否超限
if s.requests.Len() >= s.rate {
return false
}
s.requests.PushBack(now)
return true
}
func main() {
// 1秒内最多50个请求,分为10个子窗口
limiter := NewSlidingWindowLimiter(50, time.Second, 10)
passed, rejected := 0, 0
for i := 0; i < 60>
四、漏桶算法(Leaky Bucket)
漏桶算法以水做类比:请求像水一样流入桶中,桶以固定速率漏水(处理请求)。如果水流入速度过快(请求过多),桶满后水会溢出(请求被拒绝)。
特点:输出速率恒定,能将突发流量整形为匀速输出,适合下游处理能力固定的场景。
package main
import (
"fmt"
"sync"
"time"
)
// LeakyBucketLimiter 漏桶限流器
type LeakyBucketLimiter struct {
capacity int // 桶容量
rate float64 // 漏水速率(每秒处理请求数)
water float64 // 当前桶中水量
lastLeak time.Time // 上次漏水时间
mu sync.Mutex
}
func NewLeakyBucketLimiter(capacity int, rate float64) *LeakyBucketLimiter {
return &LeakyBucketLimiter{
capacity: capacity,
rate: rate,
lastLeak: time.Now(),
}
}
func (l *LeakyBucketLimiter) Allow() bool {
l.mu.Lock()
defer l.mu.Unlock()
now := time.Now()
// 计算自上次漏水以来流出的水量
elapsed := now.Sub(l.lastLeak).Seconds()
leaked := elapsed * l.rate
l.water -= leaked
if l.water < 0 xss=removed xss=removed> float64(l.capacity) {
return false
}
l.water++
return true
}
func main() {
// 桶容量20,每秒处理5个请求
limiter := NewLeakyBucketLimiter(20, 5)
// 模拟突发流量
fmt.Println("=== 突发流量测试 ===")
for i := 0; i < 10>
五、令牌桶算法(Token Bucket)
令牌桶算法是实践中最常用的限流算法。系统以固定速率向桶中添加令牌,每个请求需要消耗一个令牌。如果桶为空且令牌不足,请求被拒绝。
特点:允许突发流量(只要桶中有足够的令牌),同时限制了平均速率。这是API网关(如Redis的redis-cell模块)常用的算法。
package main
import (
"fmt"
"sync"
"time"
)
// TokenBucketLimiter 令牌桶限流器
type TokenBucketLimiter struct {
capacity int // 桶最大容量
rate float64 // 令牌生成速率(每秒)
tokens float64 // 当前令牌数
lastRefill time.Time // 上次补充令牌时间
mu sync.Mutex
}
func NewTokenBucketLimiter(capacity int, rate float64) *TokenBucketLimiter {
return &TokenBucketLimiter{
capacity: capacity,
rate: rate,
tokens: float64(capacity), // 初始满桶
lastRefill: time.Now(),
}
}
func (t *TokenBucketLimiter) Allow() bool {
return t.AllowN(1)
}
// AllowN 尝试消耗n个令牌
func (t *TokenBucketLimiter) AllowN(n int) bool {
t.mu.Lock()
defer t.mu.Unlock()
t.refill()
if float64(n) > t.tokens {
return false
}
t.tokens -= float64(n)
return true
}
// refill 补充令牌
func (t *TokenBucketLimiter) refill() {
now := time.Now()
elapsed := now.Sub(t.lastRefill).Seconds()
tokensToAdd := elapsed * t.rate
t.tokens += tokensToAdd
if t.tokens > float64(t.capacity) {
t.tokens = float64(t.capacity)
}
t.lastRefill = now
}
// Wait 等待直到获取令牌,返回等待时间
func (t *TokenBucketLimiter) Wait(n int) time.Duration {
t.mu.Lock()
defer t.mu.Unlock()
t.refill()
if t.tokens >= float64(n) {
t.tokens -= float64(n)
return 0
}
// 计算需要等待的时间
deficit := float64(n) - t.tokens
waitTime := time.Duration(deficit/t.rate*1000) * time.Millisecond
t.tokens = 0
t.lastRefill = t.lastRefill.Add(waitTime)
return waitTime
}
func main() {
// 桶容量50,每秒生成10个令牌
limiter := NewTokenBucketLimiter(50, 10)
// 测试突发消费
fmt.Println("=== 令牌桶突发消费测试 ===")
for i := 0; i < 15>
六、三种算法对比与选型建议
| 算法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 滑动窗口 | 平滑过渡、无临界问题 | 内存开销较高 | 需要精确限流的场景 |
| 漏桶 | 输出速率恒定 | 无法应对突发流量 | 下游处理能力固定的场景(如数据库写入) |
| 令牌桶 | 允许突发流量 | 参数调优复杂 | API网关、大多数Web服务 |
七、生产环境最佳实践
在实际工程中,限流通常部署在API网关层(如Kong、Nginx、APISIX),建议关注以下几点:
- 分布式限流:多实例部署时使用Redis实现集群级限流,例如用Lua脚本实现原子化令牌桶
- 多维度限流:同时按IP、用户ID、API路径等维度设置不同限流规则
- 熔断配合:限流不是万能的,应与熔断器(Circuit Breaker)、降级策略配合使用
- 监控告警:对限流触发次数进行监控,发现异常流量及时告警
八、总结
限流算法本质是在"系统吞吐"与"服务保护"之间做权衡。滑动窗口提供精确的速率控制,漏桶实现流量整形,令牌桶则在限流的同时保留了一定的突发处理能力。理解各算法的原理与适用场景,才能在实际架构设计中做出正确的选型。

发表评论 取消回复