Redis 位图 Bitmap 统计活跃用户
Redis 位图(Bitmap)入门:概念与优势
Redis 位图虽然名义上是“位图”,但它并非一种独立的数据类型,而是基于 字符串 类型的位操作扩展。所谓的位图,就是将字符串当作一组连续的二进制位(0 或 1)来操作。每一位可以代表一个布尔状态,例如用户登录、签到、视频播放标记等。
为什么选择位图做活跃用户统计?
- 内存极度节省:1 亿用户的日活统计仅需约 12 MB 内存(
100,000,000 bits / 8 / 1024 / 1024 ≈ 11.92 MB),这是传统集合或哈希结构无法比拟的。 - 极快的位运算:Redis 对位的操作时间复杂度通常是 O(1),并且支持对多个位图做位与、位或、位异或等操作,适合多日活跃聚合计算。
- 操作简单:只需一条命令就能标记用户在某天的活跃状态,无需复杂的应用层逻辑。
位图的核心思想
将用户 ID 直接映射到某个偏移量(offset)上。例如,用户 ID 为 10086,就将其映射到位图的第 10086 位上。如果该位为 1,表示当日活跃;为 0 表示不活跃。
前置准备:理解 Redis 位图命令
在统计活跃用户之前,必须先掌握几个最常用的位图操作命令。以下命令均操作 Redis 字符串类型的 key。
SETBIT key offset value
- 作用:对 key 所存储的字符串,设置或清除指定偏移量上的位。
- 参数:
offset为位索引(从 0 开始),value为1或0。 - 返回值:该位原来存储的值。
- 示例:
SETBIT user:active:20250408 10086 1 # 将用户 10086 在 2025-04-08 的活跃标记为 1
GETBIT key offset
- 作用:获取指定偏移量上的位值。
- 返回值:
1或0。 - 示例:
GETBIT user:active:20250408 10086 # 查询用户 10086 在当天是否活跃
BITCOUNT key [start end]
- 作用:统计字符串(位图)中被设置为
1的位数。可选参数可指定字节范围,不指定则统计整个位图。 - 返回值:
1的个数。 - 示例:
这个命令直接给出了当日活跃用户总数,无需扫描全部用户。BITCOUNT user:active:20250408 # 统计 2025-04-08 的总活跃用户数
BITOP operation destkey key [key ...]
- 作用:对一个或多个位图执行位运算,结果存入
destkey。 - 支持的操作:
AND(与)、OR(或)、XOR(异或)、NOT(非)。 - 返回值:结果字符串的长度(字节)。
- 示例:
BITOP AND user:active:week user:active:20250401 user:active:20250402 ... user:active:20250407 # 计算连续 7 天都活跃的用户 BITCOUNT user:active:week # 得到连续 7 天活跃用户数
BITPOS key bit [start] [end]
- 作用:查找第一个被设置为指定值的位的位置。
- 返回值:位的位置索引。
- 示例:
BITPOS user:active:20250408 1 # 找到第一个活跃用户的 ID
实战场景:统计每日活跃用户(DAU)
第一步:设计 key 命名规范
合理的 key 命名能方便后期维护和自动过期。建议采用 业务:日期 的形式:
user:active:20250408 # 表示 2025-04-08 的活跃用户位图
user:active:20250409
一般统计类数据可以设置过期策略:
EXPIRE user:active:20250408 2592000 # 30 天后自动删除
第二步:用户活跃时标记
当用户登录或完成某个核心操作(如浏览、下单)时,执行 SETBIT:
# 用户 ID 为 123456 的用户发起登录
SETBIT user:active:20250408 123456 1
注意:用户 ID 通常是从 1 递增的整型,或者可以 Hash 映射到固定范围。如果用户 ID 不是整型,请先将其转换为唯一数字(如自增主键),避免偏移量过大或冲突。
第三步:查询单个用户是否活跃
在个人中心或报表中,快速判断某用户当天是否活跃:
GETBIT user:active:20250408 123456
第四步:获取日活跃用户总数
运营后台需要实时查看 DAU(Daily Active Users),直接使用:
BITCOUNT user:active:20250408
不需要任何遍历,瞬间返回。
进阶分析:多日聚合统计
统计连续 N 天活跃用户(留存分析)
假设需要统计 2025-04-01 至 2025-04-07 这一周内每天都活跃的用户,只需对这 7 天的位图执行位与操作:
BITOP AND active_7_days user:active:20250401 user:active:20250402 user:active:20250403 user:active:20250404 user:active:20250405 user:active:20250406 user:active:20250407
BITCOUNT active_7_days
原理:对每一位来说,只有所有位图中该位都为 1,结果位才为 1。因此 active_7_days 中为 1 的位就代表连续 7 天活跃的用户。
统计周活跃用户(WAU)
周活跃用户只要是这一周内任意一天活跃过即可,使用位或操作:
BITOP OR active_week user:active:20250401 user:active:20250402 user:active:20250403 user:active:20250404 user:active:20250405 user:active:20250406 user:active:20250407
BITCOUNT active_week
计算次日留存用户
次日留存 = 今天和明天都活跃的用户。用位与即可:
BITOP AND retention_day1 user:active:20250408 user:active:20250409
BITCOUNT retention_day1
常见问题与最佳实践
1. 用户 ID 过大导致位图稀疏怎么办?
如果用户 ID 是 30000000 这种大数,直接使用偏移量会导致生成一个大字符串,即使中间很多位为 0,也会占用内存。位图长度由最大偏移量决定。
优化方案:
- 重新映射用户 ID:将真实 ID 映射到从
1开始的自增数字,维护一份映射表(可使用 Hash 或本地缓存)。 - 分片思想:按用户 ID 范围使用多个位图,例如
user:active:20250408:shard0存放0~999999的用户,再用BITOP时分别处理。
2. 如何获取活跃用户的具体 ID 列表?
位图直接返回的是位的统计值,不直接返回用户 ID 列表。如果需要列出活跃用户 ID,可以:
- 使用
BITPOS找到第一个1的位置,再用GETBIT遍历? - 更方便的方式:结合 Redis 集合。在标记活跃的同时,将用户 ID 加入一个
Set,用SMEMBERS获取列表。两个结构并行使用,位图负责统计和位运算,Set 负责列表查询。
3. 内存占用估算
假设 1 亿用户:
- 每个用户一位,共 1 亿 bits = 12.5 MB。
- 保存 90 天的日活:
12.5 MB * 90 ≈ 1.125 GB,仍在可接受范围。 - 对比使用
Set存储用户 ID(一个 ID 按 64 位整型算,1 亿个约 800 MB,90 天就是 72 GB),位图优势明显。
4. 位图操作是否影响 Redis 主线程?
SETBIT、GETBIT、BITCOUNT 都是 O(1) 或 O(N)(N 为字节数)操作,对于位图来说很快。但 BITOP 对不同 key 执行位运算可能需要遍历整个字符串,当位图极大(几兆字节)时,可能会短暂阻塞。建议在从节点或低峰期执行大规模 BITOP。
总结
Redis 位图为“活跃用户统计”这类布尔型海量数据场景提供了优雅解决方案:
- 用
SETBIT以用户 ID 为偏移量标记活跃状态。 - 用
BITCOUNT瞬间得到日活、周活。 - 用
BITOP AND/OR轻松实现连续活跃、留存分析。 - 极低的内存开销使其成为处理亿级用户行为统计的首选。
掌握以上模式,你就能快速搭建一个高性能、低成本的用户活跃分析系统。