结论:用 bitop not + bitcount 组合可在毫秒级筛出未活跃用户,但需用户 id 映射为连续非负整数偏移且最大 id ≤ 2³²−1;set 和 hyperloglog 无法支持“非活跃”筛选,因前者需全量差集开销大,后者不存原始 id;bitmap 的 not 运算天然适用,但要求位图长度严格对齐真实用户空间,否则稀疏 id 会导致空位翻转虚高结果;key 设计须使 offset = 用户 id(或经映射的连续整数),超限会报错;执行流程为:① setbit 扩容至最大 offset,② bitop not 翻转,③ bitcount 指定字节范围统计;导出具体 id 建议应用层处理;需每日 del 清理过期 key,避免内存膨胀与 not 结果失真。

直接说结论:用 BITOP NOT + BITCOUNT 组合,能在毫秒级完成“从千万活跃用户中筛出未活跃者”,但前提是你的位图 key 已按用户 ID 映射为连续非负整数偏移量,且最大 ID ≤ 2³²−1。
为什么不能直接用 SET 或 HyperLogLog 做“非活跃”筛选
SET 存的是完整字符串,查“不在集合里的用户”必须遍历全量用户池再做差集,IO 和内存开销爆炸;HyperLogLog 根本不存原始 ID,只估算基数,连“谁没来”都无从得知。而 Bitmap 天然支持 NOT 运算——它把整个位数组翻转,0 变 1、1 变 0,再统计 1 的个数,就是非活跃人数。
但注意:NOT 操作会作用于整个底层字符串(即所有已分配字节),所以如果用户 ID 稀疏(比如最大 ID 是 1000 万但只用了其中 50 万个),位图实际长度仍是 1000 万 bit,BITOP NOT 后会把中间大量空闲位也翻成 1,导致结果虚高。必须控制偏移范围。
key 设计与 offset 映射必须对齐业务 ID 空间
假设你有 1000 万注册用户,ID 范围是 1~10000000,那么当天活跃位图的 key 可设为 active:20261019,每个用户 ID 直接作为 offset(ID=1 → offset=1,ID=10000000 → offset=10000000)。
若 ID 是 UUID 或雪花 ID,必须先做映射:
- 用 MySQL 或 Redis Hash 建
user_id_map表,把 UUID → 自增整数 ID,写入时同步维护 - 或用布隆过滤器 + 分段哈希预分配槽位,避免运行时冲突
- offset 必须 ≥ 0 且 ≤ 4294967295(2³²−1),超限会报错
ERR bit offset is not an integer or out of range
执行非活跃用户定位的三步命令流
以“查 2026-10-19 当天未活跃的用户”为例(假定总用户数 N = 10000000):
① 先确保位图长度覆盖全部用户:向 active:20261019 的第 10000000 位写一个 0(不改变原状态,仅扩容):SETBIT active:20261019 10000000 0
② 执行翻转,结果存到临时 key:BITOP NOT inactive:20261019 active:20261019
③ 统计非活跃人数(注意限定字节范围,避免统计高位空字节):BITCOUNT inactive:20261019 0 1249999(10000000 bit ÷ 8 = 1250000 字节,start=0, end=1249999)
如果要导出具体非活跃用户 ID,得用 BITPOS inactive:20261019 1 循环扫描,但千万级下不建议——应用层拉取再过滤更可控。
容易被忽略的 TTL 和内存清理陷阱
Bitmap 不像普通 String 那样能靠 EXPIRE 自动释放空间:一旦某位被设为 1,即使后续没再更新,该位所在的字节就一直占内存。长期运行后,active:20261019 可能残留大量历史零散置位,导致 NOT 结果膨胀。
必须配合策略:
- 每日凌晨用
DEL active:YYYYMMDD清理过期 key(不要依赖 TTL,因为 SETBIT 不会重置 TTL) - 对长期留存分析,用新 key 做
BITOP OR合并多日数据,旧 key 立即删掉 - 监控
MEMORY USAGE active:*,单 key 超 10MB 就要检查是否出现 ID 映射错乱或异常大 offset
真正难的不是命令怎么敲,而是让 offset 始终紧贴真实用户 ID 空间——漏一位、错一位,NOT 出来的就全是噪声。











