在API服务开发中,当短时间内涌入大量请求时,很容易导致服务资源耗尽、响应变慢甚至崩溃,因此实现基础的请求频率控制是非常有必要的。Golang作为常用的后端开发语言,有多种方式可以实现API限流功能,其中令牌桶算法是应用比较广泛的一种方案。

常见限流算法简介
在实现限流之前,先了解几种常见的限流算法,方便选择适合自己业务的方案:
- 固定窗口算法:将时间划分为固定长度的窗口,统计窗口内的请求数量,超过阈值就拒绝请求,实现简单但存在窗口边界突刺问题。
- 滑动窗口算法:在固定窗口基础上细分时间片,统计更精准的时间段内请求数,解决了边界突刺问题,但实现复杂度稍高。
- 令牌桶算法:系统以固定速率向桶中放入令牌,请求到来时先从桶中取令牌,取到则处理,取不到则拒绝,允许一定程度的突发请求,适合大多数API场景。
- 漏桶算法:请求像水一样进入漏桶,漏桶以固定速率流出请求处理,超出桶容量的请求会被丢弃,流量输出非常平稳,不适合突发请求场景。
基于令牌桶算法实现Golang限流
令牌桶算法的核心逻辑是维护一个令牌桶,有两个关键参数:桶容量和令牌生成速率。下面我们手动实现一个简单的令牌桶限流器,不依赖第三方库,方便理解原理。
令牌桶结构体定义
首先定义令牌桶的结构体,包含桶容量、当前令牌数、令牌生成速率、上次更新时间等字段:
package main
import (
"sync"
"time"
)
// TokenBucket 令牌桶结构体
type TokenBucket struct {
capacity int // 桶的最大容量
rate int // 每秒生成的令牌数
currentToken int // 当前剩余的令牌数
lastUpdate time.Time // 上次更新令牌的时间
mu sync.Mutex // 互斥锁,保证并发安全
}
// NewTokenBucket 创建新的令牌桶实例
func NewTokenBucket(capacity int, rate int) *TokenBucket {
return &TokenBucket{
capacity: capacity,
rate: rate,
currentToken: capacity, // 初始化时桶是满的
lastUpdate: time.Now(),
}
}
令牌补充逻辑实现
每次请求到来时,需要先根据时间差补充令牌,再判断是否有足够的令牌处理请求:
// replenish 补充令牌
func (tb *TokenBucket) replenish() {
tb.mu.Lock()
defer tb.mu.Unlock()
now := time.Now()
// 计算距离上次更新的时间差(秒)
elapsed := now.Sub(tb.lastUpdate).Seconds()
// 计算这段时间内应该生成的令牌数
addToken := int(elapsed * float64(tb.rate))
if addToken > 0 {
// 令牌数不能超过桶容量
tb.currentToken = min(tb.currentToken+addToken, tb.capacity)
tb.lastUpdate = now
}
}
// min 辅助函数,返回两个整数中的较小值
func min(a, b int) int {
if a < b {
return a
}
return b
}
限流判断逻辑实现
实现获取令牌的方法,返回布尔值表示是否允许当前请求通过:
// Allow 判断当前请求是否允许通过
func (tb *TokenBucket) Allow() bool {
tb.replenish()
tb.mu.Lock()
defer tb.mu.Unlock()
if tb.currentToken > 0 {
tb.currentToken--
return true
}
return false
}
在API中接入限流功能
接下来我们把上面实现的令牌桶限流器接入到Golang的HTTP API中,模拟一个基础的接口限流场景。
API接口与限流中间件
我们创建一个简单的HTTP服务,包含一个测试接口,用限流中间件包裹接口处理逻辑:
package main
import (
"fmt"
"net/http"
)
func main() {
// 创建令牌桶,容量为10,每秒生成2个令牌
bucket := NewTokenBucket(10, 2)
// 测试接口处理函数
testHandler := func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "请求处理成功,时间:%v", time.Now().Format("2006-01-02 15:04:05"))
}
// 限流中间件
limitMiddleware := func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if bucket.Allow() {
next(w, r)
} else {
w.WriteHeader(http.StatusTooManyRequests)
fmt.Fprintf(w, "请求过于频繁,请稍后再试")
}
}
}
// 注册路由,接入限流中间件
http.HandleFunc("/test", limitMiddleware(testHandler))
fmt.Println("服务启动在 :8080 端口")
http.ListenAndServe(":8080", nil)
}
测试限流效果
启动服务后,我们可以用压测工具或者循环请求测试接口,观察限流效果:
// 测试代码,模拟10次并发请求
package main
import (
"fmt"
"net/http"
"sync"
)
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
resp, err := http.Get("http://127.0.0.1:8080/test")
if err != nil {
fmt.Printf("请求%d失败:%vn", idx, err)
return
}
defer resp.Body.Close()
fmt.Printf("请求%d,状态码:%dn", idx, resp.StatusCode)
}(i)
}
wg.Wait()
}
运行测试代码后,会发现部分请求返回200状态码,部分返回429状态码,说明限流逻辑已经生效。初始桶容量为10,所以前10个请求会直接通过,后续请求需要等待令牌生成,每秒最多处理2个请求。
限流优化与注意事项
实际业务中使用的限流还需要考虑更多场景:
- 如果是分布式服务,单机限流无法满足需求,需要使用Redis等中间件实现分布式令牌桶,保证多个服务实例的限流统一。
- 可以根据不同的接口设置不同的限流规则,比如核心接口限流阈值高,非核心接口限流阈值低。
- 限流触发后可以返回更友好的提示,或者引导用户稍后重试,避免影响用户体验。
- 高并发场景下要做好锁的优化,避免限流逻辑成为性能瓶颈,也可以使用Golang标准库中的
golang.org/x/time/rate包,它已经实现了高效的令牌桶限流,生产环境可以直接使用。
以上就是Golang中实现基础API限流功能的完整过程,开发者可以根据自身业务需求调整令牌桶的参数,或者选择其他适合的限流算法,保障API服务的稳定运行。