亿级用户场景下的签到统计?Redis Bitmap 高效解决方案
在当今的互联网应用中,日活统计、用户签到、连续打卡等是极为常见的运营需求。如何为海量用户高效地记录和统计这些“是/否”状态?传统关系型数据库(如 MySQL)COUNT(*)
的方式在小体量下尚可应对,但面对亿级用户时,每天产生海量记录,将导致存储空间爆炸和查询性能急剧下降。
本文将介绍 Redis 中一种强大而节省空间的数据结构——Bitmap(位图),并演示如何利用它轻松解决亿级用户的签-到统计难题。
什么是 Redis Bitmap?
很多人可能会误以为 Bitmap 是 Redis 的一种全新数据类型,但实际上并非如此。Bitmap 在本质上是构建在 Redis String 类型之上的按位(bit)操作。
你可以将 Bitmap 想象成一个以 bit
为单位的数组,数组的每个单元只能存储 0
或 1
。这个数组的索引(index)在 Bitmap 中被称为偏移量(offset) 。
由于其底层是 String 类型,一个 String 类型的值最多可以存储 512MB 的数据,因此 Bitmap 支持的最大位数是 2^32
位。这意味着,我们仅需 512MB 的内存,就可以为超过 42.9 亿(2^32 = 4,294,967,296
)个对象记录一个二值状态。这在处理大规模用户状态时,展现了无与伦比的空间效率。
Bitmap 的应用场景
Bitmap 主要用于统计大量的布尔状态(Y/N, true/false, 0/1),其核心应用场景包括:
- 日活/月活统计:记录用户每天/每月是否登录过。
- 用户签到打卡:例如钉钉的每日打卡,京东每日签到领京豆。
- 功能或广告触达统计:记录用户是否看到过某个功能引导或点击过某个广告。
- 用户在线状态:记录用户当前是否在线。
总而言之,只要是需要统计二值状态的场景,都可以考虑使用 Bitmap 作为高效解决方案。
痛点分析:传统方式为何难以应对海量签到?
以京东签到领京豆为例。截至2020年3月,京东的年度活跃用户数已达3.87亿。假设其中有10%的用户(约3870万)参与每日签到。
如果采用传统 MySQL 的方式,为每个用户每次签到都创建一条记录:
user_id | sign_in_date |
---|---|
1001 | 2025-09-25 |
1002 | 2025-09-25 |
… | … |
这种设计会带来两个致命问题:
- 巨大的存储开销:一天就会产生近 4000 万条记录,一个月就是 12 亿条。这对数据库的存储是巨大的挑战。
- 统计查询性能瓶颈:当需要查询某用户本月签到天数,或统计某天总签到人数时,
COUNT
操作将变得非常缓慢。
如何破局?
我们的核心痛点是“一条记录对应一天签到”太过浪费。换个思路:一个月的签到状态最多只有31天,而一个 int
类型是32位。我们是否可以用一个 int
的每一位来代表每一天?当天签到,对应位就置为 1
,未签到则为 0
。
这样,一个用户一个月的签到记录,就可以用一个整数来存储。这个思路,正是 Bitmap 的核心思想。
四、基于 Redis Bitmap 的大厂签到解决方案
使用 Bitmap,每个用户每天的签到状态只占用 1 个 bit。一个月(31天)的记录占用 31个 bit,一年也只需 365个 bit。这极大地压缩了存储空间。
核心命令
1. SETBIT key offset value
设置指定 key
在 offset
(偏移量) 处的 bit 值 (value
只能是 0
或 1
)。
-
key
: 通常我们可以设计成u:sign:{userId}:{yyyyMM}
,代表某用户某年月的签到记录。 -
offset
: 代表第几天,注意偏移量从 0 开始。所以,1号的偏移量是0,2号是1,以此类推。 -
value
:1
代表已签到,0
代表未签到。
示例:用户 1001 在 2025年9月25日 签到。9月份的第25天,对应的 offset
是 24
。
# 用户1001在2025年9月25日签到
127.0.0.1:6379> SETBIT u:sign:1001:202509 24 1
(integer) 0 # 返回该位原来的值
2. GETBIT key offset
获取指定 key
在 offset
处的值。
示例:查询用户 1001 在 2025年9月25日 是否签到。
127.0.0.1:6379> GETBIT u:sign:1001:202509 24
(integer) 1 # 返回1,表示已签到
场景实战
有了这两个核心命令,我们就可以轻松实现复杂的签到统计功能了。
场景1:日活统计 (统计2025年9月25日所有签到用户)
我们可以为每一天创建一个 Bitmap key,s:20250925
。当用户签到时,以他的 userId
作为 offset
,将其置为 1
。
# 用户1001签到
SETBIT s:20250925 1001 1
# 用户8888签到
SETBIT s:20250925 8888 1
统计当天的总签到人数,只需使用 BITCOUNT
命令。
3. BITCOUNT key [start end]
统计指定 key
中值为 1
的 bit 数量,可以指定字节范围。
127.0.0.1:6379> BITCOUNT s:20250925
(integer) 2 # 当天总共有2人签到
场景2:统计指定用户一个月/一年之中的登陆天数
这个场景更简单,直接对该用户的月度/年度 key 执行 BITCOUNT
即可。
# 统计用户1001在2025年9月的总签到天数
127.0.0.1:6379> BITCOUNT u:sign:1001:202509
(integer) 1
场景3:查询最近一周/一个月的活跃用户
要统计最近7天都活跃的用户,我们需要用到 BITOP
命令。
4. BITOP operation destkey key [key ...]
对一个或多个 key 进行位运算(AND
, OR
, XOR
, NOT
),并将结果保存到 destkey
。
我们可以将最近7天的 key (s:20250919
到 s:20250925
) 进行 AND
运算。运算结果中,只有在7天里对应位都为 1
的 offset
(即 userId
)才会是 1
。
# 将最近7天的日活key进行AND运算,结果存入 temp:7day:active
BITOP AND temp:7day:active s:20250919 s:20250920 s:20250921 s:20250922 s:20250923 s:20250924 s:20250925
# 统计结果
BITCOUNT temp:7day:active
场景4:查询某用户一年中,哪几天登陆过?
这需要我们遍历该用户一年的 Bitmap (偏移量从 0 到 364),逐一使用 GETBIT
判断。在业务代码中循环处理即可。
场景5:统计连续签到天数
这个逻辑相对复杂,通常从今天开始向前遍历,直到遇到第一个 0
为止。
例如,要查询用户 1001 截止到 25号的连续签到天数:
-
GETBIT u:sign:1001:202509 24
(25号) -> 1 -
GETBIT u:sign:1001:202509 23
(24号) -> 1 -
GETBIT u:sign:1001:202509 22
(23号) -> 0,循环停止。
结论:连续签到2天。
总结
Redis Bitmap 通过其巧妙的设计,为处理海量用户的二值状态统计提供了一种极其高效和节省空间的解决方案。它将复杂的统计问题,转化为简单的位运算操作,充分体现了 Redis 在性能和资源利用上的极致追求。在你的下一个项目中,如果遇到类似的场景,不妨尝试一下 Bitmap,它可能会给你带来意想不到的惊喜。