1. 流控基础概念:流量控制、为什么需要、常见算法

各位好,我是老张。今天咱们聊聊流控最基础的东西。你可能会觉得「基础」嘛,翻翻文档就懂了。但我带过不少团队,发现很多人栽跟头恰恰是在这些「基础」上。所以这一章,咱们把地基打扎实。

1.1 什么是流量控制

流量控制,说白了就是限制单位时间内进入系统的请求量。就像你进地铁站,闸机一次只放一个人,不会让所有人一窝蜂冲进去。

我习惯把流控比作水龙头:后端服务是水池,请求就是水流。水太大,池子会溢;水太小,池子又空着浪费。流控就是那个恰到好处的阀门。

核心定义:流量控制是一种通过限制请求速率、队列深度或并发数,来保护系统不被突发流量冲垮的机制。

举个例子。我几年前维护过一个电商订单系统。平时每秒几百单,双十一峰值能到每秒几万单。如果没有流控,数据库连接池瞬间被打满,整个服务就挂了。嗯,那场面,运维群里全是告警。

1.2 为什么需要流控

你想想看,一个系统能承载的容量是有上限的。CPU、内存、数据库连接、网络带宽,哪个都不是无限的。流控要解决的就是三个问题:

  • 防止雪崩:一个服务挂了,上游重试,下游压力更大,最后整条链路全挂。流控能在源头掐住。
  • 保证公平:不能让某个用户或某个接口把资源全占了。我见过一个案例,某个爬虫客户端把整个 API 网关打满,正常用户全被拒绝。
  • 削峰填谷:流量有波峰波谷。流控可以把波峰的请求排队,在波谷慢慢消化。就像水库蓄水一样。

注意:流控不是万能的。它只能缓解瞬时压力,不能解决容量不足的根本问题。如果长期超过系统上限,该扩容还是要扩容。

1.3 常见流控算法

算法这块,我见过很多新手一上来就背概念,结果面试过了,线上出问题却不知道怎么调。咱们换个思路,先理解每种算法解决了什么问题,再记公式。

1.3.1 漏桶算法

漏桶算法,形象点说:你有一个桶,底部有个小洞。水(请求)以恒定速率从洞里漏出。不管上面倒水多快,下面出水永远是匀速的。

它的核心思想是强制限速。桶满了,新来的请求直接丢弃。

// 漏桶算法伪代码
class LeakyBucket {
    capacity    // 桶容量
    rate        // 漏水速率(请求/秒)
    water       // 当前水量
    lastTime    // 上次漏水时间

    function allowRequest():
        now = currentTime()
        // 先漏水
        leaked = (now - lastTime) * rate
        water = max(0, water - leaked)
        lastTime = now

        if water < capacity:
            water++
            return true   // 放行
        else:
            return false  // 拒绝
}

我在项目中用过漏桶处理日志上报。日志量忽大忽小,但后端写入 Elasticsearch 的速率必须稳定。漏桶正好合适——不管前端怎么狂刷日志,后端始终匀速写入。

适用场景:需要严格平滑流量、保护下游系统不被突发流量冲击的场景。比如数据库写入、消息队列生产端。

1.3.2 令牌桶算法

令牌桶跟漏桶正好相反。它允许一定程度的突发。怎么做的呢?桶里放令牌,以固定速率生成。请求来了,拿一个令牌就走。桶里有令牌就能走,没有就等或者拒绝。

关键区别:令牌桶允许短时间内的突发流量。因为桶里可以攒令牌,攒多了就能一次性放行多个请求。

// 令牌桶算法伪代码
class TokenBucket {
    capacity    // 桶容量(最大令牌数)
    rate        // 令牌生成速率(个/秒)
    tokens      // 当前令牌数
    lastTime    // 上次生成令牌时间

    function allowRequest():
        now = currentTime()
        // 先补充令牌
        newTokens = (now - lastTime) * rate
        tokens = min(capacity, tokens + newTokens)
        lastTime = now

        if tokens >= 1:
            tokens--
            return true   // 放行
        else:
            return false  // 拒绝
}

我个人习惯用令牌桶做 API 网关限流。比如某个接口限制每秒 100 次,但允许瞬间 200 次的突发。令牌桶的 capacity 设成 200,rate 设成 100,就能实现「平均 100,峰值 200」的效果。

漏桶 vs 令牌桶:漏桶强制匀速,适合保护下游;令牌桶允许突发,适合应对用户行为的不确定性。选哪个,取决于你的业务容忍度。

1.3.3 滑动窗口算法

滑动窗口,你可以理解成一个会移动的时间段。比如限制 1 分钟内最多 100 个请求。传统做法是固定每分钟重置一次计数器。但问题来了:如果用户在 59 秒时发了 100 个请求,下一秒又发了 100 个,实际上 2 秒内就发了 200 个,突破了限制。

滑动窗口解决了这个问题。它把时间切成小格子(比如 1 秒一格),窗口随着时间平滑移动。每个格子独立计数,窗口内的总请求数就是所有格子之和。

// 滑动窗口算法伪代码
class SlidingWindow {
    windowSize  // 窗口大小(秒)
    bucketCount // 格子数
    buckets     // 格子数组,每个格子记录请求数
    currentTime // 当前时间

    function allowRequest():
        now = currentTime()
        // 找到当前格子
        bucketIndex = (now / windowSize) % bucketCount
        // 如果格子过期,重置
        if buckets[bucketIndex].time != now:
            buckets[bucketIndex].reset()
        buckets[bucketIndex].count++

        // 计算窗口内总请求数
        total = sum(buckets)
        if total <= limit:
            return true
        else:
            return false
}

我曾经踩过一个坑。用固定窗口限流,结果某个用户卡着整点边界刷接口,把我们的优惠券库存刷走了大半。后来换成滑动窗口,这种边界攻击就防住了。

注意:滑动窗口的格子数越多,精度越高,但内存消耗也越大。一般 10~100 个格子就够用了。别为了追求完美把内存打满。

1.4 三种算法对比

我把它们放在一张表里,方便你对照选型:

算法 核心特点 是否允许突发 典型场景 我踩过的坑
漏桶 强制匀速输出 否 日志写入、消息生产 突发流量全丢,业务方投诉
令牌桶 允许短时突发 是(受桶容量限制) API 网关、接口限流 令牌生成速率没算对,导致限流过严
滑动窗口 精确控制时间窗口 否(窗口内总量限制) 防刷、秒杀、优惠券 格子数设太多,内存暴涨

1.5 知识体系总览

下面这张图,把本章的核心逻辑串起来了。你可以把它当作流控的「地图」:

流控基础概念知识体系 流量控制 什么是流控 为什么需要流控 常见流控算法 防止雪崩 保证公平 削峰填谷 漏桶算法 令牌桶算法 滑动窗口算法 选型建议:保护下游用漏桶,允许突发用令牌桶,防边界攻击用滑动窗口

这张图把本章的三个核心问题串在了一起。你从上往下看:先搞清楚「什么是流控」,再理解「为什么需要」,最后落到「怎么实现」。后面的章节,我们会基于这些算法,搭建真正的调试工具链。

我的建议:别急着背代码。先想清楚你的业务场景——是保护数据库?还是防刷?还是平滑流量?场景定了,算法自然就选出来了。