1. 这三种结构解决什么问题
Redis 除了五大基础类型,还有几个非常适合做统计和位置计算的结构:Bitmap、HyperLogLog、GEO。
| 结构 | 适合场景 | 关键特点 |
|---|---|---|
| Bitmap | 签到、活跃标记、布尔状态 | 用 bit 表示是否发生,占用内存低 |
| HyperLogLog | UV、独立用户数估算 | 近似去重统计,内存固定且很小 |
| GEO | 附近门店、附近的人 | 基于经纬度做距离和范围查询 |
可以把几类统计需求放在一起看:聚合统计、排序统计、二值统计、基数统计。Redis 不同数据结构解决的是不同统计问题,不应使用 String 结构承载所有统计语义。
2. Bitmap 不是新数据类型
Bitmap 基于 Redis string 的 bit-level 操作。它不是新的底层类型,只是 Redis 提供了按 bit 读写的命令,可以把一个二值状态映射到一个 offset 上,每个 offset 对应一个 bit。
setbit sign:202411:1001 0 1
getbit sign:202411:1001 0
bitcount sign:202411:1001
这里 sign:202411:1001 表示用户 1001 在 2024 年 11 月的签到状态。offset 从 0 开始,11 月 1 日可以用 offset 0,11 月 2 日用 offset 1。
offset 本质是 bit index,不是业务日期本身。日期、用户 ID、活跃状态都只是业务层对 bit index 的映射规则。
Bitmap 的优势是省内存。一个月 31 天只需要 31 个 bit,不需要存 31 个字符串或 31 条记录。
Bitmap 支持的最大 offset 是 2^32 - 1,也就是最多可以使用 2^32 个 bit。这个上限来自 Redis string 最大 512MB 的限制。换算下来,约 512MB 内存可以表示 42.9 亿个二值状态。这里说的是 bit 状态数量,不是 42.9 亿字节。
3. Bitmap 签到案例
用户签到:
# 用户 1001 在 2024-11-21 签到,11 月 21 日对应 offset 20
setbit sign:202411:1001 20 1
判断今天是否签到:
getbit sign:202411:1001 20
统计当月签到天数:
bitcount sign:202411:1001
统计多个用户某天是否活跃,可以换一个 key 维度:
# 2024-11-21 这一天,用户 1001 活跃
setbit active:20241121 1001 1
# 当天活跃用户数
bitcount active:20241121
这种写法要求用户 ID 不能太稀疏。如果用户 ID 动不动就是几十亿,直接拿用户 ID 当 offset 会浪费大量空间,需要做映射或换方案。
签到场景适合按月或按固定周期使用 Bitmap。如果要做跨月连续签到、动态窗口统计,通常需要在应用层合并多个 Bitmap,或者改用更适合时间窗口的存储模型。
4. Bitmap 连续签到怎么算
Redis 可以用 BITFIELD 取出一段 bit,再在应用里计算连续 1 的个数。
bitfield sign:202411:1001 get u21 0
表示从 offset 0 开始读取 21 位无符号整数。BITFIELD 返回的是整数,不是 bit array。应用拿到这个整数后,从低位或高位按约定计算连续签到天数。
上面的示例里,offset 0 表示 11 月 1 日,读取 u21 后最低位对应第 21 天,所以循环检查低位可以计算截至 11 月 21 日的连续签到天数。
Java 伪代码:
long bits = redisTemplate.opsForValue()
.bitField("sign:202411:1001",
BitFieldSubCommands.create().get(BitFieldType.unsigned(21)).valueAt(0))
.get(0);
int days = 0;
while ((bits & 1) == 1) {
days++;
bits >>= 1;
}
实际实现时要统一 bit 顺序。不要今天用 offset 0 表示 1 号,明天又从末尾倒着算。Java 里做位运算时尤其要明确 LSB / MSB 的读取约定,否则很容易把连续签到方向算反。
5. HyperLogLog 统计 UV
HyperLogLog 用于基数统计,也就是“去重后有多少个”。典型场景是 UV:
pfadd uv:20241121 user:1 user:2 user:3
pfadd uv:20241121 user:2
pfcount uv:20241121
PFCOUNT 返回的是近似值,不是精确值。Redis HyperLogLog 的标准误差大约是 0.81%,这是概率误差,不是每次都固定偏差 0.81%。它的好处是每个 key 只需要很小的固定空间。
这意味着:
| 场景 | 是否适合 HLL |
|---|---|
| 首页 UV 趋势 | 适合 |
| 活动 UV 大盘 | 适合 |
| 结算、计费、审计 | 不适合 |
| 必须知道具体用户列表 | 不适合 |
如果需要精确去重并能查出用户列表,用 Set;如果只需要估算数量,用 HyperLogLog。
HyperLogLog 不保存可回溯的元素明细,不能判断某个用户是否出现过,也不能反查用户列表。因此它适合 UV 趋势、大盘估算,不适合用户画像分析、计费、审计等需要明细和可复核的场景。
6. HyperLogLog 合并统计
多天 UV 合并:
pfmerge uv:202411 uv:20241101 uv:20241102 uv:20241103
pfcount uv:202411
这个命令适合做按天写入、按周或按月聚合。注意它仍然是近似统计,不能用于强精确场景。PFMERGE 是近似结构的合并,不会提高精度,也不能恢复元素明细。
关于基数统计,可以抓住这个关键点:基数统计关注的是“集合里不重复元素的数量”,不是元素明细。
7. GEO 前置知识:经纬度
经度(longitude):东经为正数,西经为负数,表示东西经。
纬度(latitude):北纬为正数,南纬为负数,表示南北纬。
使用 Redis GEO 时要注意参数顺序:GEOADD key longitude latitude member,也就是先经度、后纬度。
8. GEO 存门店位置
Redis GEO 底层基于 Sorted Set 和 geohash encoding 编码经纬度信息。常用命令:
geoadd shop:geo 113.93041 22.53332 shop:1001
geoadd shop:geo 113.93450 22.54120 shop:1002
geodist shop:geo shop:1001 shop:1002 km
geopos shop:geo shop:1001
查询某个坐标附近 5 公里门店:
geosearch shop:geo fromlonlat 113.93041 22.53332 byradius 5 km withdist asc count 20
WITHDIST 会返回距离,WITHCOORD 会返回坐标,两个选项都加上时,返回结构会比只返回 member 更复杂,业务解析时要固定返回字段顺序。
老版本里常见的是 GEORADIUS,新版本更推荐使用 GEOSEARCH。
9. GEO 附近酒店案例
写入酒店位置:
geoadd hotel:shenzhen 113.92313 22.54090 hotel:1
geoadd hotel:shenzhen 113.94022 22.54833 hotel:2
geoadd hotel:shenzhen 113.95566 22.53012 hotel:3
查询当前位置附近 3 公里酒店:
geosearch hotel:shenzhen \
fromlonlat 113.93041 22.53332 \
byradius 3 km \
withcoord withdist asc count 10
返回值可以拿到 member、距离和坐标。业务侧再根据 hotel ID 批量查详情缓存或数据库。
附近查询结果会受距离计算、浮点精度和相同距离排序影响,不应该把返回顺序当成绝对稳定排序。如果业务需要复杂地理围栏、polygon、geofence 或更强的地理检索能力,通常要考虑 Elasticsearch、PostGIS 或专门的地理索引系统。

