限流算法完全指南:从一次"促销把数据库打挂"看懂固定窗口、滑动窗口、令牌桶

2022 年我负责一个商品详情接口,平时扛得很轻松。直到一次运营促销,流量几十倍涌入,响应时间从几十毫秒飙到几秒,大面积超时,整个服务雪崩——不只是这个接口,同服务里别的功能全挂了。问题不是流量本身,是我的服务对"一瞬间涌进多少请求"毫无防备。我加了个固定窗口计数器每秒限 1000,本地完美,下一次促销系统还是被打出问题。我一直以为限流就是数个数够了就拒绝,真相是限流是一整套关于时间和速率的算法。本文把限流从头梳理。为什么需要:任何系统的连接、线程、下游配额都有物理上限,流量越线后是雪崩式崩溃,限流是"拒绝一部分换保住全部"的主动取舍。固定窗口:时间切成定长窗口逐窗计数,最简单,但两窗交界处会放行近两倍限额,下游被这个边界突刺打垮。滑动窗口:窗口永远是此刻往前推固定一段——日志实现记每个请求时间戳精度最高但内存开销大,计数器实现只存两个窗口计数按重叠比例加权估算,抹平突刺、是工业界最常用折中。漏桶:请求以恒定速率流出、输出绝对平滑,但完全不允许突发。令牌桶:按速率攒令牌、请求要取令牌才放行,refill_rate 控均值、capacity 控突发上限,贴合真实流量、应用最广。工程坑:进程内限流器在多实例下会被实例数放大十倍,状态必须放共享 Redis、取令牌用 Lua 脚本保证原子;限流维度按用户/IP/接口构造 key,太粗误伤、太细限不住;被限流要返回 429 带 Retry-After 让客户端退避重试。核心一句:限流不是计数问题,是时间和速率问题——每种算法都在用不同方式定义"超速",看清它会在哪里漏,才选得对。

2022 年我负责一个商品详情接口。平时流量平稳,这个接口扛得很轻松。直到一次运营搞促销,活动开始的那一瞬间,流量几十倍地涌了进来。我盯着监控,看着接口的响应时间从几十毫秒一路飙到几秒,然后是大面积超时,最后整个服务挂了——不只是这一个接口,连带着同一个服务里别的功能也全部不可用。复盘时我才看清:促销的流量本身不是错,错的是我的服务对"一瞬间会涌进来多少请求"毫无防备。每个请求进来都老老实实地去查数据库,几万个请求同时砸过来,数据库连接池一瞬间被占满,后面的请求开始排队、等待、超时,工作线程一个接一个被拖死,服务雪崩。我第一反应是加机器,可运营的促销说来就来,我不可能为了几分钟的峰值常年养着十倍的机器。正确的思路是限流:在请求真正打到数据库之前,先挡住超过系统承载能力的那一部分。于是我加了一个最朴素的"固定窗口计数器"——每秒最多放 1000 个请求进来。本地一测,完美。可下一次促销,系统还是在某个瞬间被打出了问题。我又懵了:我明明限了每秒 1000,怎么还会被冲垮?盯着日志看了很久才想明白:固定窗口在两个窗口的交界处,会放进将近两倍的请求。我一直以为"限流就是数个数,够了就拒绝",而真相是——限流是一整套关于时间和速率的算法,固定窗口、滑动窗口、漏桶、令牌桶,每一种都在用不同的方式回答同一个问题:怎样才算"超速"。算法选错了,你的限流器在 Demo 里看着稳如泰山,在真实的流量尖峰面前却照样会漏。那次之后我才认真把限流这件事从头搞明白。这篇文章就把它梳理一遍:为什么需要限流、固定窗口错在哪、滑动窗口怎么补、漏桶和令牌桶差在哪,以及把限流真正做进生产要避开的那些坑。

问题背景

先把那次事故的现象和我的误判讲清楚,后面所有的设计都是冲着纠正这个误判去的。

