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
+99
View File
@@ -43,6 +43,90 @@ Redis 提供五种基础数据类型,用一句话总结各自的核心价值
> 想深入了解? [[02-核心数据类型/02-5-SDS|SDS]] · [[02-核心数据类型/02-1-ziplist与listpack|ziplist 与 listpack]] · [[02-核心数据类型/02-2-skiplist|skiplist]] · [[02-核心数据类型/02-3-hashtable|hashtable]] · [[02-核心数据类型/02-4-quicklist|quicklist]]
### key → value 映射全景
> [!tip] 五种类型,同一个套路
> 无论哪种类型,Redis 的 key 永远是**一个字符串**(底层 SDS)。区别只在于 **value 的内部组织方式**。
```mermaid
flowchart LR
K["Redis Key 空间, 永远是 SDS"] --> V1["String: 一坨不可分割的值"]
K --> V2["Hash: 多个 field-value 对"]
K --> V3["List: 有序可重复序列"]
K --> V4["Set: 无序不重复集合"]
K --> V5["ZSet: member-score 对, 按 score 排序"]
classDef key fill:#e1f5fe,stroke:#2196f3
classDef val fill:#fff3e0,stroke:#ff9800
class K key
class V1,V2,V3,V4,V5 val
```
每个类型的 value 具体长什么样——用实际业务数据来感受:
**String**:value 就是一个原子值,Redis 不关心里面是 JSON 还是纯文本。
```
SET "session:abc123" "eyJhbGciOi..."
┌──────────────────┐ ┌──────────────────────┐
│ key │ │ value │
│ "session:abc123" │ ──→ │ "eyJhbGciOi..." │
│ │ │ 就是一个字符串,没了 │
└──────────────────┘ └──────────────────────┘
```
**Hash**:value 是一张"小表格",field 活在 value 内部,不是 Redis key。
```
HSET "user:1001" name "alice" age "25" email "a@x.com"
┌──────────────────┐ ┌───────────────────────────┐
│ key │ │ value │
│ "user:1001" │ ──→ │ name → "alice" │
│ │ │ age → "25" │
│ │ │ email → "a@x.com" │
│ │ │ field 不是 key,仅限 value 内│
└──────────────────┘ └───────────────────────────┘
```
**List**:value 是一条有序队列,元素没有名字,只有位置。
```
LPUSH "unread:1001" "notif:501" "notif:500" "notif:499"
┌──────────────────┐ ┌─────────────────────────────────┐
│ key │ │ value │
│ "unread:1001" │ ──→ │ [0]notif:499 [1]notif:500 [2]... │
│ │ │ ← 旧 新 → │
│ │ │ 元素可以重复,有顺序 │
└──────────────────┘ └─────────────────────────────────┘
```
**Set**:value 是一个不重复的袋子,没有顺序、没有分数。
```
SADD "online:chat42" "user:1001" "user:2002" "user:3003"
┌──────────────────┐ ┌──────────────────────────────┐
│ key │ │ value │
│ "online:chat42" │ ──→ │ { user:1001, user:2002, │
│ │ │ user:3003 } │
│ │ │ 去重无序,SADD 重复元素只保留一份 │
└──────────────────┘ └──────────────────────────────┘
```
**ZSet**:value 是一组 (member, score) 对,member 唯一、score 可重复。
```
ZADD "leaderboard" 100 "alice" 200 "bob" 150 "carol"
┌──────────────────┐ ┌──────────────────────────────┐
│ key │ │ value │
│ "leaderboard" │ ──→ │ bob → 200 ← 最高 │
│ │ │ carol → 150 │
│ │ │ alice → 100 ← 最低 │
│ │ │ 按 score 排序,member 不可重复 │
└──────────────────┘ └──────────────────────────────┘
```
> [!question] Hash 的 field 和 ZSet 的 member 都"像 key",它们到底是不是 key?
> **都不是。** 它们只存在于所属 key 的 value 内部。`HGET user:1001 name` 必须带上 key `user:1001` 才能访问——你不能用 `name` 全局查找。同理,ZSet 的 `member` 也不能脱离 `leaderboard` 独立存在。它们的作用域**仅限于各自 key 的 value 内部**。
我们先从最简单的 String 开始,逐个击破。
## 一、String(字符串)
@@ -318,6 +402,21 @@ skiplist 天然有序,擅长范围查询("给我分数在 100~200 之间的
> [!warning] ZSet 性能陷阱
> 不要往同一个 ZSet 里塞超过百万级别的元素——skiplist 虽然 O(log N),但百万级的维护成本不小。可以按分片拆成多个 ZSet。
## 同一业务场景:五种类型怎么选?
假设你在做一个"用户主页",需要缓存以下信息:
| 信息 | 最佳类型 | key 设计 | value 存什么 |
|------|---------|---------|-------------|
| 用户 Token | **String** | `"token:uid1001"` | `"eyJhbG..."` (一个字符串) |
| 用户资料 | **Hash** | `"user:1001"` | `{name, age, bio...}` (多个字段,可部分更新) |
| 最近浏览记录 | **List** | `"browsed:1001"` | `["商品A","商品B","商品C"]` (有序可重复) |
| 用户标签 | **Set** | `"tags:1001"` | `{"Go","Redis","后端"}` (去重无序) |
| 全站积分排名 | **ZSet** | `"rank:global"` | `{uid1001:5200, uid2002:4800...}` (按分数排序) |
> [!summary] 一句话记忆
> **key 是门牌号,value 是房间里的东西。** String 放一坨、Hash 放一张表、List 放一条队列、Set 放一个袋子、ZSet 放一个排行榜。
## 快速对照表
| 数据类型 | 编码 | 最佳场景 | 时间复杂度 |
+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