高级数据结构实战:Bitmap、HyperLogLog、GEO、布隆过滤器与 Stream

Redis 高级数据结构实战:Bitmap(签到/在线状态/位图去重)、HyperLogLog(UV 统计)、GEO(附近的人)、布隆过滤器(Bloom Filter 模块防缓存穿透)、Stream 消费组进阶、bitfield 位运算、组合应用案例(实时排行榜+UV)与选型决策

Redis 的价值远不止 String/Hash 五个基础类型。在生产中,Bitmap 能把签到、在线状态的存储量压缩到极致;HyperLogLog 用不到 12KB 就统计上亿 UV;GEO 一行命令解决"附近的人";布隆过滤器在缓存穿透防护中立下汗马功劳;而 Stream 是唯一真正适合做可靠消息队列的类型。

本文逐个实战这些高级数据结构,给出真实命令、适用场景与组合案例,并在最后给出与普通类型的选型决策,帮你构建"用最小内存解决复杂问题"的能力。


一、Bitmap 位图:签到、在线状态与位级去重

1.1 什么是 Bitmap

Bitmap 并不是独立的数据类型,它本质上是 String 类型的位操作视图,一个 String 最多 512MB,即最多 2^32 个 bit(约 42 亿个位)。每个 bit 只有 0/1 两种状态,非常适合"布尔型"的海量记录。

场景位含义内存估算
用户签到(1 年)每天 1 bit365 bit ≈ 46 Byte/用户
在线状态(每分钟)每分钟 1 bit1440 bit ≈ 180 Byte/用户
亿级用户黑白名单每用户 1 bit1 亿 bit ≈ 12MB

1.2 用户签到实战

以"2026 年 9 月"为例,用户 1001 的签到位图 Key 为 sign:202609:1001,第 N 天对应第 N-1 位:

# 第 5 天签到:设置第 4 位为 1
redis-cli SETBIT sign:202609:1001 4 1

# 查询第 5 天是否签到
redis-cli GETBIT sign:202609:1001 4
# (integer) 1

# 统计本月签到总天数
redis-cli BITCOUNT sign:202609:1001
# (integer) 5

# 找出本月第一次签到(第一个为 1 的 bit)
redis-cli BITPOS sign:202609:1001 1
# (integer) 0   # 表示第 1 天

# 连续签到统计:当月签到位做 BITOP 运算后 BITCOUNT
redis-cli BITOP AND today sign:202609:1001 sign:202609:1001

签到系统的标准做法:签到天数 BITCOUNT、是否签到 GETBIT、首次签到 BITPOS、连续签到用位运算判断。1 亿用户一年的签到数据用 Bitmap 仅约 4.3GB,远小于用 Hash 存储的数量级。

1.3 在线状态与去重

# 每个用户映射到固定的 bit 位,1 表示在线
redis-cli SETBIT online:20260927 1001 1
redis-cli SETBIT online:20260927 1002 1

# 查询用户 1001 是否在线
redis-cli GETBIT online:20260927 1001

# 统计在线总人数
redis-cli BITCOUNT online:20260927

# 位图去重:统计两个用户集合的交集
redis-cli BITOP AND both_online user_a online user_b online
redis-cli BITCOUNT both_online

1.4 BITFIELD:位级批量操作

BITFIELD 支持在一个位图中批量 GET/SET/INCRBY 任意长度的子字节段:

# 把 32 位分成 4 个 8 位计数器,实现 4 个独立计数器
redis-cli BITFIELD counter:user SET u8 0 5 SET u8 8 10 SET u8 16 3
# 1) (integer) 0  2) (integer) 0  3) (integer) 0

# 对第 2 个 8 位计数器自增
redis-cli BITFIELD counter:user INCRBY u8 8 1
# 1) (integer) 11

