Redis位图用SETBIT/GETBIT标记千万级在线状态可行,但需避免误判离线:必须结合心跳机制(如每30秒写分钟级时间戳offset)、BITPOS检测超时、按时间维度分key统计BITCOUNT,禁用EXPIRE而改用BITFIELD清旧位或定期DEL,且offset须为非负整数并合理分片防内存浪费。

直接用 SETBIT + GETBIT 能撑住千万级在线状态标记,但关键不在“能不能存”,而在“怎么避免误判离线”和“怎么不把 Redis 压垮”。
为什么不能只靠客户端发 SETBIT key offset 0 下线
网络抖动、进程崩溃、强杀 App 都会导致位残留,状态永远为“在线”。Redis 不会自动感知连接断开,单次写入无法反映真实生命周期。
- 必须搭配心跳机制:客户端每 30 秒执行
SETBIT user:online:1001 (timestamp_sec / 60) 1,用分钟级时间戳作 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 user:online:1001
BITCOUNT 是查单个用户历史心跳数,不是查当前在线数。要统计实时在线总数,得反过来设计:每个用户一个 bit,但 key 要按时间维度组织。
- 正确做法:用固定 key
online:20260525:11(精确到小时),offset = 用户 ID mod 2^24(约 1600 万),写入前必须校验if uid >= 1 - 这样
BITCOUNT online:20260525:11就是该小时在线用户数,O(1),且内存可控(1600 万用户 ≈ 2MB) - 禁止把所有用户塞进同一个 key 并用大 offset:
offset超过 2^32−1 会触发 Redis 内部 realloc,单次操作卡顿可达毫秒级;更糟的是,offset 稀疏(比如只写 1 和 1000000)会让底层 SDS 分配大量零字节,浪费内存
Go 客户端调用 SetBit 和 BitCount 的坑
手拼 Do(ctx, "SETBIT", ...) 看似灵活,但容易传错类型、漏处理负 offset、忽略返回值语义。go-redis/v9 提供封装方法,但细节仍需手动兜底。
立即学习“go语言免费学习笔记(深入)”;
- 用
rdb.SetBit(ctx, key, int64(offset), 1),它内部自动转 int64→string,且对负 offset 直接 panic,逼你提前校验;offset 必须是非负整数:用户 ID 是int64时,别直接uint64(id) & 0xFFFFFFFF——ID 为负会变成极大正数;应先if id -
BitCount的Start/End参数是 byte 索引,不是 bit 索引;但 go-redis/v9 的redis.BitCount结构体已自动按 bit 换算,你传入的仍是 bit offset(源码里做了 div 8 和 mod 8) - 如果用
Do()手动发BITCOUNT key start end,必须传 byte 偏移:即start/8、end/8,否则结果错乱
Redis 集群下在线状态 key 必须带 {} 分组
在线状态这类功能依赖高频读写和批量扫描,集群环境下 key 路由错乱会直接导致状态丢失或查询失败。
- 错误写法:
user:123:online→ 不同用户 hash 到不同 slot,MGET 批量查大概率触发CROSSSLOT错误,SCAN 也无法跨节点聚合 - 正确写法:
user:{123}:online→ 强制所有 123 相关 key 落在同一 slot,保障心跳更新、批量查询、定时扫描都在单节点完成 - 初始化
redis.NewClusterClient()时,Addrs至少填一个在线节点,Password全局统一,Dialer必须设超时(首次拓扑发现可能耗时 >1s)
最易被忽略的是:位图本身不提供“时效性语义”,所有超时判断、分片策略、集群路由都得业务层硬扛;一旦 offset 映射出错或 key 设计失当,问题会在千万量级上指数放大,排查成本远高于初期多写几行校验逻辑。


















