vault backup: 2026-05-25 23:07:45

This commit is contained in:
hhs
2026-05-25 23:07:45 +08:00
parent 9000c90abe
commit 268ee58de6
49 changed files with 282 additions and 54 deletions
+43 -24
View File
@@ -7,7 +7,7 @@ create time: 2026-05-15 18:14
## 概述
Sorted Set(有序集合)是 Redis 中**功能最丰富、场景最广泛**的数据结构。它的底层由 skiplist + hashtable 组成,天然支持按分数排序和 O(1) 的成员查找。本节聚焦实战中常用的高级模式。
Sorted Set(有序集合)是 Redis 中**功能最丰富、场景最广泛**的数据结构。底层采用双编码策略:小集合(默认 ≤128 个元素,Redis 7.0+ 使用 listpack 替代 ziplist)用连续内存紧凑存储;超过阈值后自动切换为 **skiplist + hashtable** 组合,天然支持按分数排序和 O(log N) 的范围查询。本节聚焦实战中常用的高级模式。
## 排行榜系统
@@ -30,6 +30,10 @@ ZREVRANK leaderboard "user:101" # 2
ZRANGEBYSCORE leaderboard 3000 4000
```
> [!WARNING] ZRANGEBYSCORE 已废弃(Redis 6.2+)
> `ZRANGEBYSCORE` 在 Redis 6.2 起被统一到 `ZRANGE ... BYSCORE` 语法。本文档保留旧命令便于理解,但新代码建议写成:
> `ZRANGE leaderboard 3000 4000 BYSCORE WITHSCORES`
### Go 代码示例
```go
@@ -112,6 +116,9 @@ ZREM delayed_tasks <completed-tasks>
LPUSH processing_queue <completed-tasks>
```
> [!CAUTION] 多步操作需保证原子性
> 上面的"查询 → 删除 → 转存"三步并非原子操作,多个消费者同时拉取会导致**重复消费**。生产环境应将这段逻辑封装到 Lua 脚本中,通过 `EVAL` 一次执行,或使用 `BZPOPMIN` 的阻塞弹出模型天然规避此问题。
> [!WARNING] 延迟队列不是消息队列
> 对于大量并发任务,ZSet 的 O(log N) 插入和维护成本会累积。考虑专用 MQ(Kafka/RabbitMQ)更合适。Redis ZSet 适合**规模适中(万级以内)**的延迟任务场景。
@@ -121,9 +128,9 @@ ZSet 天然适合做**固定时间窗口内的去重计数**(如每分钟独
```bash
# key = page:home:uv,score = unix timestamp ms,member = visitor_id
ZREMRANGEBYSCORE page:home:uv $now_ms 60000 # 清除 60s 前的数据
ZREMRANGEBYSCORE page:home:uv 0 $((now_ms - 60000)) # 清除 60s 窗口外的旧数据
ZADD page:home:uv $now_ms visitor:a visitor:b visitor:c
SCARD page:home:uv # 当前窗口内独立访客数
ZCARD page:home:uv # 当前窗口内独立访客数(ZSet 用 ZCARD,不是 SCARD)
```
```go
@@ -148,9 +155,10 @@ count, _ := rdb.ZCard(ctx, "page:home:uv").Result()
```bash
# === 固定窗口限流 ===
# key = limit:user:1001, score = unix timestamp(每秒一个请求)
ZREMRANGEBYSCORE limit:user:1001 $now_ts $(($now_ts - 1)) # 清除 1s 前数据
ZADD limit:user:1001 $now_ts req:$RANDOM # 记录本次
# 按整秒截断:窗口起点 = floor(now_s / 1) * 1
WINDOW_START=$((now_ts - now_ts % 1)) # 当前秒的起始时间戳
ZREMRANGEBYSCORE limit:user:1001 0 $((WINDOW_START - 1)) # 清除上一个窗口之前的数据
ZADD limit:user:1001 $now_ts req:$RANDOM # 记录本次请求
ZCARD limit:user:1001 # 当前窗口 QPS
# === 滑动窗口限流(更精准)===
@@ -206,16 +214,23 @@ ZADD rank:us 800 "vip_user:A" 900 "user:C"
# 合并求和(默认分数相加)
ZUNIONSTORE global:top 2 rank:cn rank:us AGGREGATE SUM
# AGGREGATE 三种模式:
# SUM — 分数求和(默认,适合多维度累加)
# MAX — 取各集合中的最大分数(适合"最高分"场景)
# MIN — 取各集合中的最小分数(适合"保底分"场景)
# 取全局 Top 10
ZRANGE global:top 0 9 WITHSCORES REVERSE
ZREVRANGE global:top 0 9 WITHSCORES
# → vip_user:A(1800), user:C(900), user:B(500)...
```
```go
keys := []string{"rank:cn", "rank:us", "rank:jp"}
// 合并三个区域排行榜,取分数最高的 20 名
rdb.ZUnionStore(ctx, "global:top20", keys)
// 第一个参数是目标 key,结果写入 "global:top"
rdb.ZUnionStore(ctx, "global:top", &redis.ZStore{
Keys: keys,
Aggregate: "SUM", // 可选: "SUM" / "MAX" / "MIN"
})
top20, _ := rdb.ZRevRangeWithScores(ctx, "global:top20", 0, 19).Result()
```
@@ -232,26 +247,30 @@ GEOADD cities 116.4074 "beijing" 121.4737 "shanghai" 108.9690 "nanning"
# 计算两点距离(米)
GEOPOS cities beijing shanghai # 获取经纬度坐标
GEODIST cities beijing nanning km # 距离(km/m/fi 单位)
GEODIST cities beijing nanning km # 距离(km/m/ft/mi 单位)
# 附近的人(半径 50km 内)
GEORADIUS cities 116.4074 39.9042 50 km WITHDIST WITHCOORD
# 附近的人(半径 50km 内)—— Redis 6.2+ 推荐
GEOSEARCH cities FROMLONLAT 116.4074 39.9042 BYRADIUS 50 km WITHDIST WITHCOORD ASC COUNT 5
# ASC: 从近到远(默认)
# 新版本 API(推荐)
GEORADIUSBYMEMBER cities shanghai 100 km DESC COUNT 5
# DESC: 从远到近(默认从近到远)
# 以某个成员为中心搜索
GEOSEARCH cities FROMMEMBER shanghai BYRADIUS 100 km DESC COUNT 5
# DESC: 从远到近
```
> [!WARNING] GEORADIUS / GEORADIUSBYMEMBER 已废弃
> 这两个命令在 Redis 6.2 后被 `GEOSEARCH`(查询)和 `GEOSEARCHSTORE`(查询并存储)取代。旧命令仍可使用,但新代码应直接用 `GEOSEARCH`。
### 常见场景
| 场景 | 命令 | 注意 |
|------|------|------|
| 附近门店 | GEORADIUSBYMEMBER | 限流 + 分页 |
| 打车接单范围 | GEOADD + GEORADIUS | 结合 spatial index |
| 围栏检测 | GEOPOS + 数学计算 | 超出范围触发告警 |
| 附近门店 | `GEOSEARCH FROMMEMBER` | 限流 + `COUNT` 分页 |
| 打车接单范围 | `GEOSEARCH FROMLONLAT` | 结合空间索引,`ASC` 按距离排序 |
| 围栏检测 | `GEOPOS` + Haversine 公式 | 超出范围触发告警 |
> [!TIP] Geo 底层就是 Sorted Set
> `GEOPOS` 本质是 `ZSCORE`(存的是 encode 后的 lat+lng),所以也可以用 ZREVRANGE 做通用查询。Geo 只是一层语法糖。
> `GEOPOS` 本质是 `ZSCORE` + GeoHash 解码(score 存的是 52 位 GeoHash 编码值,不是原始经纬度),所以也可以用 `ZSCORE` 读出编码值,或用 `ZREVRANGE` 做通用范围查询。Geo 只是一层语法糖。
## 社交关系 —— 共同好友 / 标签匹配
@@ -282,12 +301,12 @@ ZADD rec:user:2 "go" 0.95 "python" 1.0 "flask" 0.6 "k8s" 0.9
ZUNIONSTORE temp:rec 2 rec:user:1 rec:user:2 AGGREGATE SUM
# 分数越高 = 共同兴趣越多越强
ZRANGEBYSCORE temp:rec 1.5 2.0 REVERSE WITHSCORES
ZREVRANGEBYSCORE temp:rec 2.0 1.5 WITHSCORES
# → go(1.95), k8s(1.6)
```
> [!WARNING] 大集合运算警告
> `SINTER` / `SUNION` / `ZUNIONSTORE` 的时间复杂度是 O(N × M),在大型集合上可能阻塞主线程。建议预计算 + 定时刷新,或在低峰期异步完成。
> `SINTER` / `SUNION` / `ZUNIONSTORE` 的时间复杂度是 O(N × M)(N 为最小集合的元素数,M 为集合个数),在大型集合上可能阻塞主线程。建议预计算 + 定时刷新,或在低峰期异步完成。
## 优先级队列
@@ -337,12 +356,12 @@ mindmap
ZRANGEBYSCORE + min/max
避免 OFFSET 深翻页
编码降级
<64元素 &lt;64B→ziplist
≥128元素→skiplist+hashtable
小集合→listpack 紧凑编码
大集合→skiplist+hashtable
```
> [!TIP] 编码转换的触发条件
> `maxziplist` entries(默认 128)和 `maxziplistvalue`(默认 64 bytes)控制着 ziplist ↔ skiplist 的转变。在数据量较大时建议调大 `zset-max-ziplist-entries` 以利用 ziplist 的内存紧凑性,但要权衡查询性能。
> 配置参数 `zset-max-ziplist-entries`(默认 128)和 `zset-max-ziplist-value`(默认 64 bytes)控制着 listpack(Redis 7.0 前为 ziplist)↔ skiplist 的转变。小集合优先使用 listpack 紧凑编码以节省内存,超过阈值自动升级为 skiplist + hashtable。在数据量较大时可适当调大 entries 阈值以延迟升级,但需权衡 listpack 的 O(N) 插入性能。
## 关联笔记