Bitmap 位图签到与统计

举报
yd_227606267 发表于 2026/11/07 00:11:11 2026/11/07
【摘要】 用 SETBIT 与 BITCOUNT 实现签到、日活统计,避开位图稀疏陷阱

签到打卡、日活统计、留存分析,本质都是"一个对象在某天是否发生某事"的布尔事实。用 Set 存一天一亿用户的活跃名单要数 GB,而位图只要约 12 MB。Bitmap 不是独立类型,而是 String 的位视角,理解这一点才能解释它的行为与陷阱。本文以 Redis 7 为基准展开。

一、位图本质与基本命令

SETBIT 把指定偏移的位设为 0 或 1,返回该位的旧值;GETBIT 读取指定位。位图自动扩展:设置的偏移超过当前长度时中间补 0,因此 GETBIT 任何越界偏移都返回 0 而不报错。偏移上限是 2^32 - 1,即单个位图最大 512 MB——这是 String 的 512 MB 上限换算成位的结果。

127.0.0.1:6379> SETBIT sign:1001:202610 24 1
(integer) 0
127.0.0.1:6379> GETBIT sign:1001:202610 24
(integer) 1
127.0.0.1:6379> BITCOUNT sign:1001:202610
(integer) 1

BITCOUNT 统计值为 1 的位个数,start/end 参数默认按字节偏移,Redis 6.2 起可加 BIT 单位按位统计;BITPOS 返回第一个指定值的位的位置,找"第一个没签到的天"用它。单位搞错是位图统计最常见的错误来源。

内存换算很简单:offset 上限为 N 的位图约占 N/8 字节。一天一亿用户的日活位图是 12.5 MB,一年 365 张约 4.5 GB,可控;反过来,只有 1 万活跃用户但 ID 域到 30 亿,一张图就要 375 MB,绝大多数位是 0,完全划不来。选位图之前先做这道除法。

二、签到场景的标准实现

key 设计为 sign:{uid}:{yyyyMM},offset 用"当天是本月第几天减 1",一个月一张图,31 天最多 4 字节:

# 10 月 25 日签到
SETBIT sign:1001:202610 24 1
# 今天是否已签到(防重复)
GETBIT sign:1001:202610 24
# 本月签到总次数
BITCOUNT sign:1001:202610

连续签到天数的计算,用 BITFIELD 一次取出一个位段再在应用层逐位判断:

# 取从第 3 位(含)开始的 28 位
BITFIELD sign:1001:202610 GET u28 3

拿到整数后在代码里从低位往高位数连续的 1 即可。整月数据一次往返取回,比按天 GETBIT 循环快得多;位段宽度按 4 字节整数上限设计,不要一次 GET u300,跨月的连续签到就分两次取再拼接。月度 key 设好 TTL,保留策略(比如 13 个月)由业务定。全勤判断是 BITCOUNT 与当月天数比较,注意月底之前"全勤"只能是暂定值;补签直接对历史偏移 SETBIT 覆写 1,位图天然支持修改历史,这一点比 HyperLogLog 友好得多。同一用户同一天重复 SETBIT 是幂等的,但接口层仍建议先 GETBIT 短路,省掉无谓写。key 命名也有讲究:按 sign:{uid}:{yyyyMM} 把 uid 放在前缀,SCAN MATCH sign:1001:* 就能拉出单个用户的全部签到史,排查客诉时省力。

三、日活、留存与 BITOP

日活统计每天一张位图:dau:20261025,用户活跃则把对应偏移置 1,偏移来自"用户 ID 到自增序号"的固定映射。多天聚合用 BITOP:

# 周活跃 = 7 天日活位图按位或
BITOP OR dau:week:2026w43 dau:20261019 dau:20261020 dau:20261021 dau:20261022 dau:20261023 dau:20261024 dau:20261025
# 连续 7 天活跃(留存口径)= 按位与
BITOP AND dau:retain7 dau:20261019 dau:20261020 dau:20261021 dau:20261022 dau:20261023 dau:20261024 dau:20261025
BITCOUNT dau:week:2026w43

