基于Bitmap实现亿级用户日活/周活以及其他应用场景

6 阅读14分钟

基于Redis的Bitmap实现亿级用户日活/周活以及其他应用场景

1. Redis Bitmap 核心原理

1.1 底层存储结构

Redis Bitmap 并非独立的数据结构,而是基于 Redis String 类型封装的位级存储方案。其核心逻辑是将 String 类型的字节(byte)拆分为单个比特(bit),以比特为最小操作单位,实现高效的空间利用。

关键特性:

  • 每个 Bitmap 对应一个二进制数组,数组中每一位(bit)仅能存储 0 或 1 两个值,分别表示“未触发”和“已触发”两种状态;

  • 底层依赖 String 类型的存储机制,String 最大支持 512MB,对应最大偏移量(offset)为 2^32 - 1(约42.9亿),完全满足亿级、十亿级用户场景;

  • 操作轻量,所有 Bitmap 命令均为原子操作,可避免并发场景下的数据错乱,适配分布式系统需求。

1.2 核心操作命令

Bitmap 提供简洁的原子命令,覆盖“设置、查询、统计、运算”四大核心场景,具体如下表所示:

命令格式

功能说明

使用场景

SETBIT key offset value

设置指定 key 对应 Bitmap 的第 offset 位(从0开始计数)为 value(0 或 1),原子操作

标记用户行为(如访问、签到)

GETBIT key offset

获取指定 key 对应 Bitmap 的第 offset 位的值(0 或 1)

查询用户是否有指定行为(如是否签到)

BITCOUNT key [start end]

统计指定 key 对应 Bitmap 中值为 1 的比特数;start/end 为可选参数,指定字节范围(默认统计全部)

统计日活、签到人数等

BITOP op destkey key1 key2...

对多个 Bitmap 执行位运算(op 支持 AND/OR/XOR/NOT),结果存入 destkey

统计周活、月活(多日数据合并)

BITPOS key value [start end]

查找指定 key 对应 Bitmap 中第一个值为 value(0 或 1)的比特位偏移量

查找首个活跃/未活跃用户

1.3 空间占用优势(核心亮点)

Bitmap 最核心的优势是极致的空间利用率,其空间占用计算方式如下:

空间大小(MB)= 最大偏移量(offset)÷ 8 ÷ 1024 ÷ 1024

典型场景测算:

  • 1亿用户(offset 最大为 1亿):100,000,000 bit ÷ 8 ÷ 1024 ÷ 1024 ≈ 12 MB;

  • 10亿用户(offset 最大为 10亿):1,000,000,000 bit ÷ 8 ÷ 1024 ÷ 1024 ≈ 119 MB;

  • 42亿用户(Bitmap 最大支持):约 512 MB(String 最大容量)。

相比其他存储方案(如 Set、Hash),Bitmap 在大数据量场景下的空间优势极为明显,这也是其能支撑亿级日活统计的核心原因。

2. Bitmap 典型应用场景

Bitmap 基于“位存储+高效统计”的特性,适用于“二值状态标记+大数据量统计”的所有场景,以下是最常用的5类场景:

2.1 用户签到场景

需求:记录用户每日签到状态,统计每日签到人数、用户累计签到天数。

实现方案:

  • key 设计:sign:yyyyMMdd(如 sign:20260226,表示2026年2月26日的签到记录);

  • offset 设计:用户唯一ID(确保offset唯一,避免冲突);

  • 状态标记:用户签到时执行 SETBIT sign:20260226 10001 1(10001为用户ID,1表示已签到);

  • 统计逻辑:每日签到人数用 BITCOUNT sign:20260226 统计;用户累计签到天数,可通过查询该用户在多个日期 Bitmap 中的位值(GETBIT)累加得到。

2.2 用户状态标记

需求:标记用户是否在线、是否激活、是否开通会员等二值状态,支持快速查询。

实现方案:

  • key 设计:user:active(激活状态)、user:online(在线状态);

  • offset 设计:用户唯一ID;

  • 状态标记:用户激活时执行 SETBIT user:active 10001 1,未激活为0;用户上线时设1,下线时设0;

  • 查询逻辑:通过 GETBIT user:active 10001 快速判断用户是否激活,无需查询数据库,提升响应速度。