现象:促销活动带来几十倍的流量尖峰,没有限流时,请求直接打满数据库连接池,引发请求排队、超时、线程耗尽,整个服务雪崩。加了"固定窗口每秒 1000"之后,系统仍在某个瞬间被冲垮。

我当时的错误认知:"限流就是数个数——开一个计数器,每来一个请求加一,到了上限就拒绝,过一秒清零重来,就这么简单。"

真相:限流真正的难点,根本不在"数个数"那一下,而在"怎么数":时间窗口怎么划分、速率怎么定义、要不要允许突发。固定窗口实现最简单,但它在窗口边界会放进近两倍流量;滑动窗口消除了边界突刺,但两种实现各有内存与精度的代价;漏桶强制请求匀速流出;令牌桶则在限速的同时允许短暂突发。再加上分布式部署下,计数器还必须放进所有实例共享的存储里。选错算法、或在分布式下用了进程内计数器,你的限流器就会在某个并发的瞬间悄悄失效。

要把限流做稳,需要几块认知:

  • 为什么没有限流的系统会被流量尖峰直接打垮;
  • 固定窗口计数器为什么会在边界放进两倍流量;
  • 滑动窗口怎么消除边界突刺,它的两种实现各有什么代价;
  • 漏桶和令牌桶的根本区别——一个强制匀速,一个允许突发;
  • 分布式限流、限流维度、被限流后的响应这些工程坑怎么处理。

一、为什么需要限流:系统的承载上限是真实存在的

先想清楚一件事:任何系统的处理能力都有一个上限,这个上限是物理存在的、绕不过去的。

你的服务能开多少工作线程、数据库连接池有多少个连接、下游接口每秒能扛多少调用——这些都是有限的数字。平时流量在上限以下,系统跑得好好的,你甚至感觉不到上限的存在。可一旦流量越过这条线,情况不是"变慢一点"那么温和:请求开始抢占有限的连接和线程,抢不到的就排队,排队又进一步拉长每个请求的耗时,耗时拉长导致连接占用更久、释放更慢,于是队列越堆越长……这是一个正反馈,它不会停在某个"慢但能用"的稳态,而是一路滑向彻底崩溃。下面这个接口,就是完全没有这条防线的样子:

from fastapi import FastAPI

app = FastAPI()


@app.get("/product/{pid}")
def get_product(pid: int):
    # 反面教材:接口对"一瞬间涌进来多少请求"毫无防备。
    # 每个请求进来都直接去查数据库 —— 平时流量低时毫无问题,
    # 可一旦促销流量几十倍涌入,数据库连接池瞬间被占满,
    # 后面的请求排队、超时,工作线程被一个个拖死,服务雪崩。
    return query_db("SELECT * FROM product WHERE id = %s", pid)

限流要做的事,本质上很简单:给系统画一条线,守住它。在请求真正消耗那些有限资源(连接、线程、下游配额)之前,先判断当前的请求速率有没有越线;越线了,就把多出来的请求快速拒绝掉,而不是放它们进来一起排队等死。

这里有个反直觉但极其重要的点:限流的目的不是让所有请求都成功,而是在过载时保住系统不崩。被限流拒绝的那部分请求,确实失败了——但代价是清晰的、可控的:一部分人收到"请稍后重试",而系统整体still活着,另一大部分人的请求正常完成。反过来,如果不限流,放任所有请求涌进来,结局是所有人的请求都因为雪崩而失败。限流是一种主动的、有损的取舍:用"拒绝一部分"换"保住全部"。想清楚这个取舍,后面那些算法才有意义——它们争论的,无非是"怎样判断越线"才最准。

二、固定窗口计数器:最简单,也最容易被击穿

最直观的限流算法,就是我当初写的那个:把时间切成一段段固定长度的"窗口"(比如每 1 秒一个窗口),给每个窗口配一个计数器,请求来了就加一,超过上限就拒绝,进入下一个窗口时计数清零。

