bitmap实现亿级用户精准去重的关键在于用户id整数映射、分片落盘、原子写入与生命周期管理,而非仅存储能力;需确保映射无损、并发安全、查询准确、合并高效。
用 bitmap 实现全站数亿级用户运行期精准去重,关键不在“能不能存下”,而在于“怎么映射得准、写得稳、查得快、合得对”。它不是简单调个 setbit 就完事,而是要围绕用户 id 的特性、系统吞吐压力和数据时效性做一整套工程设计。
用户 ID 必须能转成非负整数且值域可控
Bitmap 本质是用 bit 位索引数字,所以原始用户标识(比如字符串 UID、UUID、带符号 long)必须无损映射为 0~N 范围内的 uint32 或 uint64。常见做法:
- 若业务已用自增 int 用户 ID(如 1~3.5 亿),直接使用,偏移量 = ID - 1;
- 若用 64 位雪花 ID,取低 32 位(需评估冲突概率)或高位哈希分片后取模;
- 若用字符串 UID,必须先过一致性哈希(如 Murmur3)→ 得到 32 位 hash 值 → 再映射到有效范围;
- 严禁直接用
Math.abs(uid.hashCode()),负溢出或碰撞高会导致去重失效。
单机内存扛不住?必须分片 + 异步落盘
3 亿用户对应 Bitmap 约需 37.5 MB(3e8 ÷ 8 ÷ 1024²),看似不大,但真实场景中需同时维护:昨日 UV、近 7 天活跃、实时会话、设备指纹等多张 Bitmap,还可能按省份/渠道/版本切片。此时应:
- 按高位分片:例如取用户 ID 的高 8 位(
uid >> 24)作为分片号,生成 256 个子 Bitmap; - 每个子 Bitmap 独立管理内存,达到 500 MB 阈值时,立刻序列化为二进制文件写磁盘(不等全量结束);
- 查询时按分片路由,合并结果用
BITOP OR(Redis)或并行|=(本地 C++/Java); - 避免单 key 存超 512 MB —— Redis String 有硬上限,超限写入失败且无提示。
并发写入必须原子,否则丢位
高并发注册或埋点场景下,多个线程/进程同时 SETBIT user:uv:20260603 123456789 1,若底层非原子,会导致某次写被覆盖,造成漏计。保障方式:
- Redis 场景:所有
SETBIT、GETBIT、BITCOUNT均为服务端原子操作,无需额外加锁; - 本地内存 Bitmap(如 Java/C++):不能用
byte[]直接位操作,必须用AtomicLongArray或std::atomic<uint64_t></uint64_t>数组,set 时用fetch_or,get 时用load+ 位与; - 禁止用
Vector<boolean></boolean>或BitSet(非线程安全)承载实时写入流。
需要精确去重结果?别只靠 BITCOUNT
BITCOUNT 返回的是当前 Bitmap 中 1 的个数,但它无法区分“刚写入的活跃用户”和“上周遗留未清理的脏位”。生产环境必须配套生命周期管理:
- 每日零点自动
DEL user:uv:20260602(TTL 不可靠,异步删除可能延迟); - 对长期统计(如周活),用
BITOP OR destkey uv:day1 uv:day2 ... uv:day7合并,再BITCOUNT,不依赖单日 key 的完整性; - 若需回溯某用户是否在某天活跃,必须保留原始日粒度 Bitmap 至少 30 天,不可仅存聚合结果;
- 上线前用小流量比对:Bitmap UV vs Hive 全量去重 count(distinct uid),误差率应稳定 ≤ 0.001%。