# 带溢出控制(SAT 饱和、FAIL 报错)
redis-cli BITFIELD counter:user INCRBY u8 16 200 OVERFLOW SAT
# 1) (integer) 255   # 饱和在 255
参数说明
GET type offset读取指定子字节
SET type offset value设置指定子字节
INCRBY type offset inc对指定子字节自增
OVERFLOW WRAP/SAT/FAIL溢出策略:回绕/饱和/报错

二、HyperLogLog:UV 统计与去重计数

2.1 原理与内存

HyperLogLog 用概率算法统计集合基数(去重数量),标准实现误差约 0.81%,每个 Key 恒定占 约 12KB,无论统计多少元素。

维度精确 SetHyperLogLog
内存(1 亿去重)数十 GB12KB 恒定
精确度100%±0.81%
适用范围需精确去重允许近似(UV、PV)

HyperLogLog 只适合"知道大概有多少个不同值",不适合需要精确名单的统计。误差 0.81% 对绝大多数运营指标(UV)完全够用。

2.2 UV 统计实战

# 用户访问页面时,PFADD 记录用户 ID
redis-cli PFADD pv:index:20260927 "user_1001" "user_1002" "user_1003"
# (integer) 1

# 统计当日独立访客
redis-cli PFCOUNT pv:index:20260927
# (integer) 3

# 多日数据合并(如周报 UV):PFMERGE
redis-cli PFMERGE pv:index:week1 \
  pv:index:20260921 pv:index:20260922 pv:index:20260923
redis-cli PFCOUNT pv:index:week1

客户端集成非常简单:Java 用 opsForHyperLogLog().add()/size(),Python 用 r.pfadd()/r.pfcount(),Go 用 rdb.PFAdd()/rdb.PFCount(),语义与命令行完全一致。

2.3 注意事项

  • PFADD 的返回值是"基数是否发生变化",不是元素数量
  • 一个 HyperLogLog 超过 12KB 不再增长,但不同 Key 不能相加得到总数,必须 PFMERGE
  • 误报率可通过 PFCOUNT 的误差区间理解:±0.81%(64 个寄存器)

三、GEO 地理位置:附近的人

3.1 基础命令

GEO 基于 ZSet 实现,内部用 geohash 编码经纬度,支持按经纬度、按成员查询附近:

# 添加商家位置:GEOADD key 经度 纬度 成员
redis-cli GEOADD shop:loc 120.153576 30.287459 "shop_001"
redis-cli GEOADD shop:loc 120.161512 30.278007 "shop_002"

# 计算两个商家距离(km/m/mi/ft)
redis-cli GEODIST shop:loc shop_001 shop_002 km
# "1.1240"

# 以某坐标为中心,查 3 公里内的商家(带距离)
redis-cli GEORADIUS shop:loc 120.158 30.282 3 km WITHDIST
# 1) 1) "shop_001" 2) "0.1234"
# 2) 1) "shop_002" 2) "0.8721"

# Redis 6.2+ 推荐用 GEOSEARCH(语义更清晰)
redis-cli GEOSEARCH shop:loc FROMLONLAT 120.158 30.282 \
  BYRADIUS 3 km ASC WITHDIST

# 获取成员的 geohash 编码
redis-cli GEOHASH shop:loc shop_001

3.2 附近的人实战

客户端封装也已就绪:Spring Data Redis 用 opsForGeo().radius(key, circle, args)(配合 includeDistance()、sortAscending()),Java/Go/Python 各客户端均有 GeoSearch 等价 API。

场景命令说明
附近门店GEOSEARCH FROMLONLAT ... BYRADIUS按坐标查附近
附近的人GEOSEARCH FROMMEMBER user ... BYRADIUS按成员查附近
距离计算GEODIST两成员直线距离
地理围栏GEOSEARCHSTORE结果落库做区域判断

注意:GEORADIUS 在 Redis 6.2 起标记为已弃用,官方推荐 GEOSEARCH。两者功能等价,GEOSEARCH 语义更清晰、性能相当。