10. 三种结构怎么选
| 需求 | 推荐结构 | 原因 |
|---|---|---|
| 判断用户今天是否签到 | Bitmap | 二值状态,空间低 |
| 统计用户当月签到天数 | Bitmap | BITCOUNT 直接统计 |
| 统计页面 UV | HyperLogLog | 近似去重,内存小 |
| 查 UV 用户明细 | Set | HLL 不能反查元素 |
| 附近门店 | GEO | 内置距离和范围查询 |
| 排行榜 | ZSet | GEO 不是通用排序结构 |
Redis 的高级结构要按问题选择。Bitmap 不适合稀疏超大 offset,HyperLogLog 不适合精确明细,GEO 不适合复杂地理围栏。知道边界,比记命令更重要。
可以用三个模型总结:
| 结构 | 数据模型 | 是否可反查 | 核心边界 |
|---|---|---|---|
| Bitmap | exact binary state | 可以按 offset 反查状态 | 稀疏 offset 会浪费内存 |
| HyperLogLog | approximate cardinality | 不可以反查元素 | 只能估算基数,不能审计 |
| GEO | spatial index | 可以返回 member 和坐标 | 不适合复杂地理围栏 |
如果要精确判断成员是否存在并能列出明细,用 Set 或 Bitmap;如果要判断“可能存在且容忍误判”,可以考虑 Bloom Filter;如果只要估算独立数量,用 HyperLogLog;如果是地理附近查询,用 GEO,但复杂空间检索要交给更专业的 geo index。