直接用 SETBIT 会丢数据,是因为 GETBIT + SETBIT 组合非原子,在高并发下多个协程读-改-写同一比特时发生覆盖;必须用 Lua 脚本封装整个操作以保证原子性。

为什么直接用 SETBIT 会丢数据?
很多人一上来就用 SETBIT 往 Redis 写单个 bit,结果发现高并发下统计不准——根本原因是没考虑原子性:多个协程同时读-改-写同一个位图字节时,GETBIT + SETBIT 非原子,中间可能被覆盖。
真正安全的做法是把整个位图操作封装进 Lua 脚本,让 Redis 一次性执行。Go 客户端(如 github.com/go-redis/redis/v9)支持 Eval,但要注意脚本里不能依赖外部状态,且 key 必须显式传入(Redis Cluster 要求所有 key 在同一 slot)。
- 别在 Go 层做“先 GETBIT 再判断再 SETBIT”,这是典型竞态源
- Lua 脚本里用
redis.call("SETBIT", KEYS[1], ARGV[1], 1)是原子的,但得自己处理返回值是否真发生了翻转 - 如果要统计“首次设置”,得用
GETBIT返回值判断原值,再决定是否计数,这必须在 Lua 里完成
怎么用 BITCOUNT 统计活跃用户却不拖慢线上服务?
BITCOUNT 看似简单,但对大位图(比如百亿用户 ID 映射成 12.5GB 的 bitmap)全量扫一遍,会阻塞 Redis 主线程、拉高延迟。生产环境必须避开全量扫描。
推荐分片 + 增量聚合:把用户 ID 对 4096 取模,写入 4096 个 key(如 active:202405:0001、active:202405:0002…),每个 key 控制在 1MB 以内;统计时用 BITCOUNT 并行查多个 key,再 sum。Go 里用 sync.WaitGroup + redis.Pipelined 能压测出 3–5 倍性能提升。
- 别用单个 key 存全年数据,key 名必须带时间维度(如天/小时)和分片号
- 避免在 Lua 脚本里循环调用
BITCOUNT,它不支持批量 key,只能靠客户端聚合 - 如果允许少量误差,可改用 HyperLogLog(
PFCOUNT),但无法支持“某用户是否活跃”这类精确查询
Go 里怎么把用户 ID 映射到位图偏移量而不越界?
Redis 位图最大支持 2^32 个 bit(约 512MB/key),对应 offset 0–4294967295。用户 ID 若是 int64 或字符串,必须做安全截断和范围检查,否则 SETBIT 报错 ERR bit offset is not an integer or is out of range。
常见做法是取 ID 的低 32 位:uint32(id & 0xFFFFFFFF),但要注意:若业务 ID 是自增 long,低 32 位可能重复(比如 ID=1 和 ID=4294967297 映射到同一 offset);更稳妥的是用一致性哈希或 Murmur3 打散后 mod 2^32。
- 永远校验 offset
,Go 里用if offset >= 1 - 别直接用
int64强转uint64再转uint32,负数 ID 会变成极大正数 - 如果用户 ID 是字符串(如 UUID),必须用哈希(
murmur3.Sum32)转成 uint32,别用sha256—— 太重,且高位浪费
如何让位图自动过期又不误删正在写的 key?
用 EXPIRE 给位图设 TTL 看起来合理,但有个坑:如果某 key 刚被创建、还没写入任何 bit,EXPIRE 后它可能被提前回收;更糟的是,如果写入是稀疏的(比如每天只写几千个用户),TTL 到期后整块位图消失,导致历史统计归零。
正确解法是“写时续期”:每次调用 SETBIT 后紧跟 EXPIREAT,把过期时间设为当前时间 + TTL 秒。Go 里可用 pipeline 合并这两个命令,避免多一次 round-trip。
- 别在初始化位图时只设一次
EXPIRE,它不会自动刷新 - 如果用 Lua 脚本做原子写入,务必在脚本末尾加
redis.call("EXPIREAT", KEYS[1], ARGV[2]),ARGV[2] 传入计算好的时间戳 - 注意系统时钟漂移:Redis 用自身时间,Go 用本地时间,
EXPIREAT参数建议用time.Now().Add(ttl).Unix()而非本地秒数硬编码
位图不是万能的——它省空间、快查询,但扩容难、调试黑盒、错误不可逆(SETBIT 写错没法 UNSETBIT)。上线前务必用真实 ID 分布压测 offset 碰撞率,以及模拟网络分区验证 Lua 脚本的幂等性。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