四、布隆过滤器:缓存穿透防护

4.1 原理

布隆过滤器(Bloom Filter)用 k 个哈希函数把元素映射到位数组,用"可能存在/一定不存在"的判定实现海量数据的快速判重,代价是有误判率(假阳性),但绝无漏判。

特性值
内存(1 亿元素,1% 误判率)约 100MB
查询时间复杂度O(k),k 为哈希函数数
漏判(假阴性)无
误判(假阳性)可控,随内存/哈希数下降

Redis 原生类型没有布隆过滤器,需加载 RedisBloom 模块(Redis Stack 自带)。生产中它最常见的用途是防缓存穿透:DB 查询前先过布隆过滤器,不存在的 key 直接被拦截。

4.2 RedisBloom 命令实战

# 预留容量:BF.RESERVE key 误判率 预计元素数
redis-cli BF.RESERVE bloom:user:2026 0.01 10000000

# 添加元素
redis-cli BF.ADD bloom:user:2026 "user_1001"
redis-cli BF.ADD bloom:user:2026 "user_1002"

# 批量判断存在性
redis-cli BF.MEXISTS bloom:user:2026 "user_1001" "user_999999"
# 1) (integer) 1        # 存在(或误判)
# 2) (integer) 0        # 一定不存在

# 查看过滤器信息
redis-cli BF.INFO bloom:user:2026
# Capacity:10000000
# Size:126MB
# Number of filters:1

4.3 防缓存穿透完整流程

请求到达后先过布隆过滤器:判定"一定不存在"则直接返回空(拦截恶意穿透),判定"可能存在"则走 查 Redis → 未命中 → 查 DB 的链路,DB 中存在则写回 Redis 并 BF.ADD 记录,不存在则不缓存。初始化阶段用 redis-cli --pipe < /data/user_ids.bfadd.txt 批量预热已有 ID。

布隆过滤器的最大陷阱是不能删除元素(删除会影响其他元素判定)。对"可能误删"的业务(如注销用户),需定期重建过滤器,或使用支持删除的 Counting Bloom Filter(RedisBloom 也提供 CF.* 命令族,Cuckoo Filter)。


五、Stream 消费组进阶

5.1 消费组核心流程

Stream 是 Redis 5.0 引入的类消息队列结构,支持持久化、消费组、ACK 与 Pending 机制:

# 生产者写入
redis-cli XADD order:stream * type "create" orderId "1001"
# "1698888888888-0"

# 创建消费组,从头消费
redis-cli XGROUP CREATE order:stream group_pay 0

# 消费者读取(以 pay_worker_1 身份)
redis-cli XREADGROUP GROUP group_pay pay_worker_1 \
  COUNT 10 STREAMS order:stream ">"

# 消费成功后确认
redis-cli XACK order:stream group_pay 1698888888888-0
# (integer) 1

5.2 消费失败与重试:Pending 机制

未 ACK 的消息进入 Pending 列表,可用 XPENDING 查看,用 XAUTOCLAIM(Redis 6.2+)自动转移超时未 ACK 的消息给其他消费者:

# 查看 Pending 消息
redis-cli XPENDING order:stream group_pay
# 1) (integer) 3        # 未 ACK 总数
# 2) "1698888888888-0"
# 3) "1698888888900-0"

# 转移超过 30 秒未 ACK 的消息给 pay_worker_2(最小空闲时间 30000ms)
redis-cli XAUTOCLAIM order:stream group_pay pay_worker_2 30000 0-0
# 1) "1698888888888-0" 2) (nil) ...

5.3 消费组状态监控

# 查看消费组信息
redis-cli XINFO GROUPS order:stream
# 1) 1) "name" 2) "group_pay"
#    3) "consumers" 4) (integer) 2
#    5) "pending" 6) (integer) 3
#    7) "last-delivered-id" 8) "1698888888900-0"

