根本原因是业务层绕过原子操作,采用“读-改-写”模式:多个线程并发GET同一key得到相同旧值,各自+1后SET,导致只累加1次而非N次;INCR虽原子但无法解决条件判断、跨key复合操作及多实例下的主从延迟、slot分散、重复请求等问题。

Redis String计数器在高并发下为什么会丢数
根本原因不是 INCR 本身不原子,而是业务层绕过原子操作、用“读-改-写”模式更新同一 key 时,多个线程/进程同时读到旧值,各自加 1 后写回,最终只 +1 而非 +N。
比如两个请求并发执行:GET counter → 都拿到 100 → 各自算出 101 → SET counter 101,结果仍是 101,而非预期的 102。
这种场景常见于手写累加逻辑、用 GET + SET 替代 INCR、或在 Lua 脚本外做条件判断再更新。
INCR 真的能解决所有计数问题吗
INCR 和 INCRBY 是原子的,但仅限于“无条件递增”。一旦涉及条件判断(如“只在小于 1000 时才加 1”)、复合操作(如“加 1 后同步更新另一个 key”)、或需要返回旧值做后续处理,单靠 INCR 就不够了。
此时必须用 Lua 脚本封装整个逻辑,否则网络往返间仍可能被其他客户端插入操作。例如:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
eval "if tonumber(redis.call('get', KEYS[1])) < 1000 then return redis.call('incr', KEYS[1]) else return 0 end" 1 counter
注意:Lua 脚本内不能调用非原子命令组合(如 GET + SET 分开执行),否则原子性失效。
为什么多实例部署时 INCR 也容易出错
当应用部署多个实例,且每个实例都独立执行计数逻辑(比如生成订单号、发号器),即使都用 INCR,也会因 Redis 单点成为瓶颈或故障单点,导致:
- 主从延迟期间,从节点读到旧值,误判计数状态
- 集群模式下,key hash 到不同 slot,
INCR无法跨 slot 原子操作(如需对多个 key 做联合计数) - 客户端重试机制未关闭,网络超时后重复发送
INCR请求(Redis 不做幂等校验)
典型表现是计数值比实际请求数少(丢数)或多(重复计数),尤其在客户端异常重启、连接池复用、或代理层(如 Twemproxy)未透传错误时更隐蔽。
String 计数器 vs 其他方案的取舍陷阱
用 INCR 最简单,但不适合以下场景:
- 需要去重计数(比如 UV)→ 应选
PFADD+PFCOUNT - 需要按维度分组统计(如每小时 PV)→
HINCRBY更合适,避免 key 泛滥 - 需要严格精确且高可用 → 单靠 Redis 不够,得结合数据库序列 + 缓存预热,或用 Redis RedLock 做分布式锁兜底
最容易被忽略的一点:INCR 返回的是 long 类型整数,超过 2^63−1 会溢出变负数,而很多 SDK 不做校验,直接转成 int 或 string 后截断,导致计数突降为负或乱码。

















