redis本身不会因并发写丢失数据,但客户端使用get+set非原子操作会导致覆盖式写入;应优先使用incr、setnx等原子命令,必要时用redisson分布式锁并控制粒度。

直接结论:Redis本身不会因并发写丢失数据,但客户端逻辑不当会导致覆盖式写入——问题不在Redis,而在你用GET + SET这种非原子组合操作。
为什么redisTemplate.opsForValue().get() + set()会丢数据
这是最典型的“读-改-写”竞态:两个线程同时执行get("counter")拿到值100,各自+1后都set("counter", 101),最终结果是101而非102。
- Redis单线程执行命令,每个命令本身是原子的,但
get和set是两次独立请求,中间可能被其他客户端插入 - Spring Boot应用多线程调用时,这个间隙就是数据丢失的窗口
- 哪怕只用一个Redis实例,只要客户端发来两个分离的命令,就存在竞争
优先用Redis原生命令替代客户端计算
能用原子命令就别在Java里算——这是成本最低、效果最稳的解法。
Redis 8.2.3 是一款安全优先的高性能键值存储系统。该版本紧急修复了可能引发远程代码执行(RCE)的高危漏洞(CVE-2025-62507),并解决了 HyperLogLog 及 Cuckoo Filter 等数据结构在特定场景下的崩溃问题。建议所有用户立即升级,以保障生产环境的系统稳定与数据安全。
-
INCR/DECR:计数类场景直接用redisTemplate.opsForValue().increment("counter", 1) -
SETNX:需要首次写入控制时,用stringRedisTemplate.opsForValue().setIfAbsent("key", "value") -
GETSET:需获取旧值并设新值,用stringRedisTemplate.opsForValue().getAndSet("key", "new") - 避免把
get结果拿回Java做判断再set,除非业务逻辑必须在应用层执行(比如复杂校验)
必须用应用层逻辑时,锁要跨JVM生效
synchronized在单机有效,集群下完全失效——这点很多人踩坑后才意识到。
- 本地锁(
synchronized或ReentrantLock)只对当前JVM进程起作用,多实例部署时形同虚设 - 推荐用
Redisson的RLock:它基于Redis实现,支持自动续期(看门狗)、可重入、公平性配置 - 锁粒度要细:按业务ID加锁,比如
"stock_lock_" + skuId,而不是全局锁"stock_lock" - 务必在
try块内操作,在finally中unlock(),否则锁可能永远不释放
序列化混用导致的“假丢失”容易被忽略
不是数据真丢了,而是读出来类型不对,抛ClassCastException被静默吞掉或日志没打全,误以为写失败。
- 同一个key,有时用
StringRedisTemplate写字符串,有时用RedisTemplate<string object></string>写对象,反序列化时必然出错 - 解决方案:统一序列化器(如全用
Jackson2JsonRedisSerializer),或严格区分key命名空间,比如"str:user:123"vs"obj:user:123" - 检查
redis-cli里实际存的值:GET key看是否为JSON字符串,TYPE key确认数据结构
真正麻烦的不是锁怎么写,而是得想清楚:这个操作到底需不需要锁?能不能拆成原子命令?有没有更轻量的方案(比如先用INCR,失败再走锁逻辑)?边界条件比代码更难测。










