大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
Stream / Geo / HyperLogLog:可靠消息、附近的题与海量 UV
这一课你将能做到
- 用 Stream 完成消息追加、消费组读取、确认与积压排查
- 用 Geo 存储地理位置并完成「附近」类查询
- 用 HyperLogLog 以极小内存统计海量 UV,并知道它的误差边界
为什么要学
前五课的结构能覆盖大多数业务,但还有三个高频需求它们做得不够好:
- List 做消息队列不可靠:消息取走就没了、多消费者没法分工、查不到谁卡住了。Stream 是 Redis 5 引入的「带持久化的日志型消息结构」,天然带消息 ID、消费组、确认机制,是 List 队列的正式升级。
- 线下训练营、上机考点有「查我附近 5 公里内的考点」需求,经纬度计算不能靠手写公式。Geo 把坐标存储和距离搜索做成了命令。
- 运营要统计题库网站的 UV(独立访客数),要求去重但不要求绝对精确。用 Set 存全量用户 ID 太费内存,HyperLogLog 用固定 12 KB 就能统计亿级 UV,标准误差约 0.81%。
先建立图像
Stream 像一本只增不改的账本:
key: stream:judge:events
ID 字段-值
1760000000000-0 user 1001 problem 501 verdict AC
1760000000500-0 user 1002 problem 502 verdict WA
1760000001000-0 user 1001 problem 503 verdict TLE
↑ ID 由「毫秒时间戳-序号」组成,天然按时间有序
消费组 g1:消费者 A、B 瓜分消息,XACK 后才算处理完成。
跟着做
步骤 1:XADD 追加消息
判题每完成一次,写入一条事件:
127.0.0.1:6379> XADD stream:judge:events * user 1001 problem 501 verdict AC score 100
"1760000000000-0"
* 表示让 Redis 自动生成 ID。可以限制 Stream 最大长度,防止无限增长:
127.0.0.1:6379> XADD stream:judge:events MAXLEN ~ 10000 * user 1002 problem 502 verdict WA score 0
"1760000000500-0"
~ 表示近似裁剪(性能更好),保留约 1 万条。
步骤 2:查看消息与长度
127.0.0.1:6379> XLEN stream:judge:events
(integer) 2
127.0.0.1:6379> XRANGE stream:judge:events - +
1) 1) "1760000000000-0"
2) 1) "user" 2) "1001" 3) "problem" 4) "501" 5) "verdict" 6) "AC" 7) "score" 8) "100"
2) ...
- 和 + 表示最小、最大 ID。
步骤 3:独立消费 XREAD
数据分析服务直接顺序读新消息(不建组也行):
127.0.0.1:6379> XREAD COUNT 10 BLOCK 5000 STREAMS stream:judge:events $
(nil)
$ 表示「只听此刻之后的新消息」;首次接入想从头读用 0。BLOCK 毫秒数实现阻塞等待。
步骤 4:消费组——多消费者分工
创建消费组(从第一条消息开始消费,MKSTREAM 表示流不存在就建空流):
127.0.0.1:6379> XGROUP CREATE stream:judge:events g1 0 MKSTREAM
OK
消费者 A 以组的身份读消息(> 表示只取尚未投递到本组的新消息):
127.0.0.1:6379> XREADGROUP GROUP g1 consumerA COUNT 1 STREAMS stream:judge:events >
1) 1) "stream:judge:events"
2) 1) 1) "1760000000000-0"
2) 1) "user" 2) "1001" ...
处理成功后确认:
127.0.0.1:6379> XACK stream:judge:events g1 1760000000000-0
(integer) 1
步骤 5:排查卡住的消息
查「已投递但未确认」的消息(PEL,待确认列表):
127.0.0.1:6379> XPENDING stream:judge:events g1
1) (integer) 1
2) "1760000000500-0"
3) "1760000000500-0"
4) 1) 1) "consumerA" 2) "1"
消费者 A 宕机后,把它未确认的消息转交给 B:
127.0.0.1:6379> XAUTOCLAIM stream:judge:events g1 consumerB 60000 0 COUNT 10
60000 是空闲超过多少毫秒才接管。这正是 Stream 比 List 可靠的关键:消息在 XACK 前一直可追踪、可转交、可重试。
步骤 6:Geo——附近的考点
录入考点坐标(经度、纬度、名称):
127.0.0.1:6379> GEOADD geo:exam-sites 116.397 39.908 beijing-center 121.474 31.230 shanghai-east
(integer) 2
查两个考点之间的距离:
127.0.0.1:6379> GEODIST geo:exam-sites beijing-center shanghai-east km
"1068.4"
以北京考点为中心,搜 50 公里内的考点,由近到远:
127.0.0.1:6379> GEOSEARCH geo:exam-sites FROMMEMBER beijing-center BYRADIUS 50 km ASC WITHCOORD WITHDIST COUNT 20
也可以直接给经纬度(FROMLONLAT 经度 纬度),或按矩形搜(BYBOX 宽 高 km)。Geo 底层是 Sorted Set,所以 ZRANGE 等命令也能看原始编码,但一般只用 Geo 系列命令。
步骤 7:HyperLogLog——海量 UV 统计
用户访问页面时 PFADD(自动去重):
127.0.0.1:6379> PFADD uv:problem:501:2026-10-07 user:1001 user:1002 user:1003 user:1001
(integer) 1
127.0.0.1:6379> PFCOUNT uv:problem:501:2026-10-07
(integer) 3
合并多天/多道题的 UV:
127.0.0.1:6379> PFMERGE uv:problem:501:week41 uv:problem:501:2026-10-05 uv:problem:501:2026-10-06 uv:problem:501:2026-10-07
OK
127.0.0.1:6379> PFCOUNT uv:problem:501:week41
(integer) 3
记住三个数字:每个 HLL 键约 12 KB、标准误差约 0.81%、不需要也无法取出具体成员。它和 Bitmap 的选择:ID 是整数且要精确结果用 Bitmap;只要去重计数、允许微小误差用 HyperLogLog。
讲清楚要点
- Stream = 持久化日志 + 消费组 + 确认机制:消息不会因取出而消失,XACK 前都在 PEL 里挂着,这是它和 List 队列的本质区别。
- ID 即顺序:
时间戳-序号让消息天然有序,也方便按时间范围回溯。 - 务必给 Stream 设 MAXLEN 或定期裁剪:Stream 不删消息就一直占内存,队列治理要靠显式策略。
- Geo 只支持 WGS-84 经纬度:距离计算基于球面模型,结果对业务足够,但不是测绘级精度。
- HyperLogLog 是概率结构:省内存的代价是误差和无法取成员;要求精确去重不能用它。
- 与专业 MQ 的边界:Stream 能解决应用内可靠异步,但在海量堆积、复杂路由、跨语言生态上不如 Kafka/RabbitMQ,按规模选择。
你可能会懵的地方
-
XREAD 的
$和 XREADGROUP 的>有什么区别?$是独立消费者用语,表示「只看新消息」;>是组内消费者用语,表示「组里还没投递过的新消息」。组内读历史消息(已投递未确认)则用具体 ID 或0。 -
消息被 XACK 之后还能读吗? 能,XACK 只是告诉 Redis「这条处理完了」,消息本身还在 Stream 里(直到被 MAXLEN 裁掉),XRANGE 仍可回溯。
-
消费者宕机消息会丢吗? 不会。未 XACK 的消息留在 PEL,用 XAUTOCLAIM 或 XCLAIM 转交其他消费者即可,所以监控 XPENDING 是运维必做项。
-
GEOSEARCH 和老的 GEORADIUS 有什么区别? GEORADIUS 已废弃,GEOSEARCH 语法更统一(中心点可选成员或坐标,形状支持圆形和矩形),Redis 7 新代码只用 GEOSEARCH。
-
HyperLogLog 的误差会累积吗? PFCOUNT 单次误差约 0.81%,PFMERGE 的结果同样保持在该误差级别,不会因为合并次数线性恶化。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 继续用 List 做需要确认的队列 | 丢消息、无追踪 | Stream + 消费组 + XACK |
| Stream 不设 MAXLEN | 内存无限增长 | XADD MAXLEN 或定期 XTRIM |
| 消费者处理完忘记 XACK | PEL 越积越多,消息反复重投 | 处理成功立即确认,监控 XPENDING |
| 用 GEORADIUS | 命令已废弃 | 用 GEOSEARCH |
| 要求精确 UV 却用 HLL | 结果有 0.81% 误差 | 精确场景用 Set/Bitmap |
| HLL 键不设过期 | 每日键永久堆积 | 统计键按周期设置 TTL |
卡点急救
- XREADGROUP 报 NOGROUP:消费组不存在,先 XGROUP CREATE;流不存在时加 MKSTREAM。
- 消息反复被消费:消费者没有 XACK 或确认时报错,查 XPENDING 定位卡住的消费者。
- GEOADD 报坐标越界:经度须在 -180
180、纬度 -8590 附近,检查两个数字顺序(经度在前)。
面试常问
-
Stream 相比 List 做队列有什么优势? 消息持久化并带全局 ID;支持消费组多消费者负载均衡;XACK 确认、PEL 追踪、XAUTOCLAIM 故障转交;可按范围回溯、可观测积压。
-
消费组里消费者宕机怎么办? 未确认消息留在 PEL;备用消费者用 XAUTOCLAIM 按空闲时间接管,处理完再 XACK。
-
统计亿级 UV 用什么结构?原理? HyperLogLog:基于伯努利试验的基数估计,每键约 12 KB,误差约 0.81%,PFMERGE 可合并多周期数据。
自测三问
- 写出「创建消费组 → 组内读取 → 确认 → 查看未确认列表」的命令序列。
- Stream 的消息在 XACK 后还在吗?宕机消费者的消息怎么交接?
- HyperLogLog 占多少内存、误差多少?什么情况下必须改用 Bitmap?
小结一句话
Stream 补上了 Redis 可靠消息的短板,Geo 让位置搜索开箱即用,HyperLogLog 用 12 KB 换亿级去重计数——三者分别对应「可靠、空间、近似」三类进阶需求。
延伸
- Stream 文档
- Geo 文档
- HyperLogLog 文档
- 下一课:Pub/Sub 消息——判题完成的实时广播及其与 Stream 的分工。