CodeRoad Map 程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。
由 程序员柏风 独立维护 · 关于作者
学习 更多
Stream / Geo / HyperLogLog — CodeRoadMap
Stream / Geo / HyperLogLog 流式日志、地理索引、基数估计。
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"
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 统计 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
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 报坐标越界 :经度须在 -180180、纬度 -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 换亿级去重计数——三者分别对应「可靠、空间、近似」三类进阶需求。
延伸