redis签到与去重必须用setbit/getbit/bitcount等命令,而非java bitset;因后者是单机内存对象,无共享、无持久化、无原子性,不支持分布式统计与跨key聚合。

BitSet 本身不适用于 Redis 场景下的签到与去重——你真正该用的是 Redis 的 SETBIT/GETBIT/BITCOUNT 命令,而不是 Java 的 BitSet 类。 后者是单机内存结构,无法跨进程共享、不支持原子操作、更没法做分布式统计。所有“用 BitSet 实现 Redis 签到”的说法,本质是混淆了底层原理和实际工具边界。
为什么不能直接用 Java 的 BitSet 做签到存储
常见错误现象:本地 new 一个 BitSet,存用户签到状态,跑压测时发现并发写错位、重启后数据全丢、集群间状态不一致。
-
BitSet是 JVM 进程内对象,无持久化、无共享、无原子性 ——set(123, true)在多线程下可能被覆盖 - 它不支持按日期分片、无法做跨 key 聚合(比如“近 7 天连续签到”),也没法用
BITOP合并多个 Bitmap - 一旦用户量上千万,单个
BitSet内存占用飙升(比如 1 亿 bit ≈ 12.5MB),但 Redis 的 string 类型能自动分片、LRU 淘汰、AOF/RDB 持久化
SETBIT 的 offset 怎么算才不越界也不浪费
关键不是“用户 ID 直接当 offset”,而是映射后控制范围。offset 超过 2^32-1(约 42 亿)会报错 ERR bit offset is not an integer or out of range,且过大导致 Redis 单 key 内存爆炸。
- 按月建 key:
user:sign:{userId}:202604,offset = 当月第几天 − 1(即 4 月 1 日 → 0,4 月 21 日 → 20) - 若需按用户 ID 映射(如去重场景),必须哈希压缩:
MurmurHash3.hash64(userId) & 0x7FFFFFFF再 % 有效容量(如 1000 万),避免负数和溢出 - 永远不要用原始用户 ID 做 offset:ID=9999999999 的用户会导致 offset 超限,Redis 直接拒绝写入
去重统计时 BITCOUNT 和 BITOP OR 的真实开销
看起来 BITCOUNT O(1),但实际复杂度取决于 bitmap 长度;BITOP OR 合并 N 个 key 时,时间取决于最长那个 key 的字节长度,不是用户数。
- 单 key 统计:比如
active:202604存 3000 万用户活跃标记,BITCOUNT active:202604耗时通常 - 跨月合并:想统计 “202603 OR 202604” 活跃用户,用
BITOP OR active:q1 active:202603 active:202604,若两个 key 都有 3000 万 bit,结果 key 将达 ~3.75MB,首次执行可能卡 20–50ms - 高频调用时,建议预计算:每天凌晨用
BITOP OR合并昨日 + 今日,生成active:7d,业务查这个 key 而非实时算
连续签到天数怎么查?别硬写 Lua 脚本
Redis 没有原生命令返回“最长连续 1 的长度”,但 BITPOS 可大幅降低客户端遍历成本。
- 先用
BITPOS key 0找第一个未签到日(返回 offset),再往前查上一个0的位置,差值就是连续段 - 更稳的做法:用
GETBIT批量读取最近 31 天(pipeline一次发 31 条),客户端 for 循环找最长连续 1 —— 比 Lua 脚本更可控、易调试、不阻塞 Redis - 注意:
BITPOS key 1 0是从 offset=0 开始找第一个 1,不是“找连续段”,别误用
最易被忽略的一点:Bitmap 的 key 生命周期必须和业务对齐。比如签到 key user:sign:123:202604 应在 5 月 1 日自动过期(EXPIRE),否则几年积累下来,冷数据占满内存又查不到——这不是算法问题,是运维契约没写进代码里。