# 查看 Stream 全貌(长度、首尾条目、消费组数)
redis-cli XINFO STREAM order:stream
# length:1500
# groups:1
# last-generated-id:1698888888900-0
命令用途
XADD追加消息
XREADGROUP消费组读取
XACK确认消费
XPENDING查看未 ACK 消息
XAUTOCLAIM自动转移超时消息
XINFO GROUPS/STREAM组与流状态
XTRIM按长度/时间裁剪(防止无限增长)

Stream 与 Kafka 的关键差异:Stream 不支持分区内乱序重排、无内置的重平衡算法。它适合单分片可靠队列、事件流等场景;追求严格分区有序与重平衡能力时仍应选 Kafka。


六、bitfield:位级计数器与紧凑存储

6.1 用 bitfield 实现多个计数器

一个用户可能同时有多个"是否某天"的布尔记录(登录、下单、领取),用 bitfield 把它们压缩在同一个 Key:

# offset 0-29: 登录记录(30 天)
# offset 30-59: 下单记录
# offset 60-89: 领取优惠券记录
redis-cli BITFIELD user:1001:flag SET u1 0 1 SET u1 1 1 SET u1 30 1

# 读取
redis-cli BITFIELD user:1001:flag GET u1 0 GET u1 1 GET u1 30
# 1) (integer) 1  2) (integer) 1  3) (integer) 1

6.2 与普通类型的内存对比

方案30 天 × 3 类记录内存
Hash 存布尔90 个字段~KB 级
String 拼接拼接字符串~百字节
Bitfield90 bit12 Byte

bitfield 适合"大量布尔/小整数标志位"的紧凑存储,配合 OVERFLOW SAT 还能实现计数器防溢出。它本质仍是 String,大 Key 风险同样适用。


七、组合应用案例:实时排行榜 + UV

7.1 案例需求

一个直播活动页需要同时提供:实时人气榜(ZSet)、独立访客数(HyperLogLog)、今日签到分布(Bitmap)、用户画像(Hash)。四类数据用四种数据结构组合:

# 1. 实时人气榜:ZSet,score=人气值
redis-cli ZADD live:rank:20260927 15000 "anchor_001"
redis-cli ZADD live:rank:20260927 12000 "anchor_002"
redis-cli ZINCRBY live:rank:20260927 500 "anchor_001"   # 人气+500

# 2. 独立访客:HyperLogLog
redis-cli PFADD live:uv:20260927 "visitor_1" "visitor_2"

# 3. 签到分布:Bitmap(按用户维度的月份位图)
redis-cli SETBIT sign:202609:1001 4 1

# 4. 用户画像:Hash
redis-cli HSET user:1001 name "张三" level 5 gold 1200

7.2 榜单读取与定期归档

# 榜单 Top 3
redis-cli ZREVRANGE live:rank:20260927 0 2 WITHSCORES
# 1) "anchor_001" 2) "15500"
# 3) "anchor_002" 4) "12000"

# 每日归档:把当日榜单复制到月度 Key
redis-cli ZUNIONSTORE live:rank:202609 1 live:rank:20260927 WEIGHTS 1

# 定期清理:到期删除当日榜单,避免 Key 堆积
redis-cli EXPIRE live:rank:20260927 86400
数据结构命令
实时人气榜ZSetZADD/ZINCRBY/ZREVRANGE
独立访客HyperLogLogPFADD/PFCOUNT
签到分布BitmapSETBIT/BITCOUNT
用户画像HashHSET/HGETALL
榜单归档ZSet 合并ZUNIONSTORE

八、与普通类型的选型决策

8.1 选型决策表

