vault backup: 2026-05-27 23:01:37

This commit is contained in:
hhs
2026-05-27 23:01:37 +08:00
parent d09524a6b2
commit 271ab1c1f4
7 changed files with 1128 additions and 180 deletions
+79 -3
View File
@@ -7,7 +7,25 @@ create time: 2026-05-15 18:14
## 概述
Sorted Set(有序集合)是 Redis 中**功能最丰富、场景最广泛**的数据结构。底层采用双编码策略:小集合(默认 ≤128 个元素,Redis 7.0+ 使用 listpack 替代 ziplist)用连续内存紧凑存储;超过阈值后自动切换为 **skiplist + hashtable** 组合,天然支持按分数排序和 O(log N) 的范围查询。本节聚焦实战中常用的高级模式。
> [!TIP] 一句话理解 Sorted Set
> 想象一个**自动按分数排序的排行榜**:你往里扔一个"成员 + 分数"的组合,Redis 帮你从小到大排好。你随时可以问"第 N 名是谁""分数在 80~100 之间的有哪些""某个成员排第几"——全部 O(log N) 搞定。
>
> 这就是 Sorted Set 的本质:**每个成员都有一个分数(score),Redis 自动按分数排序,且成员不重复**。
Sorted Set(有序集合)是 Redis 中**功能最丰富、场景最广泛**的数据结构。底层采用双编码策略:小集合(默认 ≤128 个元素,Redis 7.0+ 使用 listpack 替代 ziplist)用连续内存紧凑存储;超过阈值后自动切换为 **skiplist + hashtable** 组合,天然支持按分数排序和 O(log N) 的范围查询。
```mermaid
graph LR
A["Sorted Set"] --> B["score 排序"]
A --> C["member 唯一"]
A --> D["O(log N) 范围查询"]
B --> E["排行榜"]
B --> F["延迟队列"]
C --> G["去重计数"]
D --> H["Top-N 聚合"]
```
本节聚焦实战中常用的高级模式——你会发现,很多看似不同的业务需求,本质上都是「**带分数的排行榜**」。
## 排行榜系统
@@ -64,18 +82,40 @@ flowchart TD
### 多维度排行榜技巧
同一份数据需要按不同维度排名时,不要用多个 ZSet——用 **score = base + fraction**:
> [!QUESTION] 思考一下
> 游戏排行榜需要先按**胜场**排名,胜场相同时再按**击杀数**排。Sorted Set 只有一个 score 字段,怎么同时表示两个维度?
答案是:把两个维度"打包"成一个浮点数——**score = 主维度 × 基数 + 次维度**。
```
总分 = 胜场 × 10000 + 击杀数
例: 5胜3杀 → score = 50003
→ 先比胜率,再比击杀数,完美映射为单浮点数
↑高位决定主排序 ↑低位决定次排序
```
为什么能行?因为浮点数比较是**先比高位再比低位**的——这和我们日常说"先看总分,总分一样看小分"是一回事。只要次维度的最大值不超过基数(这里是 10000),就不会"进位"干扰主维度。
> [!TIP] 基数怎么选?
> 基数 = 次维度的最大可能值 + 1。比如击杀数最大 9999,基数就取 10000。如果有第三个维度,可以再嵌套:`(主 × 10000 + 次) × 10000 + 第三`。
## 延迟队列
### 原理
> [!QUESTION] 思考一下
> 你有 1000 个定时任务分散在不同时间点需要执行——用户注册 5 分钟后发短信、订单 30 分钟后自动取消……怎么用 Redis 保证它们按时执行?
思路很直观:**把"什么时候执行"存进 score,把"执行什么"存进 member**。消费者不断从 ZSet 里弹出 score 最小(即时间最早)的元素,如果它的 score ≤ 当前时间,说明到期了,执行它;否则放回去继续等。
```mermaid
flowchart LR
A["生产者: 提交任务"] -->|"ZADD score=执行时间"| B["ZSet: 延迟队列"]
B -->|"score最小的到期了?"| C{"消费者检查"}
C -->|"到期"| D["执行任务"]
C -->|"未到期"| B
D --> E["ZREM 移除"]
```
利用 ZSet 的 score 字段存储执行时间戳,通过 `BZPOPMIN`(阻塞弹最低分)定时拉取到期的任务:
```bash
@@ -124,6 +164,14 @@ LPUSH processing_queue <completed-tasks>
## 滑动窗口去重计数
> [!QUESTION] 思考一下
> 统计"最近 1 分钟内有多少独立用户访问了首页"——你能想到几种方案?为什么 ZSet 是其中最简单直接的一种?
核心思路可以用一句话概括:**把 ZSet 当成一个带时间戳的签到本**。每个用户来访时"签到"(score = 当前时间戳,member = 用户 ID),然后把超过 1 分钟的签到记录撕掉,剩下的就是窗口内的独立访客数。
> [!TIP] 为什么 ZSet 天然去重?
> 因为 ZSet 的 **member 是唯一的**——同一个用户多次访问,score 会被更新但不会产生重复记录。这比 List 或 Stream 省去了额外的去重逻辑。
ZSet 天然适合做**固定时间窗口内的去重计数**(如每分钟独立访客):
```bash
@@ -151,6 +199,24 @@ count, _ := rdb.ZCard(ctx, "page:home:uv").Result()
## 滑动窗口限流器
> [!QUESTION] 思考一下
> 限流器的核心问题:「在最近 N 秒内,这个用户最多只能发 100 个请求」。如果用**固定窗口**(按整秒切分),在窗口边界会发生什么?
```mermaid
graph TB
subgraph FW["固定窗口 — 按秒硬切"]
F1["0.9s 发 100 个"] --> F2["1.0s 窗口重置"]
F2 --> F3["1.1s 再发 100 个"]
F4["结果: 0.2s 内通过 200 个!"]
end
subgraph SW["滑动窗口 — 时间平滑"]
S1["任何时候都只看最近 1s"] --> S2["窗口内永远 ≤ 100"]
end
FW -->|"问题"| SW
```
这就是固定窗口的**边界突刺**问题:用户在窗口交界处 0.2 秒内发出 200 个请求,实际速率远超限制。滑动窗口通过"只看最近 N 秒"的方式平滑地解决了这个问题。
用 `ZREMRANGEBYSCORE` + `ZCARD` 实现经典的**固定窗口 / 滑动窗口限流**:
```bash
@@ -239,6 +305,11 @@ top20, _ := rdb.ZRevRangeWithScores(ctx, "global:top20", 0, 19).Result()
## Geo — 地理位置
> [!QUESTION] 思考一下
> 老板说:「加个附近 5 公里奶茶店的功能」。你手头只有 Redis,怎么做?
其实 Geo 就是 **Sorted Set 的语法糖**——底层把经纬度编码成一个 52 位 GeoHash 数字作为 score,成员是地点名称。既然 score 是个数字,自然就能排序、比较距离、查找附近范围。所以 Geo 不是新数据结构,而是**把「找附近」这个常见需求包装成了更友好的 API**。
### 基本操作
```bash
@@ -290,6 +361,11 @@ SINTER interests:user:1 interests:user:2
### 加权推荐(ZSet 方案)
> [!QUESTION] 思考一下
> 两个人兴趣标签有重叠,但重叠程度不同。如果想给用户推荐「最志同道合」的人,怎么**量化匹配度**?
思路:把每个标签赋予一个权重分数(比如用户对该话题的关注度),然后**把两个用户的兴趣 ZSet 做分数求和**——共同标签的分数会叠加,非共同标签保持原分。叠加后的分数越高,说明匹配度越强。
当每个标签有**权重分数**时,把标签转为 ZSet,利用 `ZUNIONSTORE` 计算匹配度:
```bash