import time


class FixedWindowLimiter:
    """固定窗口计数器:把时间切成定长窗口,每个窗口内独立计数。"""

    def __init__(self, limit: int, window: float):
        self.limit = limit            # 每个窗口最多放行多少请求
        self.window = window          # 窗口长度(秒)
        self.count = 0
        self.window_start = time.time()

    def allow(self) -> bool:
        now = time.time()
        # 已经迈入下一个窗口 —— 计数清零,一切重新开始
        if now - self.window_start >= self.window:
            self.count = 0
            self.window_start = now
        if self.count < self.limit:
            self.count += 1
            return True
        return False

这段代码逻辑清晰、实现简单,平时跑也确实有效。但它藏着一个致命的边界问题,正是它让我第二次促销又翻了车。

假设限额是"每秒 1000"。窗口在每个整秒清零。现在设想这样一种流量分布:有 1000 个请求集中在第一个窗口的最后半秒(比如 0.5 秒到 1.0 秒)到达,全部放行;紧接着又有 1000 个请求集中在第二个窗口的前半秒(1.0 秒到 1.5 秒)到达,因为窗口刚清零,也全部放行。于是在 0.5 秒到 1.5 秒这横跨边界的整整 1 秒之内,系统实际放行了 2000 个请求——是你设定的限额的两倍。你的下游,正是被这"任意 1 秒内的真实峰值"打垮的,而固定窗口计数器对此一无所知,它只会在每个整秒骄傲地汇报"本窗口放行 1000,没超"。

这个边界突刺的根源是:固定窗口把时间切成了互不相干的段,它只统计"某一个窗口内"的量,却管不住"任意一个滑动的 1 秒内"的量——而下游承受的,恰恰是后者。要堵上这个洞,就得让"窗口"本身能跟着当前时刻平滑地移动。

三、滑动窗口:把窗口做得更平滑

滑动窗口的核心思想,是让统计的窗口永远是"此刻往前回看固定一段时间",而不是被切死在整秒上。它有两种典型实现,精度和成本各不相同。

第一种是滑动窗口日志:把每一个放行请求的时间戳都记下来。每来一个新请求,先把"已经超出回看范围"的旧时间戳全部丢掉,再看剩下的数量有没有到上限。它统计的就是不折不扣的"最近 window 秒内的真实请求数",精度最高:

import collections
import time


class SlidingWindowLog:
    """滑动窗口日志:记下每个请求的时间戳,窗口永远是此刻往前推 window 秒。"""

    def __init__(self, limit: int, window: float):
        self.limit = limit
        self.window = window
        self.timestamps = collections.deque()

    def allow(self) -> bool:
        now = time.time()
        # 把窗口左边界之外(太旧)的时间戳从队头全部清掉
        while self.timestamps and now - self.timestamps[0] >= self.window:
            self.timestamps.popleft()
        # 剩下的就是"最近 window 秒内"的真实请求数
        if len(self.timestamps) < self.limit:
            self.timestamps.append(now)
            return True
        return False

滑动窗口日志精确,但它有个代价你必须看见:它要为每一个放行的请求存一个时间戳。如果限额是"每秒十万",这个 deque 里随时躺着十万个时间戳,内存开销和清理开销都不小。在超高 QPS 的场景下,这个代价会变得难以接受。

于是有了第二种实现——滑动窗口计数器。它是一个聪明的折中:不存每个请求的时间戳,只保留当前窗口上一个窗口两个计数;判断时,根据当前时刻在窗口里的位置,按重叠比例去加权估算"最近一个窗口"的请求量:

import time


