连载中 2/20

限流第一课:固定窗口计数器

2026-07-06 · 5612 阅读 · 0 评论 · 0 赞

把「少接点」翻译成代码

上一篇的雪崩链第一味药是限流:流量超过容量,就果断少接点。限流的需求说出来只有一句话——「下单接口每秒最多放行 200 个请求,多余的拒绝」。把这句话翻译成代码,最朴素的答案叫固定窗口计数器(Fixed Window Counter):把时间切成一格一格的窗口(比如每秒一格),每个窗口一个计数器,请求进来计数器加一,超过阈值就拒绝,窗口切换时计数器清零重来。

// 单机版固定窗口:AtomicLong + 时间戳
private final AtomicLong counter = new AtomicLong();
private volatile long windowStart = System.currentTimeMillis();
private static final long WINDOW_MS = 1000;      // 窗口 1 秒
private static final long LIMIT = 200;           // 阈值 200

public boolean tryAcquire() {
    long now = System.currentTimeMillis();
    if (now - windowStart >= WINDOW_MS) {         // 进入新窗口
        windowStart = now;                        // 换格子,清计数
        counter.set(0);
    }
    return counter.incrementAndGet() <= LIMIT;    // 超阈值返回 false
}

三行逻辑与两个必答题

核心逻辑就三行:判窗口、清计数、加计数。便宜到几乎不会出错:内存里一个 long,判定 O(1),没有锁竞争(AtomicLong 用 CAS)。但把这段代码上到生产之前,有两个必答题要先回答。第一问:限什么维度?——按接口限(全接口共享 200)、按用户限(每人每秒 1 单)、按 IP 限(防刷子)、还是按租户限(防大客户吃光)?维度不同,计数器的 key 就不同,业务上往往多维度叠加:接口整体 200,单用户 1。第二问:超了怎么办?——直接拒绝(返回「活动太火爆」)、排队等待(进队列慢慢消费)、还是降级处理(返回默认值或读缓存)?拒绝是最诚实的,排队是把压力延后,降级是把体验打折——选哪种,取决于业务能接受什么。

多实例之后:计数器放到 Redis

应用是多实例部署,每台机器各数各的,「全接口 200」就变成了「每实例 200」,总量失控。计数器必须挪到所有实例共享的地方——Redis:

-- 固定窗口的 Redis 版(Lua 保证原子)
local c = redis.call("INCR", KEYS[1])            -- 计数 +1
if c == 1 then
    redis.call("PEXPIRE", KEYS[1], ARGV[1])      -- 第一个请求设过期
end
if c > tonumber(ARGV[2]) then
    return 0                                      -- 超阈值,拒绝
end
return 1                                          -- 放行

KEYS[1] 是「接口名 + 窗口号」拼出的 key(比如 limit:order:1736889600),ARGV 是窗口时长与阈值。INCR 加 PEXPIRE 打包在一个 Lua 里,避开「先 INCR 再 EXPIRE 之间进程崩了产生永生 key」的坑——这个坑与分布式锁系列第 2 篇的永生锁同源,读者应该已经眼熟。Redis 版的代价是多一次网络往返(RT 加 1 毫秒左右)与 Redis 自身的可用性依赖,下一篇的网关层限流可以再加一道前置闸分摊压力。

阈值从哪来:压测说了算

阈值 200 是拍脑袋还是算出来的?答案是压测说了算:对接口做全链路压测,找到「RT 开始陡增、错误率开始抬头」的拐点,拐点流量打七折作为限流阈值——留三成余量吸收波动。阈值不是永恒的:代码优化后重新压、扩容后重新压、促销前重新压。把阈值标成拍脑袋数字的限流,比没有限流更危险——它给了你虚假的安全感,也让真正的容量问题被掩盖

它有个藏在边界的坑

固定窗口的弱点藏在一个极端场景里:假设窗口边界是 10:00:00,攻击者在 9:59:59.9 到 10:00:00.1 之间打出 400 个请求——前 200 个落在上一窗口(9:59 窗口刚到 200,全部放行),后 200 个落在下一窗口(10:00 窗口刚清零,又全部放行)。在跨越边界的 200 毫秒里,系统实际承受了 400 个请求,是阈值的两倍。这个「临界突刺」在平滑流量下无感,在秒杀、定时任务扎堆的场景里恰好是最要命的时刻。下一篇的滑动窗口,就是为磨平这根突刺而生。

503

10 年全栈工程师 · 503咖啡馆主理人

#限流#固定窗口#计数器#Redis#Lua#阈值压测

评论 (0)

相关推荐

连载中 12/20

排查四件套:jstack、jmap、jstat、jcmd 的实战分工

jstack 看线程在干什么,jmap 看堆里装了什么,jstat 看运行时在变什么,jcmd 是统一入口。四把刀各管一段,配合着用没有查不动的现场。

#jstack#jmap#jstat#jcmd#排查工具
2026-09-16 · 4 阅读 · 0 评论 · 0 赞
连载中 11/20

GC 日志:把回收过程翻译成人话

一行 GC 日志里塞着七种信息:谁触发的、收了哪、停了多久、活了哪些。加上 -Xlog 配置,再加上日志分析工具,GC 不再是只能盯监控曲线的黑盒。

#GC日志#Xlog#日志分析#GC监控#Full GC排查
2026-09-16 · 13 阅读 · 0 评论 · 0 赞
连载中 10/20

ZGC:亚毫秒停顿是怎么炼成的

百 G 大堆停顿不到一毫秒,靠的是把搬家全部挪到并发阶段——着色指针让引用自带状态,读屏障让搬运中的对象依然可访问。代价是吞吐与内存,收益是停顿与堆大小解耦。

#ZGC#着色指针#读屏障#亚毫秒停顿#分代ZGC
2026-09-15 · 8 阅读 · 0 评论 · 0 赞