令牌桶限流:
加锁限流:
核心思路是通过带缓冲的channel模拟令牌桶,每个空结构体代表一个可用令牌。
初始化时根据设定的最大令牌数(maxTokens)和补充间隔(refillInterval)自动计算出每次应补充的令牌数量,确保每秒补充的令牌总数精确等于最大容量。
限流器通过阻塞(Wait)和非阻塞(Allow)两种获取令牌方式,并通过独立的后台协程定时补充令牌,补充时会动态计算可用空间防止溢出。
// SmoothLimiter 实现平滑的限流控制
type SmoothLimiter struct {
mu sync.Mutex // 互斥锁,保护对共享资源的并发访问
tokenChan chan struct{} // 带缓冲的通道,用于存储令牌(空结构体节省内存)
refillInterval time.Duration // 令牌补充的时间间隔(如10ms)
tokensPerRefill int // 每次补充的令牌数量(根据QPS计算得出)
isActive bool // 控制限流器是否运行的标志位
}
// NewSmoothLimiter 创建新的限流器实例
// maxTokens: 令牌桶最大容量(最大突发请求量)
// refillInterval: 令牌补充间隔,决定限流精度(间隔越小越平滑)
func NewSmoothLimiter(maxTokens int, refillInterval time.Duration) *SmoothLimiter {
// 初始化限流器结构体
limiter := &SmoothLimiter{
mu: sync.Mutex{}, // 初始化互斥锁
tokenChan: make(chan struct{}, maxTokens), // 创建带缓冲的channel,容量为maxTokens
refillInterval: refillInterval, // 设置令牌补充间隔
tokensPerRefill: calculateRefillTokens(maxTokens, refillInterval), // 计算每次补充量
isActive: true, // 默认激活状态
}
// 启动后台goroutine定期补充令牌
go limiter.startRefillRoutine()
return limiter
}
// calculateRefillTokens 计算每次补充的令牌数量
// maxTokens: 桶容量
// interval: 补充间隔
// 返回值: 每次应该补充的令牌数(向上取整)
func calculateRefillTokens(maxTokens int, interval time.Duration) int {
// 计算公式:(maxTokens * interval) / 1秒
// 例如100容量,100ms间隔 => 100*0.1/1 = 10个/次
return int(math.Ceil(float64(maxTokens) * float64(interval) / float64(time.Second)))
}
// Allow 尝试获取令牌(非阻塞方式)
// 返回值: true表示获取成功,false表示令牌不足
func (sl *SmoothLimiter) Allow() bool {
select {
case <-sl.tokenChan: // 尝试从channel读取令牌
return true // 成功获取
default: // channel为空时执行
return false // 获取失败
}
}
// Wait 阻塞等待直到获取令牌
// 无返回值,调用会阻塞直到有可用令牌
func (sl *SmoothLimiter) Wait() {
<-sl.tokenChan // 从channel读取,无令牌时会阻塞
}
// Stop 停止限流器并释放资源
// 安全关闭channel,停止后台goroutine
func (sl *SmoothLimiter) Stop() {
sl.mu.Lock() // 获取锁
defer sl.mu.Unlock() // 确保锁释放
sl.isActive = false // 设置停止标志
close(sl.tokenChan) // 关闭channel(会使得所有等待的Wait()立即返回)
}
// startRefillRoutine 启动后台令牌补充协程
// 内部方法,由NewSmoothLimiter自动调用
func (sl *SmoothLimiter) startRefillRoutine() {
// 初始填充一次令牌
sl.refillTokens()
// 创建定时器,按照设定间隔触发
ticker := time.NewTicker(sl.refillInterval)
defer ticker.Stop() // 确保协程退出时停止ticker
// 主循环
for {
select {
case <-ticker.C: // 定时触发
sl.mu.Lock() // 获取锁检查状态
if !sl.isActive {
sl.mu.Unlock() // 释放锁
return // 退出协程
}
sl.mu.Unlock() // 释放锁
// 执行令牌补充
sl.refillTokens()
}
}
}
// refillTokens 执行实际的令牌补充操作
// 内部方法,持有锁的情况下调用
func (sl *SmoothLimiter) refillTokens() {
sl.mu.Lock() // 获取锁
defer sl.mu.Unlock() // 确保锁释放
// 计算当前可用的令牌槽位
availableSpace := cap(sl.tokenChan) - len(sl.tokenChan)
if availableSpace <= 0 {
return // 桶已满,无需补充
}
// 确定实际补充数量(不能超过可用空间)
refillCount := sl.tokensPerRefill
if refillCount > availableSpace {
refillCount = availableSpace
}
// 批量填充令牌
for i := 0; i < refillCount; i++ {
select {
case sl.tokenChan <- struct{}{}: // 非阻塞写入令牌
default: // 意外情况处理
return // 通常不会执行到这里
}
}
}
核心思路是将时间划分为固定长度的窗口(如1秒),通过互斥锁保护每个窗口期内的请求计数器。
每当新的请求到达时,首先检查当前时间是否超过窗口结束时间:若已超期则重置计数器和时间窗口,若在窗口期内则检查请求数是否已达上限——未超限时计数器递增并放行请求,已超限时根据调用方式选择立即拒绝(Allow)或按剩余时间比例休眠等待(Wait)。
这种实现严格保证任何时间窗口内的请求量都不超过设定阈值,适合需要硬性QPS限制的场景,虽然窗口切换时可能产生轻微的突发流量,但通过调整窗口大小(如改用100ms窗口)可以实现更平滑的控制。
// LockLimiter 基于互斥锁的限流器实现
type LockLimiter struct {
mu sync.Mutex // 保护所有共享变量的互斥锁
count int // 当前窗口内的请求计数
windowSize time.Duration // 时间窗口大小(默认1秒)
windowEnd time.Time // 当前窗口结束时间
maxRequests int // 窗口内最大允许请求数
}
// NewLockLimiter 创建锁式限流器
// maxRequests: 每秒最大请求数
// windowSize: 时间窗口大小(通常1秒)
func NewLockLimiter(maxRequests int, windowSize time.Duration) *LockLimiter {
return &LockLimiter{
windowSize: windowSize,
maxRequests: maxRequests,
windowEnd: time.Now().Add(windowSize), // 初始化窗口结束时间
}
}
// Allow 非阻塞式请求检查
func (ll *LockLimiter) Allow() bool {
ll.mu.Lock()
defer ll.mu.Unlock()
now := time.Now()
// 如果当前时间超过窗口结束时间,重置计数器和窗口
if now.After(ll.windowEnd) {
ll.count = 0
ll.windowEnd = now.Add(ll.windowSize)
}
// 检查是否超过限制
if ll.count >= ll.maxRequests {
return false
}
ll.count++
return true
}
// Wait 阻塞式请求等待
func (ll *LockLimiter) Wait() {
for !ll.Allow() {
// 计算需要等待的时间
remaining := time.Until(ll.windowEnd)
if remaining > 0 {
time.Sleep(remaining / time.Duration(ll.maxRequests-ll.count+1))
}
}
}
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。