class SlidingWindowCounter:
    """滑动窗口计数器:只存当前和上一个窗口的计数,按重叠比例加权估算。"""

    def __init__(self, limit: int, window: float):
        self.limit = limit
        self.window = window
        self.prev_count = 0
        self.curr_count = 0
        self.curr_start = time.time()

    def allow(self) -> bool:
        now = time.time()
        elapsed = now - self.curr_start
        if elapsed >= self.window:
            # 滚动一格:相邻则当前窗口降级为"上一个",否则太久远直接清零
            self.prev_count = self.curr_count if elapsed < 2 * self.window else 0
            self.curr_count = 0
            self.curr_start = now
            elapsed = 0.0
        # 上一个窗口还有多大比例"压"在当前的滑动窗口里
        ratio = 1 - elapsed / self.window
        estimated = self.prev_count * ratio + self.curr_count
        if estimated < self.limit:
            self.curr_count += 1
            return True
        return False

这个加权估算的妙处在于:窗口刚滚动时,上一个窗口的计数几乎全额计入(ratio 接近 1),随着时间推进它的权重平滑地衰减到 0。固定窗口那个"边界一过、上一秒的量瞬间归零"的突变被抹平了,所以边界突刺自然就没了。它的代价是结果是个估算值——它假设请求在窗口内均匀分布,这个假设不总成立,所以会有微小误差。但用两个整数的存储换掉十万个时间戳,再换来一个没有突刺的限流曲线,这个折中在绝大多数生产场景里都非常划算,它也是工业界用得最多的限流算法之一。

到这里,"窗口"这条路已经走得比较完整了。但窗口类算法有一个共同的特点:它们只回答"过去这段时间放行了多少",并不主动控制请求流出的节奏。下面两种算法,换了一个完全不同的视角。

四、漏桶:以恒定速率处理请求

漏桶算法换了个思路:它不去数"过去放行了多少",而是关心"请求流出去的节奏"。

想象一个底部有个小孔的水桶。请求就是往桶里倒的水,桶底以一个恒定的速率漏水——漏出去的水,就是被真正处理的请求。桶有一个固定的容量:水没满,新请求就能倒进来排队;水满了,新倒进来的水直接溢出,也就是请求被拒绝。它的关键特性是:无论你往桶里倒水的速度多猛多不均匀,桶底漏水的速率永远是恒定的

import time


class LeakyBucket:
    """漏桶:请求先进桶,桶以恒定速率漏水(被处理);桶满则拒绝。"""

    def __init__(self, capacity: int, leak_rate: float):
        self.capacity = capacity        # 桶容量:最多积压多少个请求
        self.leak_rate = leak_rate      # 漏水速率:每秒处理(漏出)多少请求
        self.water = 0.0                # 当前桶里的水量(积压的请求数)
        self.last_leak = time.time()

    def allow(self) -> bool:
        now = time.time()
        # 先按流逝的时间,漏掉这段时间本该漏出的水
        leaked = (now - self.last_leak) * self.leak_rate
        self.water = max(0.0, self.water - leaked)
        self.last_leak = now
        # 桶还装得下这一滴,就放进来;装不下就溢出(拒绝)
        if self.water + 1 <= self.capacity:
            self.water += 1
            return True
        return False

漏桶的优点是输出绝对平滑。无论流量怎么尖峰毛刺,经过漏桶整流之后,打到下游的请求速率永远是那个恒定的 leak_rate。如果你的下游极其脆弱、对速率波动零容忍(比如一个老旧的、严格按 QPS 收费的第三方接口),漏桶是最稳的选择。

但漏桶的优点反过来也是它的缺点:它不允许任何突发。哪怕系统此刻明明很空闲、完全有余力一口气多处理一些请求,漏桶也铁面无私地按恒定速率慢慢漏。对很多业务来说这是一种浪费——真实世界的流量本来就是一阵一阵的,用户也期望"平时点一下立刻就有响应"。我们往往希望:平均速率受控,但允许在系统有余力时短暂地爆发一下。这正是令牌桶要解决的。

五、令牌桶:既能限流,又允许突发

令牌桶把漏桶的桶"反过来用了"。漏桶装的是请求、控制的是出水;令牌桶装的是令牌,控制的是令牌的生成

