限流算法完全指南:从一次"固定窗口在临界点放进双倍流量、服务被打挂"看懂四种限流

2023 年我做一个对外开放的 API 服务。为了不被人滥用产品要求给每个用户限流每分钟最多 100 次请求超了就拒绝。第一版我做得很直接用一个计数器记下这一分钟这个用户调了多少次到 100 就拒绝等下一分钟计数器清零重新开始数。本地一测很完美一分钟内连发 100 次正常第 101 次准确地被拒掉。我心里很踏实限流不就是数个数嘛。可上线之后服务还是三番五次地被某些用户打到过载监控上的请求曲线时不时窜起一根尖刺。我盯着日志查了很久终于定位到那个诡异的现象某个用户在 10 点 00 分 59 秒这一秒把这一分钟的 100 次额度用满紧接着 10 点 01 分 00 秒新的一分钟开始计数器清零他立刻又发了 100 次。结果就是在这短短两秒里他合法地发出了 200 次请求。我的每分钟 100 次限流在窗口的临界点上被轻松突破成了两秒 200 次。我才想明白第一版错在把时间切成了一段段互不相干的格子每个格子各数各的可用户的请求不认识你的格子他完全可以骑在两个格子的接缝上。本文从头梳理为什么固定窗口会在临界点漏进双倍流量滑动窗口怎么把这个洞补上漏桶和令牌桶各自怎么工作它们之间到底怎么选以及分布式限流限流维度被限流之后怎么办这些把限流真正做对要避开的坑。

2023 年我做一个对外开放的 API 服务。为了不被人滥用,产品要求给每个用户限流:每分钟最多 100 次请求,超了就拒绝。第一版我做得很直接:用一个计数器,记下"这一分钟"这个用户调了多少次,到 100 就拒绝;等下一分钟,计数器清零,重新开始数。本地一测——很完美:一分钟内连发 100 次正常,第 101 次准确地被拒掉;过了这一分钟,又能正常发了。我心里很踏实:"限流不就是数个数嘛,这不就做完了。"可上线之后,服务还是三番五次地被某些用户打到过载,监控上的请求曲线时不时窜起一根尖刺。我盯着日志查了很久,终于定位到那个诡异的现象:某个用户,在 10:00:59 这一秒,把"10:00 这一分钟"的 100 次额度用满;紧接着 10:01:00,新的一分钟开始、计数器清零,他立刻又发了 100 次。结果就是——在 10:00:59 到 10:01:00 这短短两秒里,他合法地发出了 200 次请求。我的"每分钟 100 次"限流,在窗口的临界点上,被轻松突破成了"两秒 200 次"。我盯着这根尖刺想了很久才彻底想明白,第一版错在一个根本的认知上:我以为"限流就是按整分钟数个数"。可这个想法有个致命的破绽:它把时间切成了一段段互不相干的"格子",每个格子各数各的。可用户的请求不认识你的格子——他完全可以骑在两个格子的接缝上,在接缝的左边用满一个格子,在接缝的右边立刻用满下一个格子。限流真正要管的,是"任意一个 60 秒的滑动区间里不超过 100 次",而不是"对齐到整分钟的那个格子里不超过 100 次"。这两件事听起来一样,差别却是致命的。这篇文章就把限流梳理一遍:为什么固定窗口会在临界点漏进双倍流量、滑动窗口怎么把这个洞补上、漏桶和令牌桶各自怎么工作、它们之间到底怎么选,以及分布式限流、限流维度、被限流之后怎么办这些把限流真正做对要避开的坑。

问题背景

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

现象:一个开放 API,限流规则是"每个用户每分钟 100 次"。第一版用固定窗口计数器实现:按整分钟数数,到点清零。本地测试一切正常,但上线后服务仍被打到过载,监控曲线上时不时窜起尖刺。排查发现:某用户在某一分钟的最后一秒发满 100 次,又在下一分钟的第一秒发满 100 次,两秒内涌入 200 次请求。

我当时的错误认知:"限流就是按整分钟数个数,数到 100 就拒绝、到点清零,这就够了。"

真相:把时间对齐成固定的整分钟格子分别计数,会在相邻格子的接缝处留下一个洞——用户骑在接缝上,能在极短时间里发出接近两倍的流量。限流真正要保证的是"任意一个 60 秒滑动区间内不超限"。要做到这一点,需要滑动窗口;而如果你还想控制请求的速率、平滑突发,就要用到漏桶令牌桶。四种算法,精度、内存、是否允许突发各有取舍。

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

  • 为什么固定窗口会在临界点漏进双倍流量——格子是死的,流量是活的;
  • 滑动窗口日志与滑动窗口计数器——怎么补上临界点的洞;
  • 漏桶算法——把忽快忽慢的流量整成恒定速率;
  • 令牌桶算法——既限平均速率,又允许一定的突发;
  • 分布式限流、限流维度、被限流后怎么办这些工程坑。

