Redis 进阶数据类型实战:Bitmap、HyperLogLog 与 GEO

很多开发者对 Redis 的认知停留在 String、Hash、List、Set、ZSet 五种基础类型,遇到「海量用户签到」「页面 UV 统计」「附近门店查询」这类需求时,往往会引入更复杂的外部组件。其实 Redis 进阶数据类型 Bitmap、HyperLogLog 与 GEO 正是为这些场景而生:它们用极小的内存代价,就能完成基础类型难以胜任的统计与检索任务。本文用可直接复制的命令行与 Python 示例,带你把这三块补齐。

一、为什么需要进阶数据类型

基础五类型解决的问题是「存什么」,而 Bitmap、HyperLogLog、GEO 解决的是「怎么用极低成本完成特定统计/检索」。例如统计 1 亿用户的日签到状态,若用 Set 存用户 ID,内存可能上 GB;用 Bitmap 只需约 12 MB。再比如统计页面 UV,精确去重代价高,而 HyperLogLog 只用 12 KB 就能得到误差 0.81% 以内的近似值。GEO 则把经纬度编码进有序集合,让「附近的人/店」这类空间查询变得一行命令即可完成。它们都建立在 Redis 已有的底层结构之上,学习曲线平缓,却能在关键场景替你省下大量资源。

二、Bitmap 位图:用极小空间做海量布尔统计

Bitmap 本质是 String 类型按 bit 寻址的视图,每一位代表一个布尔状态(0/1)。它最适合「是/否」类统计:签到、在线状态、活跃标记等。偏移量(offset)通常放用户 ID 或日期序号。

2.1 核心命令

# 记录用户 10001 在 2026-11-01 签到(offset=用户ID)
SETBIT sign:2026-11-01 10001 1

# 检查该用户是否签到
GETBIT sign:2026-11-01 10001

# 统计当日签到总人数
BITCOUNT sign:2026-11-01

# 取反/或/异或:BITNOT BITOR BITXOR
# 统计最近 7 天「每天都签到」的用户(按位与)
BITOP AND sign:7d sign:2026-10-26 sign:2026-10-27 sign:2026-10-28 \
                sign:2026-10-29 sign:2026-10-30 sign:2026-10-31 sign:2026-11-01
BITCOUNT sign:7d

2.2 实战:用 Python 做签到与留存

import redis

r = redis.Redis(host="127.0.0.1", port=6379, db=0, decode_responses=True)

def sign(uid: int, day: str):
    r.setbit(f"sign:{day}", uid, 1)

def is_signed(uid: int, day: str) -> bool:
    return r.getbit(f"sign:{day}", uid) == 1

def daily_count(day: str) -> int:
    return r.bitcount(f"sign:{day}")

def retained_users(days: list[str]) -> int:
    dest = "sign:retained"
    r.bitop("AND", dest, *[f"sign:{d}" for d in days])
    return r.bitcount(dest)

sign(10001, "2026-11-01")
print(is_signed(10001, "2026-11-01"))   # True
print(daily_count("2026-11-01"))        # 当日签到人数

Bitmap 的内存开销很容易估算:1 亿个用户,每位仅占 1 bit,总计约 1e8 / 8 ≈ 12.5 MB;而如果用 Set 存储整数 ID,每个元素至少几十字节,轻松突破 GB 级。因此当状态维度是「是否存在」、且对象可以用整数偏移量表示时,Bitmap 几乎是唯一合理的选择。它的另一大优势是 BITOP 系列命令能对多天、多标签的位图做交集、并集与异或,瞬间算出留存率、重合度等指标,而无需把数据拉回应用层遍历。 Bitmap 的妙处在于 BITOP 可以按位聚合多个日期的签到图,几毫秒内算出「连续活跃」「周留存」等指标,无需扫全表。关于 Redis 的基础结构选型,可回顾 Redis 核心数据结构实战;若要做幂等防重,Bitmap 也是轻量方案之一,思路与 幂等设计实战 中的去重位图一致。

三、HyperLogLog:近似去重与 UV 统计

HyperLogLog(HLL)是一种概率数据结构,用来估算一个集合的基数(不重复元素个数)。它固定占用约 12 KB 内存,标准误差约 0.81%,非常适合 UV、独立访客、搜索词去重等「要快要省、允许微小误差」的场景。

3.1 核心命令

# 向当日 UV 集合添加访客(重复添加不计数)
PFADD uv:2026-11-01 user_10001 user_10002 user_10003

# 估算当日 UV
PFCOUNT uv:2026-11-01

# 合并多天 UV(并集去重),结果写入新 key
PFMERGE uv:week uv:2026-11-01 uv:2026-11-02 uv:2026-11-03
PFCOUNT uv:week

3.2 实战:分维度 UV 与合并

import redis
r = redis.Redis(host="127.0.0.1", port=6379, db=0, decode_responses=True)

# 记录一次访问(不同维度分别累加)
r.pfadd("uv:pc:2026-11-01", "u_10001")
r.pfadd("uv:mobile:2026-11-01", "u_10001")  # 同一用户跨端会分别计数

# 合并全端 UV(自动去重)
r.pfmerge("uv:all:2026-11-01", "uv:pc:2026-11-01", "uv:mobile:2026-11-01")
print(r.pfcount("uv:all:2026-11-01"))
维度精确方案(Set)HyperLogLog
1 亿 UV 内存数百 MB ~ GB约 12 KB
误差0≈0.81%
能否取明细能不能(只有估算值)
合并成本高极低(PFMERGE)