BITOP 是 O(N) 且在主线程执行,一亿位约 12 MB,一次运算毫秒级,但要控制频率并把结果 key 设 TTL 防止残留。BITOP NOT 可以求补集,比如"当天不活跃用户",注意 NOT 只接受单个源 key,且补集位图与源等大,用完及时设过期。留存曲线里常见的"第 N 日回访",就是当日 DAU 位图与 N 天前 DAU 位图按位与,用户粒度天然对齐,这是 Set 方案做不到的零成本对齐。留存分析的口径要分清:OR 是"期间任一天活跃",AND 是"每一天都活跃",混用会得出完全不同的数字。

成本再算一笔账:1 亿用户、日活 2000 万,Set 存一天按 2000 万个成员算要数十 GB;位图按 1 亿的域算恒定 12.5 MB,位图大胜。但若日活只有 1 万、注册域 30 亿,Set 只要几百 MB,位图却要 375 MB——真正让位图吃亏的是"ID 域巨大且活跃度极低"的组合,这时 HLL 的 12 KB 完胜。一句话总结选型:布尔事实密集选位图,稀疏低活跃选 HLL,要名单选 Set。

四、BITFIELD 与稀疏陷阱

BITFIELD 把 String 当任意位宽的整数数组用,支持 u/i 位宽、SET/GET/INCRBY 与溢出策略(WRAP 回绕、SAT 饱和、FAIL 失败拒写):

# 第 0 个无符号 8 位计数器自增,溢出时饱和在最大值
BITFIELD user:1001 OVERFLOW SAT INCRBY u8 #0 1

它可以把多个小计数器塞进一个 key 省内存,但每个计数器的位宽要按"最大值域"设计,8 位计数器存月访问量必溢出。

位图最大的陷阱是稀疏:偏移直接用原始用户 ID 时,ID 从 30 亿起步的系统第一次 SETBIT 就会分配近 400 MB 的中间空间,只为表达一个 1。治理方式是建立"用户 ID 到自增序号"的映射(映射表或固定哈希),把偏移压缩到密集区间;或者按城市、分片拆小位图。是否选位图,判断标准是"活跃用户占全量用户的比例",比例太低时 HyperLogLog(下一文)更合适。

映射的两种实现要分清:自增序号映射需要一张持久化映射表,序号回收策略要明确,优点是位图完全密集;固定哈希映射(如 uid 取模 1 亿)不需要存储,但两个用户可能落在同一位,统计口径从"精确人数"变成"上限人数",能接受这种误差的业务才能用。两种方案一旦选定不可切换,切换等于历史数据作废,务必在架构评审时定死并写进文档。

常见坑 / 最佳实践

  1. 偏移要映射成密集的自增序号,不要直接用大数值用户 ID。 稀疏置位会白白吃掉中间内存;映射关系要持久化,算法一变历史数据全部作废。
  2. BITCOUNT 的 start/end 默认是字节偏移。 想按位统计必须显式加 BIT 单位(Redis 6.2+),统计口径错误比不统计更危险。
  3. 位图 key 必须设 TTL。 签到月图、日活天图都有明确生命周期,不设过期的位图是大内存事故的常见来源。
  4. BITOP 放低峰或从库执行。 O(N) 主线程操作,超大片图聚合要控制频率,结果 key 记得设 TTL。
  5. BITFIELD 位宽按最大值域设计。 8 位计数器上限 255,月度计数场景用 u16 或 u32,并选择 SAT 溢出策略避免静默回绕。

小结

  • Bitmap 是 String 的位视角,偏移上限 2^32 - 1,越界读返回 0,置位自动扩展中间补 0。
  • 签到用"uid + 月份"的 key 加天序号偏移,BITCOUNT 求次数、BITFIELD 一次取位段算连续签到。
  • 日活与留存靠按天位图加 BITOP 的 OR/AND 聚合,两种口径分别对应"任一天活跃"与"每天都活跃"。
  • 位图适合密集布尔事实,稀疏 ID 必须先映射压缩,否则内存远超预期。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。