文章目录

Redis Bitmap实现亿级用户签到统计的内存优化

发布于 2026-06-13 15:37:58 · 浏览 5 次 · 评论 0 条

Redis Bitmap实现亿级用户签到统计的内存优化

为什么传统方案行不通?

为亿级用户(如1亿)存储每日签到状态,最直观的方法是为每位用户创建一个记录。使用传统关系型数据库,一张表可能包含 user_iddateis_signed 等字段。即使 is_signed 用最小的 TINYINT(1) 类型,存储 一天全量用户的签到数据也需要约 100,000,000 * 1 字节 ≈ 95.4 MB。如果查询“某用户本月连续签到天数”,需要扫描该用户当月所有记录,效率低下。

Redis Bitmap(位图)提供了一种极致压缩的解决方案。它将每个用户的状态映射为一个二进制位(bit),8个用户的状态才占用1个字节。查询 连续签到只需对位进行高效的操作。

第一步:理解 Bitmap 的核心思想

Redis Bitmap 不是独立数据结构,而是对字符串(String)类型的一系列按位操作命令的封装。一个字符串键值,可以视为一个无限长的、按顺序编号的二进制位数组。

核心优势

  1. 空间效率:每个用户状态仅占1 bit,是传统 TINYINT 方案内存占用的 1/8
  2. 操作高效:签到、查询、统计等操作底层是二进制位运算,时间复杂度为 O(1)。

第二步:设计你的键(Key)策略

高效的键设计是所有优化的开始。对于每日签到,我们通常以 “天”“月” 为粒度设计键。

推荐方案:使用 sign:202310 作为键名,代表 2023年10月 的用户签到集合。每位用户根据其 user_id 映射到该键的第 user_id 个 bit 位上。

操作步骤

  1. 生成 键名。格式示例:sign:{年份}{月份}, 如 sign:202310
  2. 确保 键的类型为 Redis String。Bitmap 操作会自动将字符串视为位数组。

第三步:执行每日签到操作

当一个用户(假设ID为 10086)在某天(假设为10月1日)完成签到,我们需要将对应位设置为 1

操作步骤

  1. 确定 目标键:sign:202310
  2. 确定 该用户在当前月份的第几天签到:10月1日即为第1天,偏移量为 0(因为位数组从0开始计数)。
  3. 执行 SETBIT 命令。
# 命令格式:SETBIT key offset value
# 将键 `sign:202310` 的第 `10086` 位(代表用户ID 10086)的值设置为 `1`。
# 注意:这里的 offset 就是 user_id, day - 1 是当日序号(从0开始)
# 例如,用户10086在10月1日(本月第1天)签到:
SETBIT sign:202310 10086 1

解释offset 直接使用用户 user_id。这样,每个用户在月键中都有一个固定的、唯一的 bit 位。一个键可以管理全月所有用户的所有天数签到。

修正:上述设计 offset 仅为 user_id,只能存储该用户本月是否签过到(一次),无法记录具体哪天签到。正确的键设计应为每天一个键,或使用 sign:202310:{day} 作为键名, offsetuser_id

修正后的签到操作步骤(以天为粒度):

  1. 生成 今日键名:sign:20231001(代表2023年10月1日)。
  2. 执行 SETBIT sign:20231001 10086 1

第四步:查询用户签到状态

检查用户 10086 在2023年10月1日是否签到。

操作步骤

  1. 执行 GETBIT 命令。
    # 命令格式:GETBIT key offset
    GETBIT sign:20231001 10086
  2. 解析 返回值。返回 1 表示已签到,返回 0 表示未签到。

第五步:统计关键指标

Bitmap 的强大之处在于可以对多个键进行位运算,瞬间得到统计结果。

场景一:统计某天签到总人数
操作步骤

  1. 执行 BITCOUNT 命令。
    # 统计 `sign:20231001` 这个键中,值为1的bit位数量。
    BITCOUNT sign:20231001
  2. 获取 结果。返回值即为当日签到总人数。