2.3 黑名单/白名单场景

需求:维护IP黑名单、用户黑名单(如禁止登录、禁止访问),支持快速校验。

实现方案:

  • key 设计:blacklist:ip(IP黑名单)、blacklist:user(用户黑名单);

  • offset 设计:IP转换为整数(如将IP地址分段转换为十进制)、用户ID;

  • 标记逻辑:将黑名单中的IP/用户ID对应的位设为1,正常IP/用户设为0;

  • 校验逻辑:访问时,将IP/用户ID转换为offset,通过 GETBIT 校验位值,1则拒绝访问,0则允许,响应时间可达微秒级。

2.4 布隆过滤器(防缓存穿透)

需求:过滤不存在的key(如用户ID、商品ID),避免缓存穿透(请求穿透缓存直接访问数据库)。

实现方案:

  • 基于 Bitmap 实现布隆过滤器,将多个哈希函数作用于key,得到多个offset,将这些offset对应的位设为1;

  • 校验时,对key执行相同的哈希运算,若所有offset对应的位均为1,则key可能存在;若有任意一位为0,则key一定不存在;

  • 优势:相比传统Set,布隆过滤器空间占用极低,1亿条数据仅需十几MB,可有效拦截无效请求,保护数据库。

2.5 亿级用户日活/周活/月活统计(核心场景)

需求:统计每日、每周、每月活跃用户数(去重),支撑产品运营决策,要求高效、低内存、秒级响应。

核心优势:Bitmap 极省内存、统计速度快,是互联网公司统计DAU/WAU/MAU的标准方案,详细实现见第4章节。

3. 亿级用户日活(DAU)、周活(WAU)实现方案

核心思路:利用 Bitmap 位存储特性,以“日期”为维度生成 Bitmap,标记当日活跃用户;周活通过位运算合并多日 Bitmap,实现去重统计,全程原子操作、秒级响应,支持亿级用户规模。

3.1 前置准备

确保用户ID为唯一整数(若为字符串ID,需先通过哈希算法转换为唯一整数offset,避免offset冲突);Redis 版本不低于2.6.0(支持 SETBIT 命令的原子性设置);生产环境建议使用Redis主从/哨兵集群,避免单点故障。

补充:用户ID哈希转换(字符串/UUID转整数offset)代码示例

实际开发中,用户ID可能为字符串(如UUID、雪花ID字符串形式),无法直接作为offset,需通过哈希算法转换为唯一整数。推荐使用 MurmurHash 算法(高性能、低碰撞率),以下是Java语言完整代码示例,可直接复用:

import org.apache.commons.codec.digest.MurmurHash3; import org.springframework.util.Assert;