规则是这样的:系统以一个恒定速率往桶里放令牌,桶有容量上限,放满了就不再放。每个请求要被放行,必须先从桶里取走一个令牌;取得到就放行,取不到就拒绝。

import time


class TokenBucket:
    """令牌桶:桶以恒定速率生成令牌,请求要拿到令牌才放行。"""

    def __init__(self, capacity: int, refill_rate: float):
        self.capacity = capacity         # 桶容量:最多积攒多少令牌(决定突发上限)
        self.refill_rate = refill_rate   # 生成速率:每秒补充多少令牌
        self.tokens = float(capacity)    # 初始就装满,允许一开始就突发
        self.last_refill = time.time()

    def allow(self) -> bool:
        now = time.time()
        # 按流逝的时间补充令牌,但总量不超过桶容量
        self.tokens = min(
            self.capacity,
            self.tokens + (now - self.last_refill) * self.refill_rate,
        )
        self.last_refill = now
        # 桶里有令牌就取走一个、放行;一个都没有就拒绝
        if self.tokens >= 1:
            self.tokens -= 1
            return True
        return False

令牌桶的精妙,全在桶容量这一个参数上。设想系统空闲了一阵子:这期间没有请求来取令牌,令牌就一直在按速率往桶里攒,直到攒满 capacity 个。这时候突然来了一波流量,它可以一口气取走桶里攒下的全部令牌——这就是"突发":短时间内放行的请求数,可以达到 capacity。而一旦桶里攒的令牌被取空,后续请求就只能等令牌按 refill_rate 一个个慢慢生成,速率被拉回到了 refill_rate

所以令牌桶一个算法,同时给了你两个旋钮:refill_rate 控制长期平均速率,capacity 控制允许多大的瞬间突发。这种"平时限速、闲时攒额度、忙时可短暂爆发"的特性,非常贴合真实业务的流量形态,所以令牌桶是目前应用最广的限流算法——你熟悉的很多网关、框架(如 Guava 的 RateLimiter、Nginx 的 limit_req)底层用的都是它或它的变体。把漏桶和令牌桶放一起记最清楚:要绝对平滑、零突发,用漏桶;要限住均值、又给突发留口子,用令牌桶。

六、工程坑:分布式限流、限流维度、被限流后怎么办

算法本身搞懂了,但要把限流真正用进生产,还有几个绕不开的工程坑。

坑 1:进程内的限流器,在多实例部署下会失效。前面所有那些类,计数器、桶,都是存在单个进程的内存里的。可现代服务几乎都是多实例部署的:你部署了 10 个实例,每个实例各自跑一个"每秒 1000"的限流器,那么整个系统实际放行的就是每秒 10000——限流额度被实例数悄悄放大了 10 倍。要做对,限流状态必须放进一个所有实例共享的存储里,通常是 Redis。而且"取令牌"这组"读令牌数、算补充、扣减、写回"的操作必须是原子的,否则多个实例并发取令牌会互相覆盖。标准做法是用 Lua 脚本,让 Redis 把整段逻辑作为一个不可分割的整体执行:

import time

# 分布式限流:限流状态放进所有实例共享的 Redis,
# 且"取令牌"这组读改写必须原子 —— 用 Lua 脚本一次完成。
TOKEN_BUCKET_LUA = """
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local tokens = tonumber(redis.call('hget', key, 'tokens') or capacity)
local last = tonumber(redis.call('hget', key, 'last') or now)
tokens = math.min(capacity, tokens + (now - last) * rate)
local allowed = 0
if tokens >= 1 then
    tokens = tokens - 1
    allowed = 1
end
redis.call('hset', key, 'tokens', tokens, 'last', now)
redis.call('expire', key, 60)
return allowed
"""


