千万级在线状态需用SETBIT+GETBIT配合心跳机制,避免网络抖动导致误判离线;服务端每分钟BITPOS扫描最近120位定位超时用户;统计实时在线数应按时间维度建key,而非BITCOUNT单个用户。

直接用 SETBIT + GETBIT 就能撑住千万级用户在线状态标记,但关键不在“能不能存”,而在“怎么避免误判离线”和“怎么不把 Redis 压垮”。
在线状态不能只靠单次 SETBIT
用户上线时 SETBIT user:online:1001 0 1 是对的,但下线时不能只靠客户端发 SETBIT ... 0 —— 网络抖动、进程崩溃、强杀 App 都会导致位残留,状态永远为“在线”。
- 必须搭配心跳机制:客户端每 30 秒
SETBIT user:online:1001 <code>timestamp_sec / 601,用分钟级时间戳作 offset,保留最近 2 小时(120 个 bit) - 服务端定时(如每分钟)用
BITPOS user:online:1001 0 -120扫最近 120 位,从右往左找第一个 0;若找到位置p,说明最后一次心跳在(now - p) * 60秒前,超 180 秒即判定离线 - 别用
EXPIRE控制 key 生命周期——SETBIT不刷新 TTL,key 会永久存在;改用BITFIELD user:online:1001 SET i8 0 0清旧位,或定期DEL过期 key
统计活跃用户数别直接 BITCOUNT 全量 key
BITCOUNT user:online:1001 是查单个用户历史心跳数,不是查当前在线数。要统计“此刻在线用户总数”,得反过来:每个用户一个 bit,但 key 要按时间维度组织。
- 正确做法:用固定 key
online:20260521:18(精确到小时),offset = 用户 ID mod 2^24(约 1600 万),写入前必须校验if uid >= 1 - 这样
BITCOUNT online:20260521:18就是该小时在线用户数,O(1),且内存可控(1600 万用户 ≈ 2MB) - 禁止把所有用户塞进同一个 key 并用大 offset:offset 超过 2^32-1 会触发 Redis 内部 realloc,单次操作卡顿可达毫秒级;更糟的是,offset 稀疏(比如只写 1 和 1000000)会让底层 SDS 分配大量零字节,浪费内存
Go 里调用 go-redis/v9 的位图方法要绕开 Do() 手拼命令
手动 Do(ctx, "SETBIT", "key", offset, 1) 看似灵活,但容易传错类型、漏处理负 offset、忽略返回值语义。
立即学习“go语言免费学习笔记(深入)”;
- 用封装好的方法:
rdb.SetBit(ctx, key, int64(offset), 1),它内部自动转int64→string,且对负 offset 直接 panic,逼你提前校验 -
offset必须是非负整数:用户 ID 是int64时,别直接uint64(id) & 0xFFFFFFFF——ID 为负会变成极大正数;应先if id - 批量查状态用
GETRANGE比循环GETBIT快 10 倍以上:比如查 1000 个用户,先算出它们的 offset 区间,rdb.GetRange(ctx, key, startByte, endByte).Val()拿回字节流,再本地解析 bit
离线用户清理和冷热分离必须做,否则内存只增不减
Redis 位图不会自动收缩。写过 offset=1000000 的 key,即使后续只写 offset=0~10,底层分配的内存仍是 ~125KB(1000000/8)。
- 每天凌晨执行
BITOP AND temp_key base_key mask_key:用掩码 key(只保留最近 7 天有效 bit)与原 key 做 AND,结果写入新 key,再原子替换 - 或更轻量:用 Lua 脚本遍历
GETRANGE返回的字节段,识别连续零字节块,SETBIT写 0 回去清空冗余区域(注意别清掉有效位) - 长期不用的用户位图(如 90 天无心跳),不要
DEL后重置——下次上线又得重新分配内存;改用SET user:online:1001 ""清空字符串内容,保留 key 结构,节省 realloc 开销
最易被忽略的一点:在线状态的“实时性”和“准确性”永远在 trade-off。用分钟级 offset 换来的低延迟,必然牺牲秒级下线感知;而追求精确到秒,就得上分布式锁 + 客户端保活 + 服务端兜底扫描,复杂度指数上升。选哪个,取决于你的业务到底需要“看起来在线”,还是“必须准确在线”。


















