bitmap是日活统计最省空间、最快方案,1000万用户仅占1.2mb且支持交并差运算;set存字符串开销大,hyperloglog不支持精确集合运算;需用户id可映射为非负整数,go中用go-redis/v9调用setbit/bitcount/bitop,注意key设计、位偏移计算、ttl清理及内存策略。

为什么用 Redis Bitmap 而不是 SET 或 HyperLogLog
直接说结论:Bitmap 是日活统计里最省空间、最快速的方案,尤其当用户 ID 是单调递增或可映射为整数时。SET 会存完整 ID 字符串,1000 万用户可能占几百 MB;HyperLogLog 虽然省内存(约 12KB),但不支持去重后取样、也不支持交并差——你没法查“连续 3 天都活跃的用户”。而 Bitmap 用 1 bit 表示一个用户当天是否活跃,1000 万用户只占约 1.2MB,且 SETBIT、BITCOUNT、BITOP 全是 O(1) 或 O(N/8) 时间复杂度。
注意前提:用户 ID 必须能转成非负整数,且最大值不宜超过 2³²−1(Redis Bitmap 上限)。如果用 UUID,得先做 ID 映射(比如写入 MySQL 的 user_id_map 表,或用布隆过滤器+哈希分段预分配)。
Go 中调用 BITSET 和 BITCOUNT 的典型写法
别直接拼 redis-cli 命令,用官方 github.com/redis/go-redis/v9 客户端。关键点是 key 设计和位偏移计算:
- 每天一个 key,例如
"uv:20240520",避免单 key 过大导致 RDB/AOF 压力 - 位偏移 = 用户 ID(整型),不是字符串哈希值——否则无法保证一致性
-
SETBIT操作本身是幂等的,重复调用不会翻转位,适合并发写入
示例代码片段:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
ctx := context.Background()
key := fmt.Sprintf("uv:%s", time.Now().Format("20060102"))
err := rdb.SetBit(ctx, key, int64(uid), 1).Err()
if err != nil {
// 记录错误但不中断业务,日活少 1 个不影响整体趋势
}
count, _ := rdb.BitCount(ctx, key, &redis.BitCount{Start: 0, End: -1}).Result()
如何安全地支持多天数据合并与对比
查“近 7 天新增用户”或“昨日留存率”,不能靠应用层遍历 7 个 key 累加——太慢且不准(中间有失败或延迟写入)。必须用 Redis 原生命令:
- 用
BITOP AND算交集:比如BITOP AND retent_20240519 "uv:20240519" "uv:20240518"得到昨天和前天都活跃的用户数 - 用
BITOP XOR算净增:把当天 key 和前一天 key 异或,结果中为 1 的位就是当天新出现的用户 - 所有
BITOP命令目标 key 最好带 TTL,比如EXPIRE retent_20240519 3600,避免临时结果堆积
Go 调用示例:
dstKey := "retent_" + yesterStr
err := rdb.BitOpAnd(ctx, dstKey, keyToday, keyYester).Err()
if err == nil {
count, _ := rdb.BitCount(ctx, dstKey, nil).Result()
// count 就是昨日留存用户数
}
生产环境最容易被忽略的三个坑
Bitmap 看似简单,线上出问题往往卡在细节:
- Redis 默认
maxmemory-policy是noeviction,但 Bitmap 写入频繁时内存增长快,务必配成allkeys-lru或定期清理过期 key(比如凌晨跑DEL uv:20240510) - Go 的
int64传给SETBIT时,如果uid是 uint32,别直接强转——负数会导致位偏移溢出,Redis 返回ERR bit offset is not an integer or out of range - 本地开发用 Docker Redis 默认没开
notify-keyspace-events,但如果你后续想用 key 过期事件触发归档,得显式配置notify-keyspace-events Ex
位图本身不存用户元信息,真要查某个活跃用户是谁,得靠外部 ID 映射表回查——这个关联逻辑千万别塞进 Redis pipeline 里,会拖慢主流程。










