3. Go 标准库实现计数器限流:基于 sync/atomic 与 time.Ticker 的固定窗口计数器实现

固定窗口计数器是限流算法中最直观、最容易实现的一种。其核心思想是:将时间划分为固定的窗口(例如 1 秒),每个窗口内维护一个计数器,当请求到达时计数器加 1,若计数器超过预设阈值则拒绝请求,窗口结束时计数器归零。

本章将使用 Go 标准库中的 sync/atomic 和 time.Ticker,从零构建一个线程安全、高性能的固定窗口计数器限流器。这种实现方式不依赖第三方库,适合对依赖管理有严格要求的项目。

3.1 固定窗口计数器原理

固定窗口计数器的工作流程可以概括为以下步骤:

  1. 设定一个时间窗口长度 window(如 1 秒)和一个最大请求数 limit(如 100)。
  2. 在每个窗口开始时,将计数器重置为 0。
  3. 每次请求到达时,原子性地将计数器加 1。
  4. 如果加 1 后的值超过 limit,则拒绝该请求;否则放行。
  5. 窗口结束时,重复步骤 2。

关键特性: 固定窗口算法实现简单、性能极高(仅需一次原子操作),但存在“窗口临界突发”问题——在窗口切换的瞬间,可能允许两倍于阈值的请求通过。后续章节会介绍滑动窗口算法来解决此问题。

3.2 架构设计

下图展示了固定窗口计数器的核心组件与数据流:

固定窗口计数器架构图 固定窗口计数器限流器架构 HTTP 请求入口 FixedWindowLimiter 限流器核心结构体 计数器 (atomic.Int64) 原子操作,无锁并发 定时器 (time.Ticker) 窗口到期自动重置 放行 / 拒绝决策 计数器原子递增 → 与阈值比较 → 返回结果;定时器到期自动重置计数器

3.3 核心数据结构

我们定义 FixedWindowLimiter 结构体,包含以下字段:

字段 类型 说明
limit int64 窗口内允许的最大请求数
counter atomic.Int64 当前窗口的请求计数器(Go 1.19+ 推荐)
ticker *time.Ticker 定时器,每个窗口到期触发重置
stopCh chan struct{} 停止信号,用于优雅关闭

版本提示: Go 1.19 引入了 atomic.Int64 等类型安全原子操作,推荐在新项目中使用。若兼容旧版本,可使用 sync/atomic 包中的 LoadInt64/AddInt64 函数。

3.4 完整实现代码

以下为固定窗口计数器的完整实现,包含构造函数、请求判断方法和优雅关闭:

package limiter

import (
    "sync/atomic"
    "time"
)

// FixedWindowLimiter 固定窗口计数器限流器
type FixedWindowLimiter struct {
    limit   int64           // 窗口内最大请求数
    counter atomic.Int64    // 当前窗口计数器
    ticker  *time.Ticker    // 窗口定时器
    stopCh  chan struct{}   // 停止信号
}

// NewFixedWindowLimiter 创建固定窗口限流器
// window: 时间窗口长度,如 time.Second
// limit: 窗口内允许的最大请求数
func NewFixedWindowLimiter(window time.Duration, limit int64) *FixedWindowLimiter {
    l := &FixedWindowLimiter{
        limit:  limit,
        ticker: time.NewTicker(window),
        stopCh: make(chan struct{}),
    }

    // 启动后台 goroutine,每个窗口到期重置计数器
    go func() {
        for {
            select {
            case <-l.ticker.C:
                // 窗口到期,原子重置计数器为 0
                l.counter.Store(0)
            case <-l.stopCh:
                l.ticker.Stop()
                return
            }
        }
    }()

    return l
}

// Allow 判断当前请求是否允许通过
// 返回值 true 表示放行,false 表示拒绝
func (l *FixedWindowLimiter) Allow() bool {
    // 原子递增计数器,并获取递增后的值
    current := l.counter.Add(1)
    // 如果超过限制,则拒绝
    if current > l.limit {
        return false
    }
    return true
}

// Stop 停止限流器,释放资源
func (l *FixedWindowLimiter) Stop() {
    close(l.stopCh)
}

3.5 使用示例

以下代码演示了如何在 HTTP 处理器中使用该限流器:

package main

import (
    "fmt"
    "net/http"
    "time"
    "yourproject/limiter"
)

func main() {
    // 创建限流器:每秒最多允许 10 个请求
    l := limiter.NewFixedWindowLimiter(time.Second, 10)
    defer l.Stop()

    http.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
        if !l.Allow() {
            http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
            return
        }
        fmt.Fprintln(w, "请求成功")
    })

    http.ListenAndServe(":8080", nil)
}

3.6 关键实现细节

  • 原子操作保证线程安全: counter.Add(1) 是原子操作,多个 goroutine 并发调用时不会出现数据竞争,无需加锁。
  • 后台 goroutine 重置计数器: 定时器到期后通过 Store(0) 原子重置,避免在请求路径上执行重置逻辑。
  • 优雅关闭: 通过 stopCh 通知后台 goroutine 退出,防止 goroutine 泄漏。
  • 边界判断: 使用 > limit 而非 >= limit,确保第 limit 个请求能够通过。

3.7 性能与局限性分析

维度 评价
性能 极高。每次判断仅需一次原子加法操作,无锁、无内存分配,单机可达百万 QPS
内存开销 极低。仅需几个 int64 和 channel,几乎可以忽略
实现复杂度 低。核心代码不到 50 行,易于理解和维护
临界突发问题 存在。窗口切换瞬间可能允许 2 倍流量通过,对突发敏感的业务不适用
适用场景 对突发不敏感、追求极致性能的场景,如日志上报、非关键统计接口

避坑指南: 不要在生产环境对核心业务接口直接使用固定窗口计数器。临界突发问题可能导致瞬间流量尖峰击穿下游服务。后续章节将介绍滑动窗口和令牌桶算法来弥补这一缺陷。

3.8 与 sync.Mutex 实现的对比

如果不使用 sync/atomic,也可以使用 sync.Mutex 加锁实现计数器。以下是两种方式的对比:

对比项 sync/atomic 实现 sync.Mutex 实现
性能 纳秒级,无锁竞争 微秒级,存在锁竞争和上下文切换
代码复杂度 低,无需显式加解锁 中等,需要小心 Lock/Unlock 配对
可扩展性 高,原子操作在大量并发下表现稳定 低,高并发下锁成为瓶颈
推荐度 ⭐⭐⭐⭐⭐ ⭐⭐

在高并发限流场景下,sync/atomic 是更优的选择。它利用 CPU 提供的原子指令,避免了操作系统级别的锁调度开销。

3.9 小结