setbit/getbit单命令原子但无法支持先读再算再写,高并发下易出错;lua脚本将完整逻辑下沉至服务端执行,实现真正原子的位运算闭环。

为什么直接用 SETBIT / GETBIT 不够用?
Redis 的 SETBIT 和 GETBIT 本身是原子的,但一旦涉及“先读再算再写”逻辑(比如统计某用户本周活跃天数、设置多个位并返回变化量),单命令就无能为力。网络往返 + 客户端逻辑会破坏原子性,尤其在高并发下可能漏计或重复计。Lua 脚本在 Redis 服务端执行,整个脚本就是一次原子操作——这才是真正可控的位运算闭环。
用 EVAL 执行位统计 Lua 脚本的正确姿势
常见错误是把位运算逻辑写在客户端,只用 Lua 做简单封装。正确做法是把完整判断和更新逻辑下沉到脚本里。例如统计 key 中第 0–6 位(代表周一到周日)有多少个 1:
eval "local c = 0; for i = 0, 6 do c = c + tonumber(redis.call('GETBIT', KEYS[1], i)) end return c" 1 user:active:123
-
KEYS[1]必须传入,不能硬编码 key 名,否则无法被 Redis 集群识别 - 用
tonumber()转换GETBIT返回值,它实际返回的是字符串"0"或"1" - 循环范围要明确,
GETBIT对超出当前字符串长度的 offset 返回 0,但无谓遍历浪费 CPU - 集群模式下,所有 key 必须落在同一个 slot,否则报错
CROSSSLOT Keys in request don't hash to the same slot
批量 SETBIT 并返回旧值:避免多次 round-trip
如果需要一次性设置多个位(如记录用户当天多个行为事件),又想拿到每个位操作前的原始值,靠客户端循环调用 GETBIT + SETBIT 会放大延迟。Lua 可以一步到位:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
eval "local r = {}; for i,v in ipairs(ARGV) do table.insert(r, redis.call('GETBIT', KEYS[1], tonumber(v))) redis.call('SETBIT', KEYS[1], tonumber(v), 1) end return r" 1 user:flags:456 0 3 7
-
ARGV传入要设为 1 的 bit 位置列表,脚本逐个读旧值、再设新值 - 注意
tonumber(v):ARGV 元素默认是字符串,不转会导致SETBIT报错ERR bit is not an integer or out of range - 返回的是旧值数组(0/1 字符串),客户端需自行转数字处理
- 该脚本不可用于超大位图(如百万级 offset),Redis 单次 Lua 执行有 5 秒超时限制,且内存拷贝开销明显
位运算结果缓存与过期陷阱
Lua 脚本里不能直接给计算结果设过期时间——EXPIRE 对非 key 类型无效,而位图本质是 string 类型的 value。常见误操作是:
-- ❌ 错误:EXPIRE 作用于 GETBIT 结果(数字),无效
eval "redis.call('EXPIRE', redis.call('GETBIT', KEYS[1], 0), 3600)" 1 mybitmap
- 真正要过期的是整个 key,不是某个 bit。如果业务需要“某段位区间定期清零”,得用
SETRANGE写全 0 字节,或用DEL+ 重建 - 若统计结果需长期复用(如日活总数),建议把 Lua 计算结果用
SET存到另一个带EX的 key,而非反复执行重算 - 注意
BITCOUNT本身很快,但对 1MB 位图仍需约 0.2ms;高频调用时,宁可多存一个缓存 key,也别在 Lua 里做复杂位扫描
位图的原子性优势只在“读-改-写”链路里成立,一旦牵扯到跨 key 协调、定时清理或大范围扫描,就得跳出 Lua,靠外部调度或分片设计兜底。










