vault backup: 2026-05-27 23:01:37
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user