连载中 3/20

滑动窗口:把突刺磨平

2026-07-07 · 4355 阅读 · 0 评论 · 0 赞

病根:格子是死的

上一篇结尾的突刺场景值得再细看一遍:9:59:59.9 到 10:00:00.1,200 毫秒里放行 400 个请求。病根不在计数器,在「格子是死的」——窗口边界是一条免检通道,边界两侧各有一整格的额度,扎堆打在边界上就翻倍。磨平突刺有两条路:要么让统计区间跟着时间滑(任意时刻回看最近 1 秒),要么把死格子切碎、统计最近若干格的总和——两条路都叫滑动窗口,工程上是同一思想的两种精度。

实现一:环形格子,切细了加总

把 1 秒切成 10 个 100 毫秒的小格子,每个格子一个计数器,限流判定时把最近 10 个格子的计数加总,超过阈值就拒绝。格子随时间滚动覆盖,过去的过期出窗,新的滚动进来:

// 环形格子滑动窗口(简化版)
private final long[] buckets = new long[10];     // 10 个 100ms 的格子
private long sum = 0;                            // 窗口内总数
private int head = 0;                            // 最老的格子下标

public synchronized boolean tryAcquire() {
    long now = System.currentTimeMillis();
    roll(now);                                    // 把过期格子的计数清出窗口
    if (sum >= LIMIT) return false;               // 最近 1 秒总量超阈值
    buckets[(int) ((now / 100) % 10)]++;          // 落进当前格子
    sum++;
    return true;
}

突刺为什么被磨平:边界时刻,上一格只贡献它真实 received 的量(比如 20 个),而不是整格 200 的额度——「跨边界双倍放行」变成了「跨边界按实际量累加」。误差从最坏 2 倍缩到最坏 1.1 倍(一个格子的占比)。格子越细,越平滑、越精确,内存与加总成本越高——1 秒 10 格是常用折中,Sentinel 内部的统计结构正是这套环形格子的精致版。

实现二:ZSET 记时间戳,精确但贵

多实例场景要在 Redis 里做滑动窗口,最常见的写法用 ZSET:每个请求记一个成员(时间戳),判定时先清掉窗口外的旧成员,再数剩余个数:

-- ZSET 滑动窗口(Lua 原子执行)
local key = KEYS[1]                 -- 限流标识
local now = tonumber(ARGV[1])       -- 当前毫秒
local win = tonumber(ARGV[2])       -- 窗口毫秒
local limit = tonumber(ARGV[3])
redis.call("ZREMRANGEBYSCORE", key, 0, now - win)   -- 清出窗成员
if redis.call("ZCARD", key) >= limit then
    return 0                                          -- 超阈值
end
redis.call("ZADD", key, now, now .. "-" .. math.random())  -- 记录本次
redis.call("PEXPIRE", key, win)
return 1

这是真·滑动窗口:任意时刻的判定都是「最近 N 毫秒的真实请求数」,零误差。代价也直白——每个请求一条 ZSET 成员,内存消耗与流量成正比,万级 QPS 的接口上这是百万级成员的常驻内存,还得靠 PEXPIRE 兜底清理。所以 ZSET 版适合低频高价值场景(支付接口、短信发送),高频接口老实用环形格子或下一篇的令牌桶。

两种精度的取舍

方案精度内存适用
固定窗口最坏 2 倍突刺一个计数器,极低粗粒度防线,网关初步拦截
环形格子滑动窗口误差为一个格子占比格子数 × 8 字节,低单机高频限流、指标统计
ZSET 时间戳窗口零误差与请求数成正比,高低频高价值接口的分布式限流

滑动窗口解决的是「统计得准」,但它的内心戏是回顾历史:数过去一秒来了多少,超了就拒。还有一种思路根本不数历史,而是控制发放节奏——像发号施令一样,按恒定速率发通行证。这就是下一篇的主角:漏桶与令牌桶,限流算法家族里最经典的两位。

☕
503

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

#滑动窗口#环形格子#ZSET#临界突刺#限流精度

评论 (0)

热门推荐

连载中 11/22

主从搭建实操:从零配出一主两从

光讲原理不过瘾?手把手搭一主两从:my.cnf 六个参数、复制账号、GTID、CHANGE REPLICATION SOURCE TO、SHOW REPLICA STATUS 验收,附翻车排查清单。

#MySQL#主从复制#GTID#主从搭建#高可用
2026-05-07 · 10101 阅读 · 0 评论 · 0 赞
连载中 16/22

连接池:HikariCP 参数与连接风暴

连接池不是越大越好:8 核机器配 1000 连接反而更慢的数学原理,HikariCP 四个必调参数,maxLifetime 与 wait_timeout 的隐形陷阱。

#MySQL#连接池#HikariCP#maxLifetime#连接风暴
2026-05-10 · 9873 阅读 · 0 评论 · 0 赞
连载中 4/16

缓存穿透:恶意 ID 打穿 MySQL 的四道防线

请求的数据在缓存和数据库里都不存在时,缓存形同虚设。聊聊参数校验、空值缓存、布隆过滤器、限流熔断四道防线的原理与组合打法。

#Redis#缓存穿透#布隆过滤器#高可用
2026-05-16 · 9294 阅读 · 21 评论 · 287 赞