vault backup: 2026-05-25 23:07:45
This commit is contained in:
+43
-24
@@ -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元素 <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) 插入性能。
|
||||
|
||||
## 关联笔记
|
||||
|
||||
|
||||
Reference in New Issue
Block a user