业务问题推荐类型替代方案选型理由
签到/在线/布尔标记BitmapHash、Set内存最小,位运算高效
去重计数(UV)HyperLogLogSet、精确统计12KB 恒定,允许近似
附近的人/距离GEO自算经纬度+Hash内置 geohash 与半径查询
判断存在(防穿透)布隆过滤器空值缓存不占大量内存,无漏判
可靠消息队列StreamList、Pub/Sub、Kafka持久化+消费组+ACK
紧凑计数器bitfield多 String位级批量操作
排行榜ZSetList有序+分数更新
标签系统SetHash天然支持并集/交集

8.2 何时不要用高级类型

  • 需要精确名单(如精确到个位的营销对账)——不要用 HyperLogLog
  • 需要删除元素(动态增删的集合)——慎用布隆过滤器
  • 跨分片大集合运算——Bitmap/Bitfield 本质是大 String,集群下可能触发大 Key
  • 强一致消费语义(如金融对账)——Stream 的 Pending 重试不保证幂等,需要应用层去重

8.3 内部编码视角

用 TYPE 与 OBJECT ENCODING 可观察真实存储形态:Bitmap 和 HyperLogLog 都以 string 存储(HLL 内部用 16384 个寄存器 + 稀疏/密集编码),大 ZSet 用 skiplist + dict,小数据量时则压缩为 listpack。理解编码才能解释内存差异。


九、生产实践与监控

9.1 使用要点

要点说明
命名规范携带维度与时间:sign:202609:1001、live:uv:20260927
TTL 管理按天/月的 Key 必须设置过期或归档,防止堆积
位图上限Bitmap 单 Key ≤ 512MB,超大规模用户集需分片
模块依赖布隆过滤器需 RedisBloom,K8s 用 redis-stack 镜像
与 cluster 兼容位运算 BITOP 对多 Key 要求同槽(hash tag)

9.2 监控与巡检

# 巡检高级类型 Key 的规模与内存,发现大 Key 风险
redis-cli MEMORY USAGE live:rank:20260927
redis-cli --bigkeys --types zset,string

建议为高级类型 Key 建立定时巡检:UV 用 PFCOUNT 采样上报、榜单用 ZREVRANGE 拉取 Top 变化、内存用 MEMORY USAGE 监控,防止位图/ZSet 因业务累积膨胀成大 Key。

9.3 内存收益总结

方案1 亿 UV1 亿用户签到(年)百万商家附近查询
精确 Set/Hash数 GB数 GB高内存
高级结构12KB~460MB内置索引,低内存

高级数据结构的共同哲学是"用可控的精度/语义损失,换取数量级的内存与时间收益"。选择的标准不是"哪个更高级",而是"业务能否接受这种近似或约束"。


结语

高级数据结构是 Redis 工程师的"杠杆工具",核心要点回顾:

  1. Bitmap 用位级存储解决布尔标记与去重,签到、在线状态、黑白名单的极致内存方案
  2. HyperLogLog 用 12KB 恒定内存解决海量去重计数,±0.81% 误差换来的是数量级的容量节省
  3. GEO 内置 geohash 与半径查询,一行命令解决"附近的人",6.2+ 优先用 GEOSEARCH
  4. 布隆过滤器 用"无漏判"特性防缓存穿透,但要注意不能删除元素、需 RedisBloom 模块
  5. Stream 是唯一适合可靠消息队列的类型,消费组 + Pending + XAUTOCLAIM 构成完整消费语义
  6. bitfield 把大量小整数压缩在一个 Key 里,配合溢出控制实现紧凑计数器

选型决策始终是"精度 vs 成本"的权衡:业务能接受近似,就用 HyperLogLog;需要精确名单,就用 Set;判断存在但不怕误判,就用布隆过滤器。理解了每种结构的内存与语义代价,你就能在正确的场景用最小的代价解决问题。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「redis」更多文章

  1. Redis 向量检索实战:RediSearch、HNSW 与 Embedding 管道集成
  2. 缓存一致性终极方案:双删、binlog 订阅与最终一致性架构
  3. 过期键淘汰与内存回收深入:expire cycle、驱逐策略与碎片整理