def allow(redis, key: str, capacity: int, rate: float) -> bool:
    # 整段 Lua 在 Redis 里原子执行,多实例并发也不会算错
    return redis.eval(TOKEN_BUCKET_LUA, 1, key,
                      capacity, rate, time.time()) == 1

坑 2:限流维度——你到底在对"谁"限流?限流器的 key 怎么定,直接决定了限流的效果。如果用一个全局的 key 对所有流量限流,那一个恶意用户疯狂刷接口,就会把额度占满,正常用户全被误伤。通常要按更细的维度来:按用户 ID 限,防的是单个用户刷爆;按 IP 限,兜底匿名请求;还可以按"用户 + 接口"再细分,因为查询接口和下单接口的承受力完全不同。维度的粗细是个权衡——太粗会误伤,太细则限不住真正的攻击者:

def limiter_key(request) -> str:
    # 限流维度:对谁限流?key 构造方式不同,效果天差地别。
    user_id = request.headers.get("X-User-Id")
    if user_id:
        return f"ratelimit:user:{user_id}"          # 按用户:防单个用户刷爆
    return f"ratelimit:ip:{request.client.host}"    # 匿名请求兜底到按 IP
    # 还可按"用户 + 接口"再细分,如 ratelimit:user:123:/api/order
    # —— 不同接口的承受力不同,该分开限,别用一把尺子量到底

坑 3:被限流的请求,要好好"拒绝"。限流拒绝一个请求时,绝不能简单粗暴地断开连接、或返回一个含糊的 500。规范的做法是返回 HTTP 状态码 429(Too Many Requests)——这个状态码有明确语义,客户端一看就知道"我被限流了,不是服务器坏了"。更进一步,要在响应里带上 Retry-After 头,明确告诉客户端"过多少秒之后再来",这样客户端就能据此退避重试,而不是立刻又发一遍、把限流器继续往死里撞:

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse

app = FastAPI()
bucket = TokenBucket(capacity=200, refill_rate=100)


@app.middleware("http")
async def rate_limit(request: Request, call_next):
    if not bucket.allow():
        # 被限流:返回 429,并用 Retry-After 告诉客户端多久后再来
        return JSONResponse(
            status_code=429,
            content={"error": "请求过于频繁,请稍后重试"},
            headers={"Retry-After": "1"},
        )
    return await call_next(request)

下面这张图,把一个请求从到达、过限流器、到被处理或被拒的完整路径串起来:

关键概念速查

概念 / 手段 说明
限流 在请求消耗有限资源前,挡住超过系统承载能力的那部分流量
固定窗口 时间切成定长窗口逐窗计数,实现最简单,但边界处会放行两倍流量
边界突刺 固定窗口两窗交界的任意 1 秒内可能涌入近 2 倍限额,下游被它打垮
滑动窗口日志 记录每个请求时间戳,精度最高,但高 QPS 下内存开销大
滑动窗口计数器 只存两个窗口计数按比例加权估算,抹平突刺,精度与开销的折中
漏桶 请求以恒定速率流出,输出绝对平滑,但完全不允许突发
令牌桶 按速率攒令牌,refill_rate 控均值、capacity 控突发,应用最广
分布式限流 状态放共享 Redis,取令牌用 Lua 脚本保证原子,避免按实例数翻倍
限流维度 按用户 / IP / 接口构造 key,太粗会误伤、太细则限不住攻击者
429 / Retry-After 被限流返回 429,用 Retry-After 头让客户端退避重试

