2026-06-01 00:35:50 +08:00
|
|
|
|
---
|
|
|
|
|
|
tags: [redis, sorted-set, sliding-window, lua-script]
|
|
|
|
|
|
create time: 2026-06-01 10:30
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
# Sorted Set 实现滑动窗口详解
|
|
|
|
|
|
|
2026-06-01 10:06:11 +08:00
|
|
|
|
## 一句话概括
|
2026-06-01 00:35:50 +08:00
|
|
|
|
|
2026-06-01 10:06:11 +08:00
|
|
|
|
**把每个请求的时间戳当成"分数"存进有序集合,每次来新请求时:**
|
2026-06-01 00:35:50 +08:00
|
|
|
|
|
2026-06-01 10:06:11 +08:00
|
|
|
|
> ① 清除窗口外的旧记录 → ② 数一数还剩多少 → ③ 没超就放行,超了就拒绝
|
2026-06-01 00:35:50 +08:00
|
|
|
|
|
2026-06-01 10:06:11 +08:00
|
|
|
|
就这么三步。下面是逐步拆解。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 第一步:直观理解——为什么用 Sorted Set?
|
|
|
|
|
|
|
|
|
|
|
|
### 朴素想法的问题
|
|
|
|
|
|
|
|
|
|
|
|
假设我们要限制 **每分钟最多 100 个请求**,一种想法是:把每个请求的到达时间都记下来,随时数一下"最近一分钟内来了几个"。
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
时间轴:t-60s ━━━━━┳━━━━━┳━━━━━► t(现在)
|
|
|
|
|
|
❌ ✅
|
|
|
|
|
|
过期区 保留区
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
问题在于:**你怎么快速找出"哪些记录落在最近 60 秒内"?**
|
|
|
|
|
|
|
|
|
|
|
|
如果用普通 String 或 Hash,只能遍历所有键做比较。而 Sorted Set **天然按 score 排序**,直接就能做范围操作。
|
|
|
|
|
|
|
|
|
|
|
|
### Sorted Set 里的角色
|
|
|
|
|
|
|
|
|
|
|
|
| 字段 | 放什么 | 作用 |
|
|
|
|
|
|
|------|--------|------|
|
|
|
|
|
|
| **score**(分数) | 毫秒级时间戳 | 决定顺序,也是判断是否在窗口的依据 |
|
|
|
|
|
|
| **member**(成员) | UUID / RequestID | 保证全局唯一,避免同毫秒的请求被覆盖 |
|
|
|
|
|
|
|
|
|
|
|
|
Sorted Set 的内部样子(注意看并发场景):
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
score (时间戳) member (UUID) 说明
|
|
|
|
|
|
─────────────────────────────────────────────────────────────
|
|
|
|
|
|
1748700000000 │────► abc123def456...
|
|
|
|
|
|
1748700000000 │────► 999xyz012abc... ← ⚠️ 分布式并发同一毫秒,member 不同就并存!
|
|
|
|
|
|
1748700001500 │────► 789xyz012abc...
|
|
|
|
|
|
1748700003200 │────► def456ghi789...
|
|
|
|
|
|
1748700005000 │────► ghi789jkl012...
|
|
|
|
|
|
1748700005000 │────► mmm012nnn345... ← ⚠️ 同上,score 重复完全没问题
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
有了有序性,两个操作一步到位:
|
|
|
|
|
|
|
|
|
|
|
|
1. **删过期**:所有 score < `now - 60s` 的记录一次性批量删除
|
|
|
|
|
|
2. **统计**:剩下的数量 = 窗口内的请求数
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 第二步:四条核心命令
|
|
|
|
|
|
|
|
|
|
|
|
整个算法只用了 4 条 Redis 命令。一条一条拆。
|
|
|
|
|
|
|
|
|
|
|
|
### 1. ZREMRANGEBYSCORE — 清理过期记录
|
|
|
|
|
|
|
|
|
|
|
|
```lua
|
|
|
|
|
|
ZREMRANGEBYSCORE key -inf (now - window * 1000)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**原理:** 从负无穷到窗口左边界,把所有过期元素删干净。
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
时间轴: ← 过期区 → 保留区
|
|
|
|
|
|
┃───────┼─────────────────►
|
|
|
|
|
|
▲
|
|
|
|
|
|
now - window×1000
|
|
|
|
|
|
|
|
|
|
|
|
ZREMRANGEBYSCORE 做的事:
|
|
|
|
|
|
━━━━━━━━━━━━━━━━━━━┓
|
|
|
|
|
|
删除这里面的每一条 ┗━━ 保留这里的
|
|
|
|
|
|
───────────────────┛
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**Q:为什么下界用 `-inf`?**
|
|
|
|
|
|
历史记录可能跨越多个窗口周期,用 `-inf` 确保一次性全部清理干净,不会漏掉更早的数据。
|
|
|
|
|
|
|
|
|
|
|
|
### 2. ZCARD — 统计当前数量
|
|
|
|
|
|
|
|
|
|
|
|
```lua
|
|
|
|
|
|
ZCARD key
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
返回集合中的元素个数,即窗口内的请求总数。**O(1)**,Redis 内部维护了计数。
|
|
|
|
|
|
|
|
|
|
|
|
### 3. ZADD + EXPIRE — 插入 & 兜底
|
|
|
|
|
|
|
|
|
|
|
|
没超限就放行,同时给 key 设一个过期时间:
|
|
|
|
|
|
|
|
|
|
|
|
```lua
|
|
|
|
|
|
ZADD key now member -- 以当前时间戳为 score 插入
|
|
|
|
|
|
EXPIRE key window -- window 秒后 Redis 自动删 key,兜底清理
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
`EXPIRE` 的真正作用——为什么要设过期?
|
|
|
|
|
|
|
|
|
|
|
|
这里有个容易困惑的地方:**不是说 Lua 脚本没执行到 EXPIRE**(Lua 是原子的,要么全做完要么全不做),也不是说 ZREMRANGEBYSCORE 清不干净(它每次都从 `-inf` 开始删)。
|
|
|
|
|
|
|
|
|
|
|
|
`EXPIRE` 防御的是以下几种边界场景:
|
|
|
|
|
|
|
|
|
|
|
|
| 场景 | 会发生什么 | EXPIRE 救了什么 |
|
|
|
|
|
|
|------|-----------|----------------|
|
|
|
|
|
|
| 正常限流后不再访问 | key 永远存在,内存不被释放(里面可能没几条活跃数据) | ✅ 自动清理空闲 key,否则 key 永久占用内存 |
|
|
|
|
|
|
| 客户端 ZADD 成功但断连,EXPIRE 没发到 Redis | ZADD 插入了一条记录 | ✅ Redis 内部 TTL 兜底,到点自动删除 |
|
|
|
|
|
|
| 绕过 Lua,用 redis-cli 或其他客户端直接操作 | 手动 ZADD 了元素 | ✅ 还是会过期,不会被永久污染 |
|
|
|
|
|
|
| 主从切换极端情况 | 主节点 ZADD + EXPIRE 还没同步就挂掉 | ⚠️ 部分情况下 EXPIRE 在从库上丢失(极端场景) |
|
|
|
|
|
|
|
|
|
|
|
|
**类比理解:** `ZREMRANGEBYSCORE` 是每天大扫除,把过期的东西扔了;`EXPIRE` 是给房间定个租期,到期直接搬空,不用等你想起来打扫。
|
|
|
|
|
|
|
|
|
|
|
|
### 4. 判断逻辑
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
count >= maxLen ? → 拒绝(返回 429)
|
|
|
|
|
|
count < maxLen → 放行,插入新记录
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 第三步:完整流程 & Lua 原子化
|
|
|
|
|
|
|
|
|
|
|
|
### 流程图
|
2026-06-01 00:35:50 +08:00
|
|
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
|
|
flowchart LR
|
2026-06-01 10:06:11 +08:00
|
|
|
|
A[请求到达] --> B[ZREMRANGEBYSCORE<br/>删过期]
|
|
|
|
|
|
B --> C[ZCARD<br/>数一数]
|
2026-06-01 10:06:16 +08:00
|
|
|
|
C --> D{超过max?}
|
2026-06-01 10:06:11 +08:00
|
|
|
|
D -- 否 --> E[ZADD 插入<br/>EXPIRE 设过期]
|
|
|
|
|
|
D -- 是 --> F[返回 429 拒绝]
|
2026-06-01 00:35:50 +08:00
|
|
|
|
|
2026-06-01 10:06:11 +08:00
|
|
|
|
style B fill:#fff3e0
|
2026-06-01 00:35:50 +08:00
|
|
|
|
style C fill:#fff3e0
|
2026-06-01 10:06:11 +08:00
|
|
|
|
style E fill:#e8f5e9
|
|
|
|
|
|
style F fill:#ffebee
|
2026-06-01 00:35:50 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-01 10:06:11 +08:00
|
|
|
|
### 为什么要用 Lua 脚本?
|
2026-06-01 00:35:50 +08:00
|
|
|
|
|
2026-06-01 10:06:11 +08:00
|
|
|
|
因为这 4 步必须是 **原子操作**——不允许其他请求插进来干扰:
|
2026-06-01 00:35:50 +08:00
|
|
|
|
|
2026-06-01 10:06:11 +08:00
|
|
|
|
```
|
|
|
|
|
|
❌ 非原子会怎样:
|
|
|
|
|
|
|
|
|
|
|
|
线程A: 清理过期 → 数数(得到 99)
|
|
|
|
|
|
↑ 此时线程B也来了,也得到 99
|
|
|
|
|
|
线程B: 清理过期 → 数数(得到 99)
|
|
|
|
|
|
|
|
|
|
|
|
线程A: 插入!count = 100
|
|
|
|
|
|
线程B: 插入!count = 101 ← 限流失效了
|
|
|
|
|
|
```
|
2026-06-01 00:35:50 +08:00
|
|
|
|
|
2026-06-01 10:06:11 +08:00
|
|
|
|
Lua 脚本在 Redis 里是单线程串行执行的,不会被中间打断。
|
|
|
|
|
|
|
|
|
|
|
|
### 精简 Lua 脚本
|
|
|
|
|
|
|
|
|
|
|
|
```lua
|
|
|
|
|
|
local key = KEYS[1]
|
|
|
|
|
|
local window = tonumber(ARGV[1]) -- 窗口大小,秒
|
|
|
|
|
|
local maxLen = tonumber(ARGV[2]) -- 最大请求数
|
|
|
|
|
|
local now = tonumber(ARGV[3]) -- 毫秒时间戳
|
|
|
|
|
|
local member = ARGV[4] -- 唯一标识
|
|
|
|
|
|
|
|
|
|
|
|
-- 1. 清理过期(⚠️ 注意单位换算:window 是秒 ×1000 → 毫秒)
|
2026-06-01 00:35:50 +08:00
|
|
|
|
redis.call('ZREMRANGEBYSCORE', key, '-inf', now - window * 1000)
|
|
|
|
|
|
|
2026-06-01 10:06:11 +08:00
|
|
|
|
-- 2. 统计当前数量
|
2026-06-01 00:35:50 +08:00
|
|
|
|
local count = redis.call('ZCARD', key)
|
|
|
|
|
|
|
2026-06-01 10:06:11 +08:00
|
|
|
|
-- 3. 判断 & 插入
|
2026-06-01 00:35:50 +08:00
|
|
|
|
if count >= maxLen then
|
2026-06-01 10:06:11 +08:00
|
|
|
|
return count -- 超限
|
2026-06-01 00:35:50 +08:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
redis.call('ZADD', key, now, member)
|
|
|
|
|
|
redis.call('EXPIRE', key, window)
|
2026-06-01 10:06:11 +08:00
|
|
|
|
return count + 1 -- 放行
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 第四步:关键细节汇总
|
|
|
|
|
|
|
|
|
|
|
|
### 参数一览
|
|
|
|
|
|
|
|
|
|
|
|
| 参数 | 含义 | 示例值 | 单位 |
|
|
|
|
|
|
|------|------|--------|------|
|
|
|
|
|
|
| `window` | 滑动窗口大小 | `60` | 秒 |
|
|
|
|
|
|
| `maxLen` | 窗口内最大请求数 | `100` | 个 |
|
|
|
|
|
|
| `now` | 当前时间 | `1748700008100` | **毫秒** |
|
|
|
|
|
|
| `member` | 唯一标识 | `"abc123..."` | 字符串 |
|
|
|
|
|
|
| `key` | 滑动窗口的容器(Redis Key) | `"rate:sliding:user:1001"` | 字符串 |
|
|
|
|
|
|
|
|
|
|
|
|
### key —— 按什么限流?
|
|
|
|
|
|
|
|
|
|
|
|
这是最容易混淆的概念:**Lua 脚本里的 `key` 是 Redis 的 Key,不是 Sorted Set 内部的 score 或 member。**
|
|
|
|
|
|
|
|
|
|
|
|
它回答了**"对这个维度做限流"**这个问题:
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// 按用户 ID 限流
|
|
|
|
|
|
key := fmt.Sprintf("rate:sliding:user:%s", userID)
|
|
|
|
|
|
|
|
|
|
|
|
// 按 IP 限流
|
|
|
|
|
|
key := fmt.Sprintf("rate:sliding:ip:%s", clientIP)
|
2026-06-01 00:35:50 +08:00
|
|
|
|
|
2026-06-01 10:06:11 +08:00
|
|
|
|
// 按接口路径限流
|
|
|
|
|
|
key := fmt.Sprintf("rate:sliding:api:%s", apiPath)
|
2026-06-01 00:35:50 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-01 10:06:11 +08:00
|
|
|
|
三层关系:
|
2026-06-01 00:35:50 +08:00
|
|
|
|
|
2026-06-01 10:06:11 +08:00
|
|
|
|
```
|
|
|
|
|
|
Redis Key (key) → 限流的维度(哪个用户 / 哪台 IP)
|
|
|
|
|
|
└── Sorted Set → 该 key 下的所有请求记录
|
|
|
|
|
|
├── member → 每条请求的唯一 ID(去重用)
|
|
|
|
|
|
└── score → 每条请求的时间戳(排序用)
|
2026-06-01 00:35:50 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-01 10:06:11 +08:00
|
|
|
|
类比记忆:
|
2026-06-01 00:35:50 +08:00
|
|
|
|
|
2026-06-01 10:06:11 +08:00
|
|
|
|
| 类比 | Redis 对应物 | 作用 |
|
|
|
|
|
|
|------|-------------|------|
|
|
|
|
|
|
| **班级** | `key` | 划分限流范围 |
|
|
|
|
|
|
| **学生名单** | Sorted Set | 班级里的人 |
|
|
|
|
|
|
| **学号** | `member` | 区分每个学生(不能重复) |
|
|
|
|
|
|
| **身高** | `score` | 按高低排队(可以相同) |
|
2026-06-01 00:35:50 +08:00
|
|
|
|
|
2026-06-01 10:06:11 +08:00
|
|
|
|
不同业务场景下 `key` 的设计直接影响限流效果:
|
2026-06-01 00:35:50 +08:00
|
|
|
|
|
2026-06-01 10:06:11 +08:00
|
|
|
|
- **按用户 ID**:每个用户独立配额,公平但可能被恶意用户批量注册绕过
|
|
|
|
|
|
- **按 IP**:简单粗暴,共享宽带/NAT 下的用户会被连坐
|
|
|
|
|
|
- **按接口路径**:保护后端资源,防止某个接口被打挂
|
|
|
|
|
|
- **组合 key**:`rate:sliding:user:1001:api:/pay` = 单个用户对支付接口的独立配额
|
|
|
|
|
|
|
|
|
|
|
|
### 单位换算要点
|
|
|
|
|
|
|
|
|
|
|
|
```lua
|
|
|
|
|
|
now = 毫秒 (由服务端生成)
|
|
|
|
|
|
window = 秒 (业务配置)
|
|
|
|
|
|
window * 1000 = 毫秒 (用于减法对齐)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
清理时用 `now - window * 1000`,本质是:**毫秒 − 秒×1000 = 毫秒 − 毫秒**,两边单位必须一致。
|
|
|
|
|
|
|
|
|
|
|
|
### member 必须唯一的原因
|
|
|
|
|
|
|
|
|
|
|
|
Sorted Set 中,如果 member 相同,无论 score 是什么,新值都会**覆盖**旧值。而 score 重复是完全没问题的:
|
2026-06-01 00:35:50 +08:00
|
|
|
|
|
2026-06-01 10:06:11 +08:00
|
|
|
|
```
|
|
|
|
|
|
场景 结果
|
|
|
|
|
|
──────────────────────────────────────────────
|
|
|
|
|
|
同 score + 同 member ❌ 被覆盖(两条请求丢了一条!)
|
|
|
|
|
|
同 score + 不同 member ✅ 两条并存(正常)
|
|
|
|
|
|
不同 score + 不同 member ✅ 两条并存(正常)
|
2026-06-01 00:35:50 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-01 10:06:11 +08:00
|
|
|
|
你的疑问很关键:**分布式下,不同节点在同一毫秒产生的时间戳相同怎么办?** 答案是无所谓,因为 member(UUID / ULID)已经保证了全局唯一。
|
2026-06-01 00:35:50 +08:00
|
|
|
|
|
|
|
|
|
|
```go
|
2026-06-01 10:06:11 +08:00
|
|
|
|
// 推荐 ULID(单调递增 + 全局唯一)
|
|
|
|
|
|
member := ulid.Now().String()
|
2026-06-01 00:35:50 +08:00
|
|
|
|
|
2026-06-01 10:06:11 +08:00
|
|
|
|
// 或者 UUIDv4(纯随机但足够碰撞安全)
|
|
|
|
|
|
// member := uuid.New().String()
|
2026-06-01 00:35:50 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-01 10:06:11 +08:00
|
|
|
|
**Q:为什么不用自增序号?**
|
|
|
|
|
|
分布式多实例并发时,同一毫秒内不同节点的自增 ID 可能重复。ULID / UUID / RequestID 能保证全局唯一。
|
2026-06-01 00:35:50 +08:00
|
|
|
|
|
2026-06-01 10:06:11 +08:00
|
|
|
|
### Go 调用代码
|
2026-06-01 00:35:50 +08:00
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
func SlidingWindowAllow(ctx context.Context, c *redis.Client, id string, windowSec int, maxRequests int) (int, error) {
|
|
|
|
|
|
key := fmt.Sprintf("rate:sliding:%s", id)
|
|
|
|
|
|
now := time.Now().UnixMilli()
|
2026-06-01 10:06:11 +08:00
|
|
|
|
member := generateMember() // ULID / UUID
|
2026-06-01 00:35:50 +08:00
|
|
|
|
|
|
|
|
|
|
result, err := c.Eval(ctx, slidingWindowLua, []string{key}, windowSec, maxRequests, now, member).Int()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return 0, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return result, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-01 10:06:11 +08:00
|
|
|
|
---
|
2026-06-01 00:35:50 +08:00
|
|
|
|
|
2026-06-01 10:06:11 +08:00
|
|
|
|
## 第五步:代价分析 & 方案选择
|
2026-06-01 00:35:50 +08:00
|
|
|
|
|
2026-06-01 10:06:11 +08:00
|
|
|
|
### 内存估算
|
2026-06-01 00:35:50 +08:00
|
|
|
|
|
2026-06-01 10:06:11 +08:00
|
|
|
|
每条记录的开销约 **~100 字节**(score 8字节 + member ~26字节 + 跳表节点 ~64字节)。
|
2026-06-01 00:35:50 +08:00
|
|
|
|
|
2026-06-01 10:06:11 +08:00
|
|
|
|
```
|
|
|
|
|
|
总内存 ≈ 记录数 × 100 字节
|
2026-06-01 00:35:50 +08:00
|
|
|
|
|
2026-06-01 10:06:11 +08:00
|
|
|
|
记录数 = QPS × window
|
|
|
|
|
|
```
|
2026-06-01 00:35:50 +08:00
|
|
|
|
|
2026-06-01 10:06:11 +08:00
|
|
|
|
| 场景 | QPS | 窗口(s) | 记录数 | 约需内存 |
|
|
|
|
|
|
|------|-----|---------|--------|----------|
|
|
|
|
|
|
| 低频 API | 10 | 86400 | 864,000 | ~86 MB |
|
|
|
|
|
|
| 普通接口 | 100 | 60 | 6,000 | ~600 KB |
|
|
|
|
|
|
| 高频网关 | 1000 | 60 | 60,000 | ~6 MB |
|
|
|
|
|
|
| 超高并发 | 10000 | 60 | 600,000 | ~60 MB |
|
2026-06-01 00:35:50 +08:00
|
|
|
|
|
2026-06-01 10:06:11 +08:00
|
|
|
|
### ⚠️ 阈值预警
|
2026-06-01 00:35:50 +08:00
|
|
|
|
|
2026-06-01 10:06:11 +08:00
|
|
|
|
当 **QPS > 1000 且窗口 > 10s** 时,Sorted Set 已经不太合适了:
|
2026-06-01 00:35:50 +08:00
|
|
|
|
|
2026-06-01 10:06:11 +08:00
|
|
|
|
- 单 key 存储数万 ~ 数十万条记录,内存压力大
|
|
|
|
|
|
- 每次请求要做 O(log N) 的 ZADD + ZREMRANGEBYSCORE,N 很大时 CPU 和延迟明显上升
|
2026-06-01 00:35:50 +08:00
|
|
|
|
|
2026-06-01 10:06:11 +08:00
|
|
|
|
### 替代方案对比
|
2026-06-01 00:35:50 +08:00
|
|
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
|
|
flowchart TD
|
2026-06-01 10:06:11 +08:00
|
|
|
|
Start["需要限流"] --> Q1{"要精确统计还是近似即可?"}
|
|
|
|
|
|
|
|
|
|
|
|
Q1 -- 精确 --> SS["Sorted Set<br/>精确到每条请求<br/>空间 O(QPS×window)"]
|
|
|
|
|
|
Q1 -- 近似即可 --> TC{"QPS 高不高?"}
|
2026-06-01 00:35:50 +08:00
|
|
|
|
|
2026-06-01 10:06:11 +08:00
|
|
|
|
TC -- 低 --> SWC["滑动窗口计数器<br/>分成 N 个子窗口<br/>空间 O(N),N 远小于记录数"]
|
|
|
|
|
|
TC -- 高 --> TB["令牌桶/漏桶<br/>O(1) 空间<br/>不精确但够用"]
|
2026-06-01 00:35:50 +08:00
|
|
|
|
|
2026-06-01 10:06:11 +08:00
|
|
|
|
style SS fill:#fff3e0
|
2026-06-01 00:35:50 +08:00
|
|
|
|
style SWC fill:#e3f2fd
|
2026-06-01 10:06:11 +08:00
|
|
|
|
style TB fill:#c8e6c9
|
2026-06-01 00:35:50 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-01 10:06:11 +08:00
|
|
|
|
| 维度 | Sorted Set | 滑动窗口计数器 | 令牌桶/漏桶 |
|
|
|
|
|
|
|------|-----------|-------------|-----------|
|
|
|
|
|
|
| **精度** | 精确到每条请求 | 近似(子窗口粒度) | 不精确(速率平滑) |
|
|
|
|
|
|
| **空间** | O(QPS × window) | O(子窗口数 N) | O(1) |
|
|
|
|
|
|
| **时间** | O(log N) / 次 | O(1) / 次 | O(1) / 次 |
|
|
|
|
|
|
| **适用** | QPS ≤ 1000、小窗口 | QPS ≤ 10000 | 无上限 |
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-06-01 00:35:50 +08:00
|
|
|
|
## 关联笔记
|
|
|
|
|
|
|
|
|
|
|
|
- [[分布式限流]] — 完整的限流算法演进路线
|
|
|
|
|
|
- [[Lua脚本]] — Lua 脚本中的 Sorted Set 操作
|
|
|
|
|
|
- [[滑动窗口计数器]] — 空间效率更高的替代方案
|