需要强调的是,HyperLogLog 的 12 KB 是固定开销,无论你往里 pfadd 一万个还是一亿个元素,内存都保持不变,这正是它在大数据量 UV 场景下碾压 Set 的根本原因。代价是它只能回答「大约多少」,无法给出具体是哪些元素,也不能做精确去重后的明细导出。如果既想要近似值、又偶尔需要精确核对,常见做法是「HLL 常态统计 + 周期性抽样精确校准」。 选型要点:当你只关心「大概有多少人」而不需要把每个人列出来时,HLL 几乎总是更优解。需要精确去重且数据量不大时,再用 Set 或 Bitmap。HLL 与布隆过滤器思路不同——布隆用于「是否存在」判断(见 布隆过滤器实战),HLL 用于「有多少个」估算。

四、GEO 地理索引:附近的人与门店

Redis 的 GEO 命令基于有序集合(ZSet)实现,把经纬度用 GeoHash 编码成分数,从而支持半径查询、距离计算。它非常适合「附近门店」「附近的司机」「地理围栏」等 LBS 场景,无需引入专业空间数据库即可起步。

4.1 写入与基础查询

# 添加门店(经度 纬度 名称)
GEOADD stores 116.404 39.915 "门店A" 116.410 39.920 "门店B" 116.398 39.908 "门店C"

# 取回坐标
GEOPOS stores 门店A

# 计算两门店直线距离(单位 km)
GEODIST stores 门店A 门店B km

4.2 半径检索:查找附近门店

# 以某坐标为中心,2 km 内的门店,按距离升序返回
GEOSEARCH stores FROMLONLAT 116.405 39.916 BYRADIUS 2 km ASC

# 同时返回距离
GEOSEARCH stores FROMLONLAT 116.405 39.916 BYRADIUS 2 km ASC \
            WITHDIST

# 把检索结果(含距离)存为新 key,便于缓存
GEOSEARCHSTORE stores:near:116.405:39.916 stores \
            FROMLONLAT 116.405 39.916 BYRADIUS 2 km ASC STOREDIST

在 Python 中调用同样简单:

import redis
r = redis.Redis(host="127.0.0.1", port=6379, db=0, decode_responses=True)
r.geoadd("stores", (116.404, 39.915, "门店A"), (116.410, 39.920, "门店B"))
near = r.geosearch("stores", longitude=116.405, latitude=39.916,
                   radius=2, unit="km", sort="ASC", withdist=True)
for name, dist in near:
    print(name, f"{dist:.2f}km")

GEO 实际上把经纬度编码进 52 位整数再存入 ZSet,因此所有 GEO 命令本质上都是在操作底层有序集合。这也意味着你可以用 ZRANGE、ZREM 等通用命令管理成员,也能借助 ZSet 的过期与分片能力做容量规划。当数据量达到千万级、或需要多边形围栏、路径规划等复杂空间运算时,再迁移到 PostGIS 等专业空间数据库;在中小规模下,Redis GEO 已经足够轻量高效。 GEO 的底层是 ZSet,因此你可以用 Redis 其他结构 配合做缓存与失效策略;若服务容器化部署,参考 Docker 实战 把 Redis 跑在独立容器中更有利于资源隔离。

五、三大结构对比与选型

结构回答的问题典型场景内存特征
Bitmap某状态是否为真 / 计数签到、在线、活跃标记随 offset 上限线性增长
HyperLogLog大概有多少个(去重)UV、独立访客、搜索词固定约 12 KB
GEO附近有什么 / 多远门店、司机、地理围栏随点数增长,底层 ZSet

六、注意事项与常见坑

坑点说明与对策
Bitmap offset 过大offset 决定 String 长度,ID 非连续且巨大时会浪费内存,建议做归一化映射。
HLL 不可取明细只给估算值,审计/对账场景仍需精确方案,别混用。
GEO 精度GeoHash 有边界畸变,极近距离或跨反子午线需额外处理。
大 key 风险Bitmap/HLL 虽省内存,但 BITOP、PFMERGE 在大 key 上是阻塞操作,避免在主线程高峰执行。

进阶数据类型的关键,是「用对结构替代重方案」。把签到交给 Bitmap、把 UV 交给 HyperLogLog、把附近检索交给 GEO,往往能用几 KB 到几十 MB 的代价,顶替掉一整套外部服务。它们与 Redis 基础五类型、Stream 消息队列 组合使用,足以覆盖绝大多数缓存与统计类业务。

七、小结

Bitmap、HyperLogLog、GEO 是 Redis 进阶数据类型中最常被低估却最实用的一组:Bitmap 用位运算做海量布尔统计,HyperLogLog 用 12 KB 换近似去重,GEO 用一行命令完成空间检索。掌握它们,你就能在签到、UV、LBS 等经典场景里做出既省资源又易维护的设计。下一步可以结合业务压测,确认大 key 操作的耗时是否在可接受范围内。

上一篇 前端图片优化实战:WebP 与 AVIF 格式选型
最新评论
Docker 网络模式详解:bridge/host 生产选型 - FSData
Docker 网络模式详解:bridge/host 生产选型 - FSData
8月20日
[…] 注意最后那条:把端口绑定到 127.0.0.1,再由宿主机上的 Nginx 反代进来,比直接 -p 80:80 暴露安全得多,也方便统一做
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
8月20日
[…] Pinia 的工程化用法,可以参考我们之前的实战文章 《Vue3 组合式 API + Pinia:状态管理从入门到实战》,那里覆盖了 Sto
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
8月19日
[…] 状态,点一下就能起停容器,配合我们讲过的 Docker Compose 编排 […]
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
8月19日
[…] 缓存三大坑的本质都是”缓存失效导致请求穿透到数据库”。记住口诀:穿透用布隆、击穿用锁、雪崩用错峰。配合合理的过期策略
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
8月19日
[…] Docker 镜像部署。结合我们之前写的 Docker Compose 一键编排实战,可以在 CI […]