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 bit | 365 bit ≈ 46 Byte/用户 |
| 在线状态(每分钟) | 每分钟 1 bit | 1440 bit ≈ 180 Byte/用户 |
| 亿级用户黑白名单 | 每用户 1 bit | 1 亿 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,无论统计多少元素。
| 维度 | 精确 Set | HyperLogLog |
|---|---|---|
| 内存(1 亿去重) | 数十 GB | 12KB 恒定 |
| 精确度 | 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 拼接 | 拼接字符串 | ~百字节 |
| Bitfield | 90 bit | 12 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
| 数据 | 结构 | 命令 |
|---|---|---|
| 实时人气榜 | ZSet | ZADD/ZINCRBY/ZREVRANGE |
| 独立访客 | HyperLogLog | PFADD/PFCOUNT |
| 签到分布 | Bitmap | SETBIT/BITCOUNT |
| 用户画像 | Hash | HSET/HGETALL |
| 榜单归档 | ZSet 合并 | ZUNIONSTORE |
八、与普通类型的选型决策
8.1 选型决策表
| 业务问题 | 推荐类型 | 替代方案 | 选型理由 |
|---|---|---|---|
| 签到/在线/布尔标记 | Bitmap | Hash、Set | 内存最小,位运算高效 |
| 去重计数(UV) | HyperLogLog | Set、精确统计 | 12KB 恒定,允许近似 |
| 附近的人/距离 | GEO | 自算经纬度+Hash | 内置 geohash 与半径查询 |
| 判断存在(防穿透) | 布隆过滤器 | 空值缓存 | 不占大量内存,无漏判 |
| 可靠消息队列 | Stream | List、Pub/Sub、Kafka | 持久化+消费组+ACK |
| 紧凑计数器 | bitfield | 多 String | 位级批量操作 |
| 排行榜 | ZSet | List | 有序+分数更新 |
| 标签系统 | Set | Hash | 天然支持并集/交集 |
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 亿 UV | 1 亿用户签到(年) | 百万商家附近查询 |
|---|---|---|---|
| 精确 Set/Hash | 数 GB | 数 GB | 高内存 |
| 高级结构 | 12KB | ~460MB | 内置索引,低内存 |
高级数据结构的共同哲学是"用可控的精度/语义损失,换取数量级的内存与时间收益"。选择的标准不是"哪个更高级",而是"业务能否接受这种近似或约束"。
结语
高级数据结构是 Redis 工程师的"杠杆工具",核心要点回顾:
- Bitmap 用位级存储解决布尔标记与去重,签到、在线状态、黑白名单的极致内存方案
- HyperLogLog 用 12KB 恒定内存解决海量去重计数,±0.81% 误差换来的是数量级的容量节省
- GEO 内置 geohash 与半径查询,一行命令解决"附近的人",6.2+ 优先用
GEOSEARCH - 布隆过滤器 用"无漏判"特性防缓存穿透,但要注意不能删除元素、需 RedisBloom 模块
- Stream 是唯一适合可靠消息队列的类型,消费组 + Pending +
XAUTOCLAIM构成完整消费语义 - bitfield 把大量小整数压缩在一个 Key 里,配合溢出控制实现紧凑计数器
选型决策始终是"精度 vs 成本"的权衡:业务能接受近似,就用 HyperLogLog;需要精确名单,就用 Set;判断存在但不怕误判,就用布隆过滤器。理解了每种结构的内存与语义代价,你就能在正确的场景用最小的代价解决问题。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。