1. 限流基础:什么是限流、为什么需要限流、常见限流算法

1.1 什么是限流

限流(Rate Limiting)是一种通过控制单位时间内允许通过的请求数量,来保护系统不被突发流量冲垮的防护手段。它本质上是对系统入口或关键路径施加一个“流量阀门”,确保后端资源(CPU、内存、数据库连接、下游服务)始终工作在安全水位以下。

核心思想:与其让系统在过载时崩溃,不如在入口处主动拒绝一部分请求,保障大部分请求仍能获得正常响应。

1.2 为什么需要限流

在微服务架构和高并发场景下,限流是系统稳定性的第一道防线。以下四个典型场景直接驱动了限流组件的引入:

场景 风险描述 限流作用
突发流量冲击 秒杀、热点事件、爬虫导致流量瞬间飙升 10 倍以上 削峰填谷,防止数据库/缓存被打爆
下游服务脆弱 依赖的第三方 API 或 RPC 服务有 QPS 上限 主动降速,避免触发下游熔断
资源成本控制 云资源按量计费,无限制扩容成本不可控 将流量控制在预算范围内
公平性与隔离 某个恶意租户或异常模块占用全部连接池 按租户/接口独立限流,实现资源隔离

1.3 常见限流算法概述

业界主流限流算法有三种:计数器、漏桶、令牌桶。它们在实现复杂度、流量整形效果和突发处理能力上各有侧重。

1.3.1 计数器算法(固定窗口)

计数器是最直观的限流方式:将时间划分为固定窗口(如 1 秒),每个窗口内维护一个计数器,请求到达时计数器加 1,超过阈值则拒绝。窗口结束时重置计数器。

// 伪代码:固定窗口计数器
var counter int64
var lastResetTime int64 = now()

func TryAcquire(limit int64) bool {
    if now() - lastResetTime >= 1_000_000_000 { // 1秒窗口
        counter = 0
        lastResetTime = now()
    }
    if counter >= limit {
        return false
    }
    atomic.AddInt64(&counter, 1)
    return true
}
边界突刺问题:固定窗口在窗口切换瞬间可能出现两倍阈值的流量通过。例如窗口 0.9s 时计数器满,1.0s 时重置,0.9~1.1s 之间实际通过了 2 倍阈值。滑动窗口算法可缓解此问题,但精度与存储成本成正比。

1.3.2 漏桶算法(Leaky Bucket)

漏桶将请求比作水滴,桶以固定速率“漏水”(处理请求),桶容量有限。当桶满时,新请求被丢弃。漏桶强制输出速率恒定,适合需要严格平滑流量的场景。

// 伪代码:漏桶算法
type LeakyBucket struct {
    rate       float64 // 每秒处理速率
    capacity   float64 // 桶容量
    water      float64 // 当前水量
    lastLeakMs int64   // 上次漏水时间
}

func (lb *LeakyBucket) TryAcquire() bool {
    nowMs := now()
    elapsed := float64(nowMs - lb.lastLeakMs) / 1000.0
    lb.water = max(0, lb.water - elapsed * lb.rate) // 先漏水
    lb.lastLeakMs = nowMs
    if lb.water < lb.capacity {
        lb.water += 1.0
        return true
    }
    return false
}

1.3.3 令牌桶算法(Token Bucket)

令牌桶以固定速率向桶中放入令牌,请求到达时需获取一个令牌才能通过。桶有最大容量,允许一定程度的突发流量(桶中积攒的令牌可被瞬间取走)。这是 Go 标准库 golang.org/x/time/rate 采用的算法,也是本课程压测的核心对象。

// 伪代码:令牌桶算法
type TokenBucket struct {
    rate       float64 // 令牌放入速率
    capacity   float64 // 桶容量
    tokens     float64 // 当前令牌数
    lastFillMs int64   // 上次填充时间
}

func (tb *TokenBucket) TryAcquire() bool {
    nowMs := now()
    elapsed := float64(nowMs - tb.lastFillMs) / 1000.0
    tb.tokens = min(tb.capacity, tb.tokens + elapsed * tb.rate) // 先填充令牌
    tb.lastFillMs = nowMs
    if tb.tokens >= 1.0 {
        tb.tokens -= 1.0
        return true
    }
    return false
}

1.3.4 三种算法对比

维度 计数器(固定窗口) 漏桶 令牌桶
输出速率 不保证平滑 严格恒定 平均恒定,允许突发
突发处理 窗口内可突发 不支持突发 支持突发(桶容量内)
实现复杂度 低 中 中
内存开销 低(1 个计数器) 低(2 个变量) 低(2 个变量)
适用场景 简单限流,可接受边界突刺 流量整形,数据库写入限流 通用限流,API 网关,微服务

1.4 三种算法核心流程对比

下图从“请求到达 → 判断是否通过”的视角,直观展示三种算法的决策路径差异:

三种限流算法核心流程对比 计数器(固定窗口) 判断:当前窗口计数 < 阈值? 通过 +1 拒绝 漏桶 判断:当前水量 < 桶容量? 入桶 +1 丢弃 令牌桶 判断:桶中令牌 >= 1? 取令牌通过 等待/拒绝 关键差异总结 • 计数器:时间窗口边界重置,实现简单但存在边界突刺问题。 • 漏桶:强制恒定输出速率,适合需要严格削峰填谷的场景,但不支持突发。 • 令牌桶:允许一定程度的突发流量(桶容量内),是 Go 标准库 rate 组件的底层算法。

1.5 限流在 Go 生态中的位置