在Web服务运行过程中,大量突发请求可能会瞬间占用过多服务资源,导致接口响应变慢甚至服务不可用,接口限流就是解决这类问题的核心方案。合理的限流策略可以在保护服务的同时,尽可能保障正常用户的请求能够被处理。
常见的Web限流策略
不同的限流策略适用于不同的业务场景,下面介绍四种主流的限流方案:
- 计数器限流:在固定时间窗口内统计请求数量,超过阈值就拒绝请求。实现简单但存在临界突变问题,比如窗口切换时可能瞬间涌入两倍阈值的请求。
- 滑动窗口限流:将固定时间窗口拆分为多个小时间片,统计最近一段时间内的请求总数,解决了计数器限流的临界问题,实现复杂度稍高。
- 令牌桶限流:系统以固定速率向桶中放入令牌,请求需要先获取令牌才能被处理,支持一定程度的突发流量,是比较常用的限流方案。
- 漏桶限流:请求进入漏桶,漏桶以固定速率处理请求,超出桶容量的请求会被直接拒绝,流量输出非常平稳,不支持突发流量。
基于Golang标准库实现计数器限流
Golang的sync包和time包可以组合实现简单的计数器限流,以下是固定时间窗口计数器限流的示例:
package main
import (
"fmt"
"sync"
"time"
)
// CounterLimiter 计数器限流器
type CounterLimiter struct {
limit int // 时间窗口内允许的最大请求数
window time.Duration // 时间窗口大小
count int // 当前窗口内的请求计数
lastReset time.Time // 上次窗口重置时间
mu sync.Mutex // 互斥锁保证并发安全
}
// NewCounterLimiter 创建计数器限流器
func NewCounterLimiter(limit int, window time.Duration) *CounterLimiter {
return &CounterLimiter{
limit: limit,
window: window,
count: 0,
lastReset: time.Now(),
}
}
// Allow 判断请求是否允许通过
func (l *CounterLimiter) Allow() bool {
l.mu.Lock()
defer l.mu.Unlock()
now := time.Now()
// 如果当前时间超过窗口时间,重置计数和窗口起始时间
if now.Sub(l.lastReset) > l.window {
l.count = 0
l.lastReset = now
}
// 计数未超过阈值则允许通过,同时计数加1
if l.count < l.limit {
l.count++
return true
}
return false
}
func main() {
// 创建限流器:1秒内最多允许3个请求
limiter := NewCounterLimiter(3, time.Second)
// 模拟10个并发请求
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(index int) {
defer wg.Done()
if limiter.Allow() {
fmt.Printf("请求%d: 允许通过n", index)
} else {
fmt.Printf("请求%d: 被限流拒绝n", index)
}
}(i)
}
wg.Wait()
}
使用golang.org/x/time/rate实现令牌桶限流
Golang官方扩展库golang.org/x/time/rate提供了成熟的令牌桶限流实现,无需自己重复造轮子,以下是结合Gin框架实现接口限流的示例:
package main
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"golang.org/x/time/rate"
)
// RateLimiterMiddleware 令牌桶限流中间件
func RateLimiterMiddleware(r rate.Limit, b int) gin.HandlerFunc {
// 创建令牌桶限流器,r是每秒放入的令牌数,b是桶的容量
limiter := rate.NewLimiter(r, b)
return func(c *gin.Context) {
// Allow方法判断请求是否允许通过,不阻塞等待令牌
if !limiter.Allow() {
c.JSON(http.StatusTooManyRequests, gin.H{
"code": 429,
"msg": "请求过于频繁,请稍后再试",
})
c.Abort()
return
}
c.Next()
}
}
func main() {
r := gin.Default()
// 注册限流中间件:每秒生成2个令牌,桶容量为5
r.Use(RateLimiterMiddleware(2, 5))
// 测试接口
r.GET("/api/test", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"code": 200,
"msg": "请求成功",
"time": time.Now().Format("2006-01-02 15:04:05"),
})
})
// 启动服务,监听8080端口
r.Run(":8080")
}
不同限流方案的选型建议
在实际业务中可以根据需求选择合适的限流方案:
- 如果业务场景简单,对限流精度要求不高,优先选择计数器限流,实现成本最低。
- 如果需要处理临界流量问题,同时不需要支持突发流量,可以选择滑动窗口限流。
- 如果业务需要支持一定的突发流量,同时希望流量控制比较灵活,优先选择令牌桶限流,官方扩展库已经提供了稳定实现,直接引入即可使用。
- 如果要求输出的流量必须非常平稳,不允许任何突发流量,可以选择漏桶限流。
分布式场景下的限流注意事项
以上示例都是单机限流的实现,如果服务是分布式部署的多实例架构,单机限流无法控制全局的总请求量,此时需要结合Redis等中间件实现分布式限流。可以将计数、令牌等信息存储在Redis中,通过Redis的原子操作保证多实例之间的限流状态一致,具体实现时需要注意Redis操作的性能,避免限流本身成为服务的性能瓶颈。