system column十三Tech
← 返回技术专栏
TECH

Go 高并发限流实战:四种经典算法的原理与代码实现

限流是保障服务稳定性的核心手段。本文详解令牌桶、漏桶、计数器、滑动窗口四种限流算法的原理,并给出完整的 Go 代码实现与选型建议。

Go性能优化

当流量洪峰突然袭来时,你的服务能否稳如泰山?在十三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 的架构设计中,我们通常会在网关层、服务层、数据库访问层分别部署不同策略的限流器,形成多层防御体系。理解这些算法的本质,能帮助你在面对不同的流量特征时做出正确的技术选型。