必须用 offset = user_id - 1 计算偏移量,否则因 offset 从 0 开始计数会导致所有位错一位;id 非自增时需先映射为连续整数,且全链路必须统一 offset 计算逻辑。

直接用 SETBIT + GETBIT 配合偏移量映射是最轻量、最省内存的方案;Hash 适合需要同时存多字段(如 last_active_time、ip、device)的场景,但单用户开销大、不适用于千万级在线判断。
Bitmap 的 offset 怎么算才不会错位
用户 ID 是数据库自增主键(从 1 开始)时,必须做 offset = user_id - 1。跳过这步会导致所有位向右偏移一位:比如想标记 ID=1001 的用户在线,却执行了 SETBIT online_users 1001 1,实际写入的是第 1002 位(因为 offset 从 0 计数),后续 GETBIT online_users 1001 返回 0,误判为离线。
-
user_id必须是正整数,负数或字符串会报ERR value is not an integer or out of range - 若 ID 来自 Snowflake、UUID 或分库分表,不能直接当 offset —— 得先用 Redis Hash 做映射:
HSET id_seq_map user_uuid_abc 12345,再取HGET id_seq_map user_uuid_abc得到连续整数 - 超大 ID(如 > 2³²)在旧版 Redis(BITOP 截断或溢出,建议升级或加校验
SETBIT 和 GETBIT 必须用同一套 offset 规则
写和读的 offset 不一致是线上最隐蔽的 bug 来源。常见错误是登录时用 user_id - 1,但判断时忘了减 1,结果永远读不到刚写进去的位。
- 登录成功后立刻执行:
redisTemplate.opsForValue().setbit("online_users", userId - 1, true) - WebSocket 断连或登出时执行:
redisTemplate.opsForValue().setbit("online_users", userId - 1, false) - 判断是否在线统一用:
redisTemplate.opsForValue().getBit("online_users", userId - 1),返回true表示在线,false或null均表示离线 - 绝对不要用
EXISTS online_users判断用户是否在线 —— key 存在但某位是 0,就会误认为“该用户在线”
Hash 结构适合什么场景,为什么不适合纯在线判断
Hash 本质是字符串哈希表,每个 field-value 对至少占用几十字节(含 key 开销、编码、指针等)。对单个布尔状态而言,它浪费了 100 倍以上的内存。
- 适合场景:需记录用户最后一次心跳时间、IP、设备类型、客户端版本等复合信息,例如:
HSET user:10086 last_seen 1748209800 ip "192.168.1.5" device "ios" - 不适合场景:只关心“在线 / 离线”二值状态,尤其是用户量 > 100 万时 —— 同样存 1000 万用户,Bitmap 占约 1.2 MB,Hash 可能突破 100 MB
- 如果硬要用 Hash 做在线判断,务必配合 TTL:
EXPIRE user:10086 7200(2 小时),否则脏数据堆积无法清理
真正容易被忽略的是 offset 映射的**一致性边界**:不是“写一次就完事”,而是整个服务生命周期里,所有模块(登录、登出、心跳、定时扫描、后台查询)都必须使用完全相同的 offset 计算逻辑。哪怕一个定时任务里漏写了 - 1,就会导致一批用户永久“隐身”。