/**

  • 用户ID哈希转换工具类(字符串ID -> 整数offset,适配Bitmap) */ public class UserIdHashUtil {

    /**

    • 最大偏移量(对应Bitmap最大支持的offset:2^32 - 1,约42.9亿) */ private static final long MAX_OFFSET = (1L << 32) - 1;

    /**

    • 字符串ID(UUID/雪花ID字符串等)转换为Bitmap可用的整数offset
    • @param userId 字符串类型用户ID(非空)
    • @return 唯一整数offset(0 ~ MAX_OFFSET) */ public static long stringToOffset(String userId) { // 非空校验 Assert.notBlank(userId, "userId cannot be blank"); // 使用MurmurHash3算法(128位)计算哈希值,取低32位转为正数 long hash = MurmurHash3.hash128(userId.getBytes())[0]; // 确保offset为非负数(哈希值可能为负,取绝对值后与MAX_OFFSET取模,避免越界) return Math.abs(hash) % MAX_OFFSET; }

    /**

    • 测试示例 */ public static void main(String[] args) { // 示例1:UUID类型用户ID String uuidUserId = "f47ac10b-58cc-4372-a567-0e02b2c3d479"; long offset1 = stringToOffset(uuidUserId); System.out.println("UUID用户ID转换后offset:" + offset1); // 输出示例:1234567890

      // 示例2:雪花ID字符串类型 String snowflakeUserId = "1640995200000123456"; long offset2 = stringToOffset(snowflakeUserId); System.out.println("雪花ID字符串转换后offset:" + offset2); // 输出示例:987654321

      // 校验offset范围(确保在0 ~ MAX_OFFSET之间) Assert.isTrue(offset1 >= 0 && offset1 <= MAX_OFFSET, "offset out of range"); Assert.isTrue(offset2 >= 0 && offset2 <= MAX_OFFSET, "offset out of range"); } }

关键说明:
  • 依赖引入:上述代码依赖 Apache Commons Codec 包(MurmurHash3实现),Maven依赖如下: commons-codec commons-codec 1.15

  • 碰撞优化:MurmurHash 碰撞率极低,若需进一步降低碰撞风险,可采用“双重哈希+取模”方式(如结合MD5哈希再取模);

  • offset范围:转换后的offset需控制在 0 ~ 2^32 - 1 之间,避免超出Bitmap最大支持范围;

  • 调用示例:用户活跃标记时,直接调用工具类转换ID,再执行SETBIT命令: // 字符串用户ID转换为offset long offset = UserIdHashUtil.stringToOffset("f47ac10b-58cc-4372-a567-0e02b2c3d479"); // 执行活跃标记 stringRedisTemplate.opsForValue().setIfAbsent("uv:dau:20260226", offset, 1, TimeUnit.SECONDS);

3.2 日活(DAU)实现

3.2.1 实现逻辑

日活定义:当日有过访问、操作(如登录、浏览、下单)的用户,去重后的总数。

核心逻辑:每日生成一个独立的 Bitmap,用户活跃时标记对应位为1,当日结束后统计 Bitmap 中1的个数,即为当日DAU。

3.2.2 具体操作

  1. key 设计:uv:dau:yyyyMMdd(如 uv:dau:20260226,表示2026年2月26日的日活 Bitmap);

  2. 活跃标记:用户触发活跃行为(如登录)时,执行原子命令: # offset 为用户唯一整数ID(字符串ID需通过上述工具类转换),1表示活跃 SETBIT uv:dau:20260226 10001 1 说明:同一用户当日多次活跃,多次执行 SETBIT 命令不影响结果(位值从1设为1,无变化),天然实现去重。

  3. DAU 统计:当日任意时间(或次日凌晨)执行统计命令,秒级返回结果: # 统计20260226当日活跃用户数(去重) BITCOUNT uv:dau:20260226

  4. 数据清理:可保留近30天的日活 Bitmap,用于周活、月活统计;超过30天的可定期删除(如通过Redis过期时间自动清理),避免占用过多内存。

3.2.3 性能测算

亿级用户场景:单个日活 Bitmap 占用约12MB内存;SETBIT 命令QPS支持10万+,满足高并发场景;BITCOUNT 命令统计亿级数据仅需毫秒级,完全满足运营统计需求。

3.3 周活(WAU)实现

3.3.1 实现逻辑

周活定义:近7天(自然周或滚动7天)有过活跃行为的用户,去重后总数。

核心逻辑:利用 Bitmap 的 OR 位运算(多个 Bitmap 中,任意一个 Bitmap 的对应位为1,结果即为1),合并近7天的日活 Bitmap,合并后的 Bitmap 中1的个数即为周活。

3.3.2 具体操作

  1. 依赖资源:近7天的日活 Bitmap(如 uv:dau:20260220 至 uv:dau:20260226);

  2. 合并 Bitmap:执行 BITOP OR 命令,将7天的日活 Bitmap 合并为一个新的 Bitmap: # 合并20260220-20260226的日活数据,结果存入 uv:wau:20260220-20260226 BITOP OR uv:wau:20260220-20260226 uv:dau:20260220 uv:dau:20260221 uv:dau:20260222 uv:dau:20260223 uv:dau:20260224 uv:dau:20260225 uv:dau:20260226 说明:OR 运算天然实现去重——同一用户只要任意一天活跃(对应位为1),合并后该位仍为1,无需额外去重逻辑。

  3. WAU 统计:执行 BITCOUNT 命令,统计合并后 Bitmap 中1的个数: # 统计20260220-20260226的周活用户数 BITCOUNT uv:wau:20260220-20260226

  4. 滚动周活实现:若需统计“近7天滚动周活”(如当日往前推6天),则每日合并“当日+前6天”的日活 Bitmap,覆盖原有滚动周活 key 即可。

3.3.3 性能优化

  • 合并时机:避免高峰时段合并,建议在每日凌晨(日活数据稳定后)执行合并操作,减少对业务的影响;

  • 内存优化:合并后的周活 Bitmap 可设置过期时间(如1天),避免长期占用内存;

  • 集群优化:若Redis为集群模式,确保7天的日活 Bitmap 落在同一节点(通过key哈希分片控制),避免跨节点位运算导致的性能损耗。

3.4 月活(MAU)实现

实现逻辑与周活完全一致,仅需将“7天日活 Bitmap”替换为“当月所有日活 Bitmap”,执行 BITOP OR 合并后,再用 BITCOUNT 统计即可。

示例命令:

合并202602月份所有日活数据,统计月活

BITOP OR uv:mau:202602 uv:dau:20260201 uv:dau:20260202 ... uv:dau:20260228 BITCOUNT uv:mau:202602

注意:当月天数根据实际月份调整,合并时需包含当月所有日期的日活 Bitmap。

4. 方案对比与优势

4.1 与其他统计方案对比

实现方案

优点

缺点

适用场景

MySQL 去重统计

实现简单,无需额外组件

亿级数据查询极慢(全表扫描+去重),易导致数据库宕机

小数据量(万级以下)统计

Redis Set 存储活跃用户

实现简单,支持快速去重

内存占用极高(1亿用户约需1GB+),高并发场景性能下降

百万级用户统计

Redis Bitmap

极省内存(亿级12MB/天)、统计速度快(毫秒级)、原子操作、支持高并发

需将用户ID转换为整数offset,不支持非整数ID直接使用

亿级、十亿级用户日活/周活/月活统计,签到等场景

4.2 Bitmap 方案核心优势

  • 空间高效:亿级用户日活仅需12MB,远优于Set、MySQL方案;

  • 性能优异:SETBIT、BITCOUNT、BITOP 均为原子操作,毫秒级响应,支持高并发;

  • 天然去重:同一用户多次活跃仅标记一次,无需额外去重逻辑;

  • 易于扩展:支持周活、月活等多维度统计,仅需简单位运算;

  • 维护成本低:命令简洁,无需复杂逻辑,可结合Redis过期时间自动清理历史数据。

5. 生产环境注意事项

  1. 用户ID处理:确保用户ID为唯一整数,若为字符串ID(如UUID),需通过哈希算法(如MurmurHash)转换为整数,避免offset冲突;转换时需注意哈希碰撞,可通过双重哈希优化(代码示例见3.1节)。

  2. Redis 集群适配:若使用Redis集群,需确保同一维度的 Bitmap(如同一周的日活 Bitmap)落在同一节点,避免跨节点位运算导致的性能损耗;可通过key前缀+哈希分片控制节点分配。

  3. 过期时间设置:日活 Bitmap 可设置30天过期时间,周活、月活 Bitmap 可设置1天过期时间,避免历史数据占用过多内存;过期时间建议在生成key时通过 EXPIRE 命令设置。

  4. 并发安全:所有 Bitmap 命令均为原子操作,无需额外加锁;但需注意用户ID转换的唯一性,避免不同用户映射到同一offset,导致统计错误。

  5. 性能监控:生产环境需监控 Bitmap 相关命令的执行耗时(如 BITOP 合并大量数据时的耗时),避免影响Redis整体性能;可通过Redis监控工具(如RedisInsight)实时监控。

  6. 降级方案:若Redis集群故障,可临时降级为“抽样统计”(如抽样10%用户),避免统计功能完全不可用;故障恢复后,再补全统计数据。

6. 总结

Redis Bitmap 是基于 String 类型的位级存储方案,核心优势是空间利用率极高、操作原子高效,适用于二值状态标记和大数据量统计场景。其中,亿级用户日活、周活、月活统计是其最典型的应用,通过“每日Bitmap标记+位运算合并”的方式,实现了高效去重、秒级统计,且内存占用极低,是互联网公司产品运营统计的首选方案。

生产环境落地时,需重点关注用户ID转换、Redis集群适配、过期时间设置等细节,确保统计数据准确、服务稳定。同时,Bitmap 也可结合其他Redis特性(如布隆过滤器),进一步拓展其应用场景,提升分布式系统的性能和可靠性。