3.1 Bitmap与Geo:位与坐标的降维


文档摘要

3.1 Bitmap与Geo:位与坐标的降维 本节摘要:Bitmap 复用 String 的字节缓冲做按位寻址,一亿个布尔状态只要约 12MB;Geo 把经纬度交错编码成一个 52 位整数存进 Sorted Set,用分数范围查询实现"附近搜索"。两者都是"把数据变换到更便宜的维度"的同一条思路。 Bitmap:一个比特也是存储单元 用户 2026 年 8 月 22 日签到了,就在位图第 22 位写 1。底层数据结构没有任何新东西——就是 2.1 那个 SDS,只不过命令按位解释它: 补一个容易漏看的细节:SETBIT 一个从未写过的远端下标时,中间的空洞会被自动补零撑开——按第 900 万位写 1,这个键立刻长到约 1.1MB。所以"位图省内存"的前提从头到尾都是用户号稠密且连续;

3.1 Bitmap与Geo:位与坐标的降维

本节摘要:Bitmap 复用 String 的字节缓冲做按位寻址,一亿个布尔状态只要约 12MB;Geo 把经纬度交错编码成一个 52 位整数存进 Sorted Set,用分数范围查询实现"附近搜索"。两者都是"把数据变换到更便宜的维度"的同一条思路。

Bitmap:一个比特也是存储单元

用户 2026 年 8 月 22 日签到了,就在位图第 22 位写 1。底层数据结构没有任何新东西——就是 2.1 那个 SDS,只不过命令按位解释它:

补一个容易漏看的细节:SETBIT 一个从未写过的远端下标时,中间的空洞会被自动补零撑开——按第 900 万位写 1,这个键立刻长到约 1.1MB。所以"位图省内存"的前提从头到尾都是用户号稠密且连续;内部递增的整型 id 是最理想的素材,随机字符串 id 先做一层紧凑映射再上墙,否则省的内存全被空洞吃回去。

> SETBIT sign:u1001:202608 22 1 # 第22天签到 > GETBIT sign:u1001:202608 22 (integer) 1 > BITCOUNT sign:u1001:202608 # 本月累计签到天数 (integer) 15 > BITPOS sign:u1001:202608 1 0 # 第一次签到是第几天 > SETBIT sign:all:20260822 1001 1 # 全站签到墙,用户号为下标 > BITCOUNT sign:all:20260822 # 当日签到总人数

一亿用户一天的签到墙:一亿除以 8 约等于 12MB。换成每用户一行的表,是几百 MB 起步。位运算还赠送了关系运算:BITOP AND 算连续签到,OR 算并集活跃。

把一个"月度连续签到榜单"的完整过程走一遍。背景:运营要看 8 月每天连续签到的用户数,并给连续满 21 天的用户发券;操作:每天的活跃墙按键名存位图,连续性用逐日累积 AND 实现:

# 第一天先把8月1日的墙拷贝到累积键,之后每天 AND 前一天 > BITOP AND sign:streak:0802 sign:streak:0801 sign:active:0802 # 0803 当天:累积键再与当日活跃墙求交 > BITOP AND sign:streak:0803 sign:streak:0802 sign:active:0803 # 连续21天的那天,直接数累积墙上的1 > BITCOUNT sign:streak:0821 (integer) 38417 # 满额人数,发券量一目了然

解读:累积键好比滚雪球,第 N 天的连续签到墙只保留"从月初一路连续到今天"的用户,BITCOUNT 一条命令出结果。变式:想要"近 7 天内任意 3 天活跃"这类宽松口径,把逐日墙 OR 到一起再数每个用户的置位数,改用 BITCOUNT 的区间版逐段统计或把口径拆成多次 AND 与 OR 的组合——位运算把集合运算的语义全数继承,复杂度只与墙的体积挂钩,与用户行为多复杂无关。

# 连续三天都活跃的用户数 > BITOP AND active:3d active:0820 active:0821 active:0822 > BITCOUNT active:3d

BITPOS 与区间版 BITCOUNT 是常被忽略的两把小刀:BITPOS 指定起止字节能找"从某天起第一次活跃的位置",BITCOUNT 带 start end 能数"某一周的活跃天数"——两者组合,一个键就能答出"连续签到从哪天开始、当月活跃几周"这类运营问题,不用为每个口径单独存一份墙。

⚠️ 常见坑:BITOP 的结果写进目标键,源键越大耗时越长,一亿位级别的 AND 单次可达百毫秒——记得错峰跑,或把大墙按用户号分段拆键。

GeoHash:把二维压成一维

"找附近"难在二维:经度和纬度都得相近。GeoHash 的解法是交错编码——把经度和纬度各自二分区间,得到的两串比特交叉穿插成一个整数。两个坐标越接近,它们的整数在数值上越接近的前缀越长。于是"圆形范围搜索"被变换成"分数区间查询",而后者正是 Sorted Set(2.4 跳表)的看家本领。

经纬度交错成 GeoHash 整数

经纬度交错成 GeoHash 整数

> GEOADD shops 116.404 39.915 王府井店 116.431 39.992 朝阳店 > GEOSEARCH shops FROMLONLAT 116.41 39.92 BYRADIUS 3 km ASC 1) "王府井店" > GEODIST shops 王府井店 朝阳店 km "8.9"

底层没有独立结构:GEOADD 就是把店名按编码后的整数 ZADD 进跳表。边界情况(编码相邻但实际被对角线隔开)由服务端二次精筛真实距离兜底,你不需要自己处理。

Geo 命令的一个完整用法示例,把附近搜索的常用参数一次带齐:

> GEOSEARCH shops FROMLONLAT 116.41 39.92 BYRADIUS 3 km ASC COUNT 5 WITHDIST WITHCOORD 1) 1) "王府井店" 2) "1.2" # 距离,单位随查询指定 3) 1) "116.404" 2) "39.915" # 回带坐标,省一次反查

三个参数的讲究:ASC 让结果按距离升序,配合 COUNT 就是"最近的五家";WITHDIST 与 WITHCOORD 把补齐展示层所需的字段一并返回,省掉逐店反查的往返;半径查询在编码上是"先按分数区间粗筛候选、再逐个算真实距离",候选集大小取决于区域密度——市中心三公里可能上千家店,ASC 加 COUNT 的意义就是让服务端排完序只吐前五条。边界情况:极地附近经线汇聚,编码区间的形状会失真,业务地图如果真要覆盖高纬度地区,验证过再上;普通城市业务不用操心。

共同的思维方式

Bitmap 是"布尔状态降维到比特",Geo 是"二维坐标降维到一维分数"。判断一个需求能不能用它们,问两个问题:数据是不是稠密的(用户号连续,Bitmap 才省);查询能不能变成范围(Geo 的附近才能变分数区间)。稀疏的用户号先做一层映射,否则位图中间大片空洞反而费内存。

本节要点回顾

  • Bitmap 寄生在 String 上,一亿布尔约 12MB,位运算即集合运算
  • GeoHash 交错编码把附近搜索变成跳表分数区间查询
  • Geo 底层就是 Sorted Set,ZADD 换了个马甲
  • 稠密性与范围性是判断能否降维的两个前提
  • 大位图的 BITOP 是重操作,拆键或错峰

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U