不能直接用setbit+bitcount完事,因“判断是否首次活跃”需读-判-写三步原子化,而getbit返回0时并发setbit会导致重复计数;必须用lua脚本将逻辑封装为原子操作,如eval中先getbit再setbit并返回新老标识与统计值。

为什么不能直接用 SETBIT + BITCOUNT 就完事?
因为并发写入时,多个客户端同时对同一个 bitmap 位做 SETBIT key offset 1,虽然操作本身是原子的,但「判断用户是否首次活跃」这个逻辑无法原子化。比如两个请求同时发现 GETBIT key offset 返回 0,都会执行 SETBIT,导致重复计数或业务误判。必须把「读-判-写」三步收进一个 Lua 脚本里。
EVAL 脚本里怎么安全设置位并返回是否为新用户?
核心是用 redis.call("GETBIT", ...) 先查,再用 redis.call("SETBIT", ...) 写,全程在服务端原子执行。注意:Lua 中 bitmap 的 offset 是 number 类型,不能传字符串;key 名建议带日期前缀(如 "active:20240615")方便归档。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 脚本返回值建议用
{is_new = 1, old_count = N}这类 table,但 Redis 只支持返回 number/string/boolean/table → 实际要用return {new_flag, old_count},客户端按顺序取 - 避免在脚本里调用耗时命令(如
KEYS),否则阻塞整个 Redis - 如果 key 不存在,
GETBIT默认返回 0,不用提前SET
eval "local b = redis.call('GETBIT', KEYS[1], ARGV[1]); if b == 0 then redis.call('SETBIT', KEYS[1], ARGV[1], 1) end; local c = redis.call('BITCOUNT', KEYS[1]); return {b == 0 and 1 or 0, c}" 1 active:20240615 12345
统计日活时,BITCOUNT 的性能和精度要注意什么?
BITCOUNT 是 O(N) 复杂度,但 Redis 对 bitmap 做了优化(如稀疏 bitmap 使用 run-length encoding),实际在百万级用户、单 key 占几 MB 的情况下仍很快(通常
- 不要对跨天的 key 做
BITOP AND/OR后再BITCOUNT——这会生成新 key,内存翻倍且不可控 - 如果需小时级统计,别用单个 key 存全天,改用
active:20240615:hour12分片,用BITOP OR合并时显式指定目标 key 避免覆盖 - Bitmap 最大支持 2^32 位(约 42.9 亿),用户 ID 超过此范围需哈希映射到合法 offset(如
user_id % 4294967296),但会引入哈希冲突,慎用
如何避免 Lua 脚本被频繁重载影响性能?
每次 EVAL 都要解析脚本,高并发下开销明显。应该用 EVALSHA + SCRIPT LOAD 缓存脚本 SHA1。但注意:SCRIPT FLUSH 会清空所有缓存,运维操作可能意外触发;生产环境建议用 redis.register_script(Python redis-py)或 ScriptingCommands.evalSha(Java Lettuce)自动管理。
- 脚本内容变更后,SHA1 改变,旧
EVALSHA会报错(error) NOSCRIPT No matching script. Please use EVAL.,需捕获该错误并 fallback 到EVAL - Redis Cluster 下,key 必须落在同一 slot,所以
KEYS[1]要确保 hash tag,例如用{active}:20240615让所有日期 key 落同一 slot










