因为SETBIT以1bit存储单个用户状态,千万用户仅需约1.25MB内存,而SET存储字符串至少耗50字节/用户,同样规模超500MB;且SETBIT基于紧凑SDS位操作,原子安全、空间极致高效。

为什么用 SETBIT 而不是 SET 存用户在线状态
单个用户在线/离线只需要 1 bit,用 SET 存字符串(比如 "1")会浪费至少 50 字节以上内存——Redis 的字符串对象有额外开销,而位图底层是紧凑的 SDS + 位操作。千万用户对应约 1.25 MB 内存(10⁷ ÷ 8 ÷ 1024 ÷ 1024),用 SET 存则轻松突破 500 MB。
关键点:SETBIT 操作的是一个 key 对应的位数组,user_id 作为 offset,值为 1 表示在线,0 表示离线。注意:user_id 必须是非负整数,且不能太大(offset 上限约 2⁴⁸,但实际建议控制在 int64 安全范围内)。
- 避免用 UUID 或字符串 ID 直接当 offset——必须映射成单调递增整数(例如数据库自增主键,或用 Redis
INCR预分配) - 不要对同一个 key 写入稀疏 offset(如只设 offset=1 和 offset=10000000),中间未设置的位默认为 0,但 Redis 仍会分配内存到最大 offset 所在字节块
- 如果用户 ID 天然稀疏(比如保留了大量删除 ID),考虑按时间分片 key,例如
online:202405,用月份隔离
Go 客户端怎么安全调用 SETBIT 和 BITCOUNT
别直接拼命令字符串,用官方 github.com/go-redis/redis/v9 的 SetBit 和 BitCount 方法,它们自动处理类型转换和错误返回。
典型误用:用 client.Do(ctx, "SETBIT", key, uid, 1) 忘记检查返回值类型,结果拿到 interface{} 后断言失败 panic;或者没传 context.WithTimeout,导致网络卡住时 goroutine 泄漏。
立即学习“go语言免费学习笔记(深入)”;
-
SetBit返回旧值(0 或 1),可用于判断是否重复上线:old, err := client.SetBit(ctx, "online:202405", int64(uid), 1).Result() -
BitCount默认统计整个位图,加start/end参数可限定字节范围(单位是 byte,不是 bit),例如查某天活跃用户可配合分时 key 设计,而非在大位图里扫全量 - 并发写同一 key 的不同 offset 是安全的,Redis 位操作是原子的;但多个 goroutine 同时对同一 offset 调用
SETBIT,结果取决于最后执行者——这点需业务层确认是否可接受
如何应对 BITCOUNT 在亿级位图上变慢
BITCOUNT 时间复杂度是 O(N),N 是位图字节数。一千万用户约 1.25 MB,实测通常
解决方案不是硬扛,而是换思路:用「采样 + 增量统计」替代全量扫描。
- 每 5 分钟用
BITCOUNT算一次当前 key 总在线数,写入另一个 key(如online:202405:summary:1620234000),供后台报表用 - 实时接口改查缓存好的最近 3 个采样点的平均值,误差可控且稳定
- 真要精确实时总数?加个本地计数器(
sync/atomic.Int64),每次SETBIT成功后按旧值→新值变化做 ±1 更新——前提是能保证所有服务实例都走同一套逻辑,且不重启丢数据
上线前必须验证的三个边界场景
位图看着简单,但线上出问题往往卡在边缘 case。
- 用户 ID 为 0:
SETBIT key 0 1合法,但容易被当成空值忽略,Go 里int64(0)传进去不会报错,得在业务层显式允许或拒绝 - Redis key 过期:位图不支持为单个 bit 设置过期,只能给整个 key 设
EXPIRE。如果用月维度 key(如online:202405),记得在下月初主动DEL上月 key,否则残留垃圾 - GETBIT 返回 -1:说明 key 不存在,不是“用户不在线”,而是“从未写入过该用户”。业务代码里别把
-1当0处理,否则会误判大量用户为离线
最易被忽略的是 offset 溢出:Go 传 int64 给 SetBit,但如果 uid 实际是 uint64 且 > 2⁶³−1(约 9×10¹⁸),会溢出变负数,触发 Redis 报错 ERR bit offset is not an integer or out of range。上线前务必用最大可能 user_id 做压测。


