一、为什么固定窗口会在临界点漏进双倍流量

先把这件最根本的事钉死:固定窗口把时间切成对齐整点的、互不相干的格子,每个格子各数各的;用户只要骑在两个格子的接缝上,就能在接缝两侧各用满一个格子,瞬间发出接近两倍的流量。

下面这段代码,就是我那个"会在临界点漏流量"的第一版——固定窗口计数器:

import time


class FixedWindowLimiter:
    """固定窗口计数器:按对齐整点的窗口计数,到点清零。"""

    def __init__(self, limit: int, window: int = 60):
        self.limit = limit            # 每个窗口允许的次数
        self.window = window          # 窗口长度(秒)
        self.counts = {}              # {窗口编号: 该窗口已用次数}

    def allow(self, user: str) -> bool:
        now = time.time()
        # 关键:窗口编号 = 当前时间整除窗口长度。
        # 它把时间【对齐】成一个个固定的格子。
        win_id = int(now // self.window)
        key = (user, win_id)
        used = self.counts.get(key, 0)
        if used >= self.limit:
            return False              # 这个格子用满了,拒绝
        self.counts[key] = used + 1
        return True
        # 破绽:win_id 是【跳变】的。10:00:59 和 10:01:00
        # 属于两个不同的 win_id,各自有满额的 limit 次。
        # 用户在接缝两侧各打满,2 秒就能发出 2 倍流量。

这段代码没有任何语法错误,逻辑也自洽——对任何一个对齐整分钟的窗口,它确实把次数卡在了 100 以内。它的问题不在代码本身,而在一个错误的时间观:它默认"一分钟"是从整点开始的那个固定区间。可对用户来说,"一分钟"是从他第一次请求那一刻起往后的 60 秒——这两个"一分钟"根本不是同一个东西win_id 在整点那一瞬间啪地跳变,上一个窗口的额度瞬间作废、下一个窗口的额度瞬间满血。一个聪明的(或恶意的)用户,只要盯着这个跳变点,就能在跳变前的一瞬跳变后的一瞬各打满一次。问题的根子清楚了:你不该问"这个对齐整点的格子里用了多少次",你该问"从现在往前数 60 秒,一共用了多少次"。

二、滑动窗口日志:精确地数清最近 60 秒

上一节的死结是:固定窗口的格子是跳变的。要补这个洞,思路其实很朴素:既然要管的是"最近 60 秒",那就把每一次请求的时间戳都记下来;新请求来时,把 60 秒之前的旧记录全部丢掉,再看剩下的记录有多少条。这就是滑动窗口日志(Sliding Window Log)

from collections import deque


class SlidingWindowLog:
    """滑动窗口日志:记下每次请求的时间戳,精确统计最近 window 秒。"""

    def __init__(self, limit: int, window: int = 60):
        self.limit = limit
        self.window = window
        self.logs = {}                # {user: deque([时间戳, ...])}

    def allow(self, user: str) -> bool:
        now = time.time()
        dq = self.logs.setdefault(user, deque())
        # 关键:把 window 秒之前的旧时间戳【全部清掉】。
        # 剩下的,就是"最近 window 秒"内的真实请求。
        while dq and dq[0] <= now - self.window:
            dq.popleft()
        if len(dq) >= self.limit:
            return False              # 最近 60 秒已满,拒绝
        dq.append(now)                # 记下这一次
        return True
        # 它没有"格子"了 —— 窗口随每个 now 平滑地往前滑,
        # 不存在跳变点,临界突刺的洞被彻底堵上。

滑动窗口日志彻底消灭了临界点的洞:它的窗口不是跳变的格子,而是随每一次请求平滑往前滑动的 60 秒。无论用户在什么时刻发请求,它统计的永远是"截至此刻、往前 60 秒"——接缝不存在了。但它有个明显的代价:它要给每一次请求都存一个时间戳。如果限流是"每分钟一万次",那它就得同时存一万个时间戳;用户一多,内存蹭蹭涨。它精确,但不便宜。有没有办法,既堵住临界点的洞,又不用存这么多东西?

三、滑动窗口计数器:用一点精度换大量内存

滑动窗口日志贵在"每个请求存一个时间戳"。滑动窗口计数器(Sliding Window Counter)的思路是:退回去用计数器(只存一个数,不存每个时间戳),但不止看当前窗口——它同时看当前窗口和上一个窗口,再按当前时刻在窗口里的位置,给上一个窗口的计数打一个折,加权估算出"最近 60 秒"的近似值。

class SlidingWindowCounter:
    """滑动窗口计数器:当前窗口 + 上一窗口加权,估算最近 window 秒。"""

    def __init__(self, limit: int, window: int = 60):
        self.limit = limit
        self.window = window
        self.counts = {}              # {(user, 窗口编号): 次数}

    def allow(self, user: str) -> bool:
        now = time.time()
        win_id = int(now // self.window)
        # elapsed:当前时刻已经走进当前窗口多深(0.0 ~ 1.0)
        elapsed = (now % self.window) / self.window
        cur = self.counts.get((user, win_id), 0)
        prev = self.counts.get((user, win_id - 1), 0)
        # 核心公式:上一窗口的计数,按"还没滑出去的比例"加权计入。
        # 刚进入新窗口时 elapsed≈0,上一窗口几乎全额计入;
        # 快走完当前窗口时 elapsed≈1,上一窗口几乎不计入。
        estimated = prev * (1 - elapsed) + cur
        if estimated >= self.limit:
            return False
        self.counts[(user, win_id)] = cur + 1
        return True

这个加权公式,是滑动窗口计数器的灵魂。还拿 10:01:00 那个临界点说:此刻 elapsed 几乎是 0,于是上一分钟(10:00)那 100 次会几乎全额地被算进来——estimated 直接逼近 100,用户立刻就被拦住了,临界突刺不再发生。它的代价是精度上的近似:这个公式假设上一窗口的请求是均匀分布的,如果实际分布很不均匀,估算值会略有偏差。但它只需存两个计数器,内存极省。这是一笔非常划算的交易,也是工业界用得最多的限流算法之一。不过,前面这三种算法都只解决"数量"问题。还有一类需求它们答不了——我不光要限总数,我还想让请求匀速地被处理。

四、漏桶算法:把忽快忽慢的流量整成恒定速率

设想一个场景:你的下游是一个很娇贵的服务,它受不了流量忽高忽低,你希望无论上游怎么抖,转发给它的请求都是平稳、恒定速率的。滑动窗口做不到这件事——它只管"60 秒内别超 100",至于这 100 次是均匀来的还是挤在 1 秒里来的,它不管。能管这个的,是漏桶算法(Leaky Bucket)

它的比喻就是字面意思:一只底部有个小孔的桶。请求像水一样从上面倒进来(速率忽快忽慢),而水只能从小孔以恒定速率漏出去。桶满了,再倒进来的水就溢出(请求被拒)。

class LeakyBucket:
    """漏桶:水以恒定速率漏出,桶满则溢出(拒绝)。"""

    def __init__(self, capacity: int, leak_rate: float):
        self.capacity = capacity      # 桶容量
        self.leak_rate = leak_rate    # 漏出速率(个/秒)
        self.water = 0.0              # 当前水量
        self.last = time.time()

    def allow(self) -> bool:
        now = time.time()
        # 先按流逝的时间,把这段时间该漏掉的水漏掉
        leaked = (now - self.last) * self.leak_rate
        self.water = max(0.0, self.water - leaked)
        self.last = now
        if self.water + 1 > self.capacity:
            return False              # 桶要溢出了,拒绝这个请求
        self.water += 1               # 请求作为"一滴水"进桶
        return True
        # 注意:漏桶严格按 leak_rate 出水。哪怕桶里有水,
        # 也得等"漏"的节奏 —— 它牺牲了突发,换来绝对平稳的输出。

漏桶的性格很鲜明:它极度追求平稳。出水速率是铁打的恒定值,哪怕桶里积了很多水,也一滴一滴匀速地往外漏,绝不会因为水多就漏快。这让它非常适合"保护娇贵下游"。但它的固执也是缺点:假设系统很闲、桶是空的,这时突然来了一小批请求——理论上系统完全有能力立刻处理,可漏桶偏不,它还是按那个恒定速率慢慢漏。它不允许任何突发。可现实里,大多数业务是希望"系统闲时,允许用户痛快地突发一下"的。这个需求,要靠下一个算法。

五、令牌桶算法:限平均速率,又允许突发

漏桶不近人情:系统再闲也不让你突发。令牌桶算法(Token Bucket)把这件事反过来想,因而成了最受欢迎的限流算法。它也有一只桶,但桶里装的不是水(请求),而是令牌。系统以恒定速率往桶里放令牌;每个请求来,要先从桶里拿走一个令牌才能通过,拿不到就被拒

class TokenBucket:
    """令牌桶:以恒定速率添加令牌,请求需取走令牌才放行。"""

    def __init__(self, capacity: int, refill_rate: float):
        self.capacity = capacity      # 桶最多存多少令牌
        self.refill_rate = refill_rate  # 每秒补充多少令牌
        self.tokens = float(capacity) # 初始装满
        self.last = time.time()

    def allow(self, need: int = 1) -> bool:
        now = time.time()
        # 按流逝的时间补充令牌,但不超过桶容量
        delta = (now - self.last) * self.refill_rate
        self.tokens = min(self.capacity, self.tokens + delta)
        self.last = now
        if self.tokens >= need:
            self.tokens -= need       # 取走令牌,放行
            return True
        return False                  # 令牌不够,拒绝
        # 妙处:系统闲时令牌【攒着】,最多攒满 capacity 个。
        # 这时来一批请求,可以【一次性取走】这堆攒下的令牌 ——
        # 这就是"允许突发",突发上限正好是 capacity。

令牌桶的巧妙,全在那句"令牌会攒着"上。系统的时候,令牌没人取,就一直累积,直到把桶攒满(capacity 个)。这时如果突然来一批请求,它们可以一口气把攒下的令牌全取走——于是用户体验到的是"系统闲时,我可以痛快地爆发一下"。而一旦桶里的令牌取空,后续请求就只能等令牌按 refill_rate 慢慢补,速率被拉回到长期平均值。所以令牌桶同时管住了两件事:长期平均速率refill_rate 决定,瞬时突发上限capacity 决定。这个"平时严格、闲时宽容"的性格,恰好契合绝大多数真实业务。它和漏桶正好相反:漏桶整输出(出水恒定),令牌桶整输入(发令牌恒定,但允许攒)。把它包成一个限流器拿来用,通常长这样:

_buckets = {}   # {user: TokenBucket},每个用户一个独立的桶


def rate_limit(user: str) -> bool:
    """对外的限流入口:每个用户一个令牌桶。
    capacity=100 意味着突发最多 100,refill=100/60 即每分钟 100。"""
    bucket = _buckets.get(user)
    if bucket is None:
        bucket = TokenBucket(capacity=100, refill_rate=100 / 60)
        _buckets[user] = bucket
    return bucket.allow()


# 用在接口里(以 Flask 为例)
def handle_request(user: str):
    if not rate_limit(user):
        return {"error": "too many requests"}, 429
    return {"data": "..."}, 200

到这里,四种算法各就各位了。但这些代码都有一个共同的隐患:它们的计数器、桶,全都存在单个进程的内存里。一旦你的服务部署了多个实例,麻烦就来了——这正是第六节要面对的工程坑。

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

四种算法讲完了,但要把限流真正用在生产上,还有几个绕不开的坑。坑 1:多实例部署时,内存版限流会失效。前面所有代码的计数器都在进程内存里。假设你的服务部署了 5 个实例,负载均衡把请求分摊到它们身上——每个实例各自维护一份"每分钟 100 次",合起来用户实际能发 500 次。限流被实例数悄悄放大了 5 倍。解法是把限流状态挪到一个共享存储里,通常是 Redis,并且用 Lua 脚本保证原子性(否则"读计数、判断、写计数"这几步之间会有并发竞争):

import redis

r = redis.Redis()

# 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)
-- 按流逝时间补令牌,不超过容量
local filled = math.min(capacity, tokens + (now - last) * rate)
local allowed = 0
if filled >= 1 then
  filled = filled - 1
  allowed = 1
end
redis.call('hset', key, 'tokens', filled, 'last', now)
redis.call('expire', key, 120)
return allowed
"""
_script = r.register_script(TOKEN_BUCKET_LUA)


def distributed_allow(user: str) -> bool:
    """分布式令牌桶:状态存 Redis,Lua 保证整段逻辑原子执行。"""
    ok = _script(keys=[f"rl:{user}"],
                 args=[100, 100 / 60, time.time()])
    return ok == 1

用 Lua 脚本的关键,是原子性:补令牌、判断、扣令牌这三步必须作为一个整体执行,中间不能被其它请求插队。否则两个并发请求同时读到"还剩 1 个令牌",会双双放行,令牌被超扣坑 2:限流维度要选对。"每分钟 100 次"——这个""到底是每用户、每 IP、还是每接口?按 IP 限,同一个公司出口 NAT 后共用一个 IP 的用户会互相挤占;按用户限,没登录的请求没有用户 ID可用。通常要组合:登录用户按 user_id、匿名请求按 IP,核心接口和普通接口用不同的额度。维度选错,限流要么误伤好人,要么拦不住坏人

坑 3:被限流的请求,不能只是粗暴地丢掉。当你拒绝一个请求,要给客户端足够的信息,让它知道该怎么办。标准做法是返回 HTTP 429(Too Many Requests),并在响应头里带上 Retry-After(过多久可以再试)和限流额度信息:

from flask import Flask, request, jsonify

app = Flask(__name__)


@app.route("/api/data")
def api_data():
    user = request.headers.get("X-User-Id") or request.remote_addr
    if not distributed_allow(user):
        # 关键:不是丢掉就完了,要告诉客户端【多久后重试】
        resp = jsonify({"error": "rate limit exceeded"})
        resp.status_code = 429
        resp.headers["Retry-After"] = "5"            # 5 秒后再来
        resp.headers["X-RateLimit-Limit"] = "100"
        return resp
    return jsonify({"data": "your data here"})

带上 Retry-After,规矩的客户端乖乖等待再重试,而不是立刻、疯狂地重试——后者会让本已过载的系统雪上加霜坑 4:限流不是越严越好。限流的目的是保护系统,不是为难用户。阈值定得太低,正常用户动不动被拦,体验极差;定得太高,又起不到保护作用。合理的做法是看真实流量的分位数来定(比如取 P99 用户用量的若干倍),并留好监控,根据"被限流的比例"持续调整。坑 5:核心接口和非核心接口要分开限。下单、支付这种核心接口,和某个边角的查询接口,不该共用一个限流池——否则边角接口被刷,会连累核心接口也没额度了。下面这张图,把一次带分布式限流的请求处理串起来:

关键概念速查

概念 / 手段 说明
固定窗口计数器 按对齐整点的格子计数到点清零,实现最简单但临界点会漏进双倍流量
临界突刺问题 用户骑在两个固定窗口接缝上,各打满一个窗口,极短时间内发出近两倍流量
滑动窗口日志 记录每个请求时间戳,清掉过期的再计数,绝对精确但每请求都耗内存
滑动窗口计数器 当前窗口加上一窗口按比例加权估算,只存两个计数器,精度略近似但极省内存
漏桶算法 水恒定速率漏出桶满即溢出,输出绝对平稳但完全不允许突发
令牌桶算法 恒定速率发令牌请求取令牌通过,限平均速率又允许攒下令牌做突发
漏桶 vs 令牌桶 漏桶整输出节奏,令牌桶整输入节奏并允许突发,后者更契合多数业务
分布式限流 多实例下内存计数器会被放大实例倍,需把状态挪到 Redis 并用 Lua 保原子
限流维度 按用户还是 IP 还是接口要选对,通常组合维度,选错会误伤或拦不住
429 与 Retry-After 被限流应返回 429 并告知多久后重试,引导客户端理性退避而非疯狂重试

避坑清单

  1. 别用固定窗口计数器,它按对齐整点的格子计数,用户骑在接缝上能瞬间发出近两倍流量。
  2. 限流真正要保证的是任意一个滑动区间内不超限,而不是对齐整点的那个格子不超限。
  3. 滑动窗口日志精确无突刺,但要给每个请求存时间戳,限流额度大时内存开销很高。
  4. 滑动窗口计数器用当前加上一窗口加权估算,只存两个计数器,是工业界最常用的折中。
  5. 漏桶算法输出恒定速率适合保护娇贵下游,但它完全不允许突发,系统再闲也不放行快。
  6. 令牌桶算法限平均速率又允许突发,闲时攒令牌忙时按速率补,契合绝大多数真实业务。
  7. 漏桶整的是输出节奏,令牌桶整的是输入节奏,选算法先想清楚要不要允许突发。
  8. 多实例部署时进程内存版限流会被实例数放大,要把状态挪到 Redis 并用 Lua 保证原子。
  9. 限流维度要选对,按用户按 IP 按接口各有坑,通常要组合,选错会误伤好人或拦不住坏人。
  10. 被限流的请求要返回 429 并带 Retry-After,引导客户端理性退避,别让它疯狂重试雪上加霜。

总结

回头看那次"每分钟 100 次的限流,被两秒 200 次轻松突破"的事故,以及我后来在限流上接连踩的坑,最该记住的不是某一个算法的代码,而是我动手前那个想当然的判断——"限流就是按整分钟数个数"。这句话错在它偷偷把"一分钟"理解成了"从整点开始的那个固定格子"。可"每分钟 100 次"这个承诺,要管的从来不是某个对齐整点的格子,而是"任意一个 60 秒的区间"。固定窗口把连续流动的时间,粗暴地剁成了一段段互不相干的格子,而它没料到:用户的请求不认识你的格子。用户可以精准地骑在两个格子的接缝上,在接缝左边把上一个格子用光,在接缝右边把下一个格子用光——你以为你设了一道"每分钟 100 次"的闸,实际上在每一个整点,这道闸都会瞬间松开一次,放进一波双倍流量。限流这件事想清楚的,正是这个:时间是连续滑动的,你的窗口也必须是连续滑动的;一旦你的窗口是跳变的格子,跳变的那一瞬,就是你防线的缺口

所以做限流,真正的工程量不在"数个数"那一下。数个数,固定窗口十几行就写完了。真正的工程量,在于你要在四种算法之间,按你的真实诉求做出选择——而这四种算法,其实是在回答四个层层递进的问题。固定窗口回答"怎么数",但它在临界点漏。滑动窗口日志回答"怎么数准",但它太耗内存。滑动窗口计数器回答"怎么又准又省",它用一点精度换来了内存——这是数量维度的终点。可数量管住了,还有速率没管:漏桶回答"怎么让输出绝对平稳",代价是牺牲突发;令牌桶回答"怎么既限平均速率、又留出突发空间",于是成了最通用的那个。这篇文章的几节,其实就是顺着这条问题链展开的:先看固定窗口错在哪,再看滑动窗口怎么补精度,再看漏桶和令牌桶怎么管速率,最后是分布式、维度、429 这些把限流真正落地的工程细节。

你会发现,限流的思路,和现实里一个热门场馆怎么控制入场完全相通。一个笨拙的场馆,会说"每个整点放 100 人"——结果聪明的人都掐着 9:59 和 10:00 的接缝来,两分钟挤进 200 人(这就是固定窗口)。一个较真的场馆,会给每个人发一张带时间戳的票,精确数清"最近一小时进了多少人"(这是滑动窗口日志,准但费事)。一个聪明的场馆,会在闸机口匀速放人,无论门外排得多长,里面始终是平稳的人流(这是漏桶)。而一个最体贴的场馆会这么做:它按固定速率发号,没人来的时候号攒着,于是一大群人同时到达时,能用攒下的号一次性进去一批,之后再恢复匀速(这就是令牌桶——它既守住了长期的平均人流,又给了人群一点痛快)。限流算法的演进,本质上就是一个管理者从"机械数数"到"理解流量的脾气"的过程。

最后想说,限流做没做扎实,差距永远不会在测试环境暴露——测试时你自己手动点几下,流量温和而规律,固定窗口也好、令牌桶也罢,看起来都一样。它只在真实的、有恶意刷量、有突发热点、服务还多实例部署的生产环境里才显形。那时候它会用最直接的方式给你结账:做不好,你会像我一样,看着监控曲线在每个整点窜起一根尖刺,看着多实例把你的限流额度悄悄放大了五倍,看着核心接口因为和边角接口共用限流池而被一起拖垮——你明明设了限流,系统却照样被打挂。而做了,无论外面的流量怎么抖、怎么刷、怎么突发,你的系统始终把进来的请求稳稳地控制在它能承受的范围内,该放行的痛快放行,该拒绝的礼貌地回一个 429 和"请 5 秒后再试"。所以别等监控上那根尖刺、等服务被刷挂的告警找上门,在你写下第一行"数个数"的限流代码时就该想清楚:我要管的,是"对齐整点的格子",还是"任意一段滑动的时间"?我要不要允许用户突发?我的服务有几个实例,这个计数器放在哪?这几个问题都有了答案,你的限流才不只是测试环境里那个"数到 100 就拒绝"的玩具,而是一道无论流量怎么冲都能稳稳替系统扛住的可靠防线。

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

大模型流式输出完全指南:从一次"用户盯着空白屏幕等了十几秒以为卡死"看懂 SSE 流式响应

2026-5-21 21:54:55

技术教程

Prompt 注入完全指南:从一次"用户一句忽略以上所有指令、AI 就被策反"看懂大模型安全

2026-5-21 22:04:26

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