避坑清单

  1. 任何系统的连接、线程、下游配额都有物理上限,流量越线后是雪崩式崩溃而非平缓变慢,必须用限流守住这条线。
  2. 限流的目的不是让所有请求都成功,而是过载时主动拒绝一部分、保住系统整体可用——是"拒绝一部分换保住全部"的取舍。
  3. 固定窗口计数器实现最简单,但两个窗口交界处会放行近两倍限额,下游承受的是"任意滑动 1 秒内"的峰值。
  4. 滑动窗口日志精度最高,但要为每个请求存时间戳,高 QPS 下内存与清理开销大。
  5. 滑动窗口计数器只存两个窗口计数、按重叠比例加权估算,抹平了边界突刺,是工业界最常用的折中方案。
  6. 漏桶以恒定速率流出、输出绝对平滑,但完全不允许突发;下游对速率波动零容忍时才选它。
  7. 令牌桶用 refill_rate 控长期均值、capacity 控突发上限,贴合真实流量形态,是应用最广的算法。
  8. 进程内限流器在多实例部署下会被实例数放大,必须把状态放进共享 Redis,且取令牌操作要用 Lua 脚本保证原子。
  9. 限流维度决定效果:全局一把尺子会误伤正常用户,要按用户 / IP / 接口构造 key,粗细之间做权衡。
  10. 被限流的请求要返回 429 并带 Retry-After 头,让客户端明确知道原因并退避重试,别用含糊的 500 或直接断连。

总结

回头看那次促销把服务打挂的事故,以及我加了限流之后第二次又翻车,最该记住的不是某一段令牌桶代码,而是我动手前那个想当然的判断——"限流就是数个数,够了就拒绝"。这句话错在它把限流当成了一个计数问题,而它其实是一个时间和速率的问题。"这一秒放了多少个"听起来天经地义,可下游根本不按你的整秒来承受流量,它承受的是任意一个滑动的时间窗里的真实峰值。你用整秒切出来的计数器去对账,边界那一下的两倍突刺,你永远也看不见。

所以做限流,真正的功夫不在"写一个计数器"。计数器谁都会写,Demo 里它也确实能拦住流量。真正的功夫在于看清每种算法到底在用什么方式定义"超速",以及这个定义会在什么地方漏:固定窗口漏在边界,所以有了滑动窗口;滑动窗口日志漏在内存,所以有了滑动窗口计数器;窗口类算法都不控制流出节奏,所以有了漏桶;漏桶不给突发留余地,所以有了令牌桶。这篇文章的几节,其实就是顺着这条"一个算法补上一个算法的洞"的链条展开的——你理解了这条链,才不会在选型时拍脑袋,而是能对着自己的业务问:我的下游能不能容忍突发?我的 QPS 有多高?我是不是多实例部署?

你会发现,这套思路和我们处理任何"过载"问题的工程经验是相通的。我们给数据库连接池设上限,给线程池设队列长度,给消息队列设积压告警——本质上都是承认"资源是有限的",并且主动决定"超出部分怎么办",而不是被动地等系统自己崩给你看。限流只是把这种"为有限资源主动设防"的纪律,用在了入口流量这件事上。它不创造处理能力,它做的是更诚实的一件事:在系统还清醒的时候,替它说出那句"我今天最多只能接这么多"。

最后想说,限流做没做对,差距永远不会在 Demo 里暴露——Demo 的流量是你自己手点出来的,平缓、均匀、量也小,固定窗口、令牌桶随便挑一个都跑得好好的。它只在真实的促销洪峰、真实的恶意刷量、真实的多实例部署面前才显形。那时候它会用最难看的方式给你结账:一次边界突刺让下游瞬间过载,一个没做分布式的限流器让额度悄悄翻了十倍。所以别等运营拿着"服务又挂了"的截图来找你,在你写下第一个限流器的时候就该想清楚:它在窗口边界会不会漏?它在多实例下还准不准?它拒绝请求时,客户端知道该过多久再来吗?这几个问题都有了答案,你的限流器才不只是 Demo 里那个看着安心的计数器,而是一道在洪峰面前真正守得住的闸门。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

Function Calling 完全指南:从一次"AI 调用了一个根本不存在的接口"看懂工具调用

2026-5-21 18:54:15

技术教程

RAG 完全指南:从一次"AI 把公司根本没有的制度讲得头头是道"看懂检索增强生成

2026-5-21 19:06:39

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索