场景二:统计用户本月连续签到天数(核心优化)
操作步骤

  1. 确定 月份和用户:user_id=10086年月=202310截止日=15
  2. 收集 截止到指定日期的所有单日键:sign:20231001sign:20231015
  3. 使用 BITOP 命令对多个键进行“与”操作,合并出一个临时结果键。
    # BITOP AND destkey key [key ...]
    # 对多个键的每个对应bit位进行AND(与)运算,结果存入 destkey。
    # 只有用户在这15天里每天都签到了(每天对应位都是1),结果键中的该位才为1。
    BITOP AND user:10086:oct15:check sign:20231001 sign:20231002 ... sign:20231015
  4. 检查 结果键中用户对应的 bit 位。
    GETBIT user:10086:oct15:check 10086
  5. 如果返回 1,则该用户从1号到15号连续签到。通过循环或二分查找(配合 BITOP),可以高效确定最长连续签到天数

更高效的“OR”合并查询:通常,我们更想知道用户本月哪些天签到了。可以使用 BITOP OR 合并一个月的所有键。

# 合并10月1日到31日的所有签到数据
BITOP OR sign:202310:merged sign:20231001 sign:20231002 ... sign:20231031
# 然后对合并后的键进行查询
GETBIT sign:202310:merged 10086 # 查询该用户本月是否签过到(任何一天)
BITCOUNT sign:202310:merged     # 统计本月至少签到一次的用户总数

第六步:内存占用精确计算

我们来计算存储1亿用户一个月签到数据的内存消耗。

计算公式
用户总数(N) / 8 = 字节数(Bytes)
字节数 / 1024 = 千字节(KB)
千字节 / 1024 = 兆字节(MB)

计算过程(以存储一个月(31天)数据为例,方案A为每天一个键):

  1. 单个日键 内存:100,000,000 / 8 = 12,500,000 Bytes ≈ 11.92 MB
  2. 一个月总内存(31个键):11.92 MB * 31 ≈ 369.6 MB

这看似不小,但对比传统方案:

方案 存储逻辑单位 每条记录大小 (假设) 存储1亿用户1个月数据 优化比例
关系型数据库 行 (Row) 约 20 Bytes (id, date, status等) 100,000,000 * 31 * 20 B ≈ 58.18 GB 基准
Redis Hash 字段 (Field) 约 50 Bytes (key overhead + field + value) 100,000,000 * 31 * 50 B ≈ 145.5 GB 更高
Redis Bitmap 位 (Bit) 1/8 Byte 100,000,000 * 31 / 8 B ≈ 369.6 MB 降低约 160倍

结论:Bitmap 方案将内存消耗从数十GB级别,压缩到了数百MB级别,实现了数量级的优化

第七步:关键优化与注意事项

  1. 键的过期:务必为每日签到键(如 sign:20231001)设置 TTL,例如 EXPIRE sign:20231001 2592000(30天),避免磁盘空间无限增长。
  2. 稀疏用户ID的处理:如果用户ID不连续(如 1, 100000, 200000),直接使用ID作为 offset 会极大浪费内存。此时应建立一个 “用户ID到连续数字” 的映射表,将稀疏ID转换为从 0 开始的连续序号。
  3. 批量操作:对于初始化或数据迁移,使用 PipelineLua 脚本批量执行 SETBIT 命令,减少网络开销。
  4. 监控:使用 MEMORY USAGE key 命令定期检查键的实际内存占用,确保符合预期。

稀疏ID映射示例

# 假设有一个全局自增ID,用于生成紧凑的位偏移量。
# 1. 将原始用户ID映射到紧凑ID。
SET compact:id:10001 0
SET compact:id:10002 1
SET compact:id:10003 2
# ... 在业务代码中维护这个映射。

# 2. 签到时使用紧凑ID作为offset。
SETBIT sign:20231001 0 1  # 紧凑ID 0 的用户签到
SETBIT sign:20231001 2 1  # 紧凑ID 2 的用户签到

通过以上步骤,你可以使用 Redis Bitmap 构建一个能够支撑亿级用户、存储开销极低、查询速度极快的签到统计系统。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文