Redis 系列 11. Bitmap、HyperLogLog、GEO 实战


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 或专门的地理索引系统。

GEO 附近酒店查询示意

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。


文章作者: hnbian
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 hnbian !
评论
  目录