032缓存模块基于Redis Bitmap的用户行为统计实战:签到与日活分析
本项目代码: https://gitee.com/yunjiao-source/tutorials4j
1. 引言
在互联网应用中,用户签到、日活(DAU)、周活(WAU)等统计是常见的运营需求。传统的关系型数据库面对海量用户的高频写入和聚合查询时,往往面临性能瓶颈。Redis的Bitmap(位图)数据结构提供了一种极致内存效率的解决方案------它利用每个bit位存储二元状态(0或1),能以极小的空间开销完成大规模用户的布尔状态记录与位级聚合运算。
本文通过分析一个完整的Spring Boot示例代码(包含UserActivityController与RedisBitmapUtils两个类),深入讲解如何利用Redis Bitmap实现以下功能:
- 用户每日签到与月度签到统计
- 每日活跃用户(DAU)记录与查询
- 连续N天活跃用户数(AND运算)
- 周活跃用户数(OR运算)
同时,文章将剖析工具类的设计思路、关键技术细节以及最佳实践建议。
2. 代码结构概览
| 类名 | 职责 |
|---|---|
RedisBitmapUtils |
封装Redis Bitmap底层操作,提供位设置/获取、位计数、位运算(AND/OR/XOR/NOT)等核心能力,并支持通过Murmur3哈希将字符串映射为偏移量。 |
UserActivityController |
提供REST API,利用RedisBitmapUtils实现签到、日活记录、连续活跃统计等业务接口。 |
两个类配合使用,上层业务无需关心Redis命令细节,同时工具类可复用于其他需要bitmap的场景。
3. RedisBitmapUtils:功能完备的位图工具箱
3.1 核心字段与依赖注入问题
java
private static StringRedisTemplate stringRedisTemplate;
工具类采用静态方法封装,但stringRedisTemplate被声明为静态字段。代码中提供了setStringRedisTemplate方法供外部注入。注意 :这种方式需要调用方(如Spring配置类)显式调用注入,否则会引发空指针异常。更优雅的做法是使用@Component并将工具类设计为非静态Bean,或者使用@PostConstruct初始化。
3.2 哈希偏移量:解决字符串到bit位的映射
对于诸如"记录用户是否活跃"的场景,常需要将一个业务ID(如userId)映射为bitmap中的偏移量。RedisBitmapUtils提供了基于Guava的Murmur3_128哈希算法的映射方法:
java
private static long hash(String key) {
return Math.abs(Hashing.murmur3_128()
.hashObject(key, Funnels.stringFunnel(StandardCharsets.UTF_8))
.asInt());
}
优点:
- 分布均匀,碰撞概率极低
- 输出范围在0 ~ 2^31-1之间,完全适配Redis Bitmap的偏移量上限(2^32-1)
- 避免了直接使用大数值userId可能带来的偏移量过大问题
工具类同时提供了支持字符串参数的setBit(String key, String param, boolean value)和直接指定偏移量的setBit(String key, Long offset, boolean value),兼顾灵活性和性能。
3.3 位统计与范围统计
java
public static Long bitCount(String key)
public static Long bitCount(String key, int start, int end)
利用Redis的BITCOUNT命令,可统计整个bitmap中1的个数,或限定字节范围进行统计。后者在分页或分段统计时非常有用。
3.4 位运算:聚合多个bitmap的核心
java
public static Long bitOp(RedisStringCommands.BitOperation op, String saveKey, String... destKey)
public static Long bitOpResult(RedisStringCommands.BitOperation op, String saveKey, String... destKey)
bitOp执行AND/OR/XOR/NOT运算,并将结果存储到saveKey中,返回结果占用的字节长度。bitOpResult则在运算后直接对结果执行bitCount,一步到位获得聚合后的用户数------这正是连续活跃和周活跃统计的关键技术。
4. UserActivityController:业务API实现详解
4.1 用户签到与月度统计
签到设计 :Redis Key格式为 user:sign:{userId}:{yyyyMM},偏移量为 date.getDayOfMonth() - 1(每月1日对应offset=0)。这样每个用户每月占用一个bitmap,最多31位。
API:
POST /sign-- 用户签到,返回该天之前是否已签到GET /sign/check-- 检查某天是否签到GET /sign/count-- 统计用户当月累计签到天数
示例:用户1001在2026年5月13日签到,对应Key为user:sign:1001:202605,offset=12。调用RedisBitmapUtils.setBit(key, 12L, true)。
4.2 每日活跃用户(DAU)记录与查询
活跃记录设计 :Key格式为 dau:{yyyy-MM-dd},偏移量直接使用userId。
java
String todayKey = DAU_PREFIX + LocalDate.now().toString();
RedisBitmapUtils.setBit(todayKey, userId, true);
潜在风险 :如果userId非常大(例如超过2^32-1 ≈ 42.9亿),Redis的
SETBIT命令会拒绝并返回错误。在实际生产环境中,建议对userId进行哈希处理(如调用RedisBitmapUtils.setBit(todayKey, String.valueOf(userId), true),内部会调用哈希方法),以避免偏移量越界。当前示例直接使用Long作为offset,假设userId在安全范围内。
查询DAU :GET /active/dau?date=2026-05-13 调用bitCount即可得到当天活跃用户数。
4.3 连续N天活跃用户数(AND运算)
业务含义:在指定起始日期开始的连续N天里,每天都登录过的用户数量。
实现步骤:
- 生成这N天的bitmap key数组(例如
dau:2026-05-10,dau:2026-05-11,dau:2026-05-12) - 执行
BITOP AND temp:consecutive:and key1 key2 ... keyN - 对结果key执行
BITCOUNT,得到同时出现在所有bitmap中的用户数
代码中使用RedisBitmapUtils.bitOpResult(AND, tempKey, keys)一步完成运算+计数,最后可选择性删除临时键。
4.4 一周内活跃用户数(OR运算)
业务含义:某周内(周一至周日)至少活跃过一次的用户总数。
实现步骤:
- 根据任意日期计算当周的周一
- 生成周一到周日共7个bitmap key
- 执行
BITOP OR temp:weekly:or key1 ... key7 - 统计结果中的1的个数
同样利用bitOpResult简洁实现。
5. 核心设计思想与关键技术
| 设计点 | 实现方式 | 优点 |
|---|---|---|
| 用户签到 | 每月一个bitmap,日期偏移量 | 单用户每月仅占31位,空间极小;统计月度签到直接BITCOUNT |
| DAU记录 | 每天一个bitmap,用户ID/哈希做偏移量 | 单日百万用户仅需约0.12MB内存(100万bits = 125KB) |
| 连续活跃 | BITOP AND + BITCOUNT |
一次聚合得到多日交集用户数,无需遍历用户 |
| 周活跃 | BITOP OR + BITCOUNT |
高效求并集,计算周期留存 |
| 通用位运算 | 工具类封装bitOpResult |
调用方能方便实现任意组合逻辑(如7日留存:今日AND前7日) |
6. 技术细节与注意事项
6.1 Redis Bitmap的限制
- 偏移量最大为
2^32-1(约42.9亿),若使用原始userId需保证不超过此值。 BITOP命令在处理大bitmap时可能阻塞Redis,建议在从库或低峰期执行,或使用BITOP的saveKey临时存储结果后尽快删除。BITCOUNT对于超大bitmap(数GB)性能较好,但需注意网络传输耗时。
6.2 内存估算举例
- 日活1亿用户:
100,000,000 bits ≈ 11.92 MB,存储一个月30天的DAU bitmap仅需约357 MB,远低于使用Set或Hash。 - 月度签到:若用户1000万,每用户每月31bits ≈ 3.875MB,百万用户约3.7GB -- 实际上每个key有少量元数据开销,但依然远优于传统关系表。
6.3 临时键管理
getConsecutiveActiveUsers和getWeeklyActiveUsers中创建了临时Key(temp:consecutive:and等),但代码中注释掉了删除操作。生产环境务必删除 或设置过期时间(如expire tempKey 60),否则会堆积大量无用的Redis键。更好的做法:使用RedisBitmapUtils.bitOp后立刻执行DELETE,或者利用MULTI/EXEC事务保证清理。
6.4 原子性考量
签到和日活记录都是单key单bit操作,天然原子。但连续活跃统计的"写入临时key + 计数 + 删除"不是原子操作,若两个并发请求使用相同临时key可能导致结果错误。解决方案:为每次请求生成唯一临时key(如temp:and:uuid)并在用后删除,或者使用Lua脚本封装。
6.5 日期与偏移量处理的边界
- 签到偏移量使用
dayOfMonth-1,需注意每月天数不同,但Redis Bitmap支持动态扩展,即使offset超出31(如2月30日)也不会报错,但业务上应提前校验。 - 日活记录中的
LocalDate.now()应统一使用服务器时区,避免跨天问题。
7. 改进与扩展建议
7.1 避免超大偏移量
在recordActive方法中,应改用哈希方式:
java
Boolean oldValue = RedisBitmapUtils.setBit(todayKey, String.valueOf(userId), true);
利用工具类的hash方法将userId映射到安全范围。或者修改工具类,增加setBitByObject(String key, Object id, boolean value)重载。
7.2 临时键自动过期
bitOpResult方法在返回计数后,可以自动为saveKey设置过期时间(如60秒),防止残留:
java
public static Long bitOpResultWithExpire(BitOperation op, String saveKey, int expireSeconds, String... destKey) {
Long result = bitOpResult(op, saveKey, destKey);
stringRedisTemplate.expire(saveKey, expireSeconds, TimeUnit.SECONDS);
return result;
}
7.3 支持更复杂的留存分析
利用BITOP AND和BITOP OR组合,可以实现次日留存(今日活跃且昨日活跃)、7日留存(今日活跃且7日前活跃)等。只需将两天的bitmap做AND运算即可。
7.4 监控与性能优化
- 对于频繁的
BITOP操作,可以预热结果并缓存一段时间(例如每小时计算一次DAU/WAU)。 - 使用Redis 7.0+的
BITFIELD命令可以同时操作多个位,进一步减少网络往返。
8. 总结
本文通过两个实际Java类,完整展示了如何利用Redis Bitmap高效实现用户签到、日活统计、连续活跃及周活跃等经典场景。RedisBitmapUtils提供了一组简洁而强大的位图操作API,而UserActivityController则作为业务示例,体现了以下设计原则:
- 空间换时间:用极致的内存占用换取统计查询的毫秒级响应。
- 位运算聚合 :利用
BITOP一次计算用户交/并集,避免扫描全部用户。 - 业务数据分片:按月份、按天拆分key,既方便管理又避免单key过大。
生产落地时,只需注意偏移量范围、临时键清理、时区处理等细节,这套方案可以支撑亿级用户的高并发统计需求。希望本文能为你的用户行为分析系统设计提供有价值的参考。