Redis缓存一致性问题:如何应对及有无现成解决方案?
Redis缓存最终一致性:多实例场景下的问题与解决方案
问题场景
Redis在Java生态里应用广泛,Spring框架的@Cacheable注解或者cache.get(..)、cache.evict()这类API,让缓存实现变得非常简单。但在多实例水平扩容的场景下,缓存最终一致性很容易踩坑:
- 实例A加载某条数据耗时久,实例B先加载完把新值写入缓存,结果实例A加载完成后把旧值写进去,直接覆盖了新值;
- 实例A正在往缓存写值的时候,实例B把这条缓存清了,实例A还是把旧值写进去,导致缓存里留着过期数据。
常用解决思路
1. 分布式锁串行化缓存写入
用Redis分布式锁(比如Redisson的RLock,或者基于SETNX自己实现的锁),确保同一个缓存Key的加载和写入只能有一个实例在做:
- 加载数据前先抢锁,抢到锁的实例才去查库、写缓存;
- 没抢到锁的实例,可以等锁释放后读缓存,或者直接查库返回(看业务能不能接受);
- 一定要给锁设过期时间,防止实例挂了锁一直占着。
2. 版本号校验避免旧值覆盖
给每个缓存Key加个版本号:
- 写缓存的时候,把数据和当前版本号一起存进去;
- 清理缓存的时候,先把版本号更新(或者标记成过期版本);
- 实例加载完准备写缓存时,先对比自己加载时的版本号和缓存里的版本号,不一样就别写了。
3. 延迟双删解决写入-清理冲突
数据更新后,分两次删缓存:
- 第一次删完,等一小段时间(比业务加载数据的时间稍长),再删第二次;
- 这样能避免第一次删完后,刚好有实例在加载旧数据并写入,第二次删除可以把这个残留的旧值清掉。
4. 扩展Spring Cache实现
Spring Cache本身没直接处理这个问题,但可以自定义CacheManager或者Cache实现类,在缓存写入、清理的时候加上锁或者版本号校验逻辑,不用改原来@Cacheable这些注解的用法,对业务代码透明。
现成工具
- Redisson有带锁的缓存实现,直接集成到Spring项目里就能用,不用自己写锁逻辑;
- 有些二级缓存方案(比如Caffeine+Redis)也内置了多实例一致性处理,配置的时候注意开启相关功能就行。
为什么讨论度低?
主要是很多业务对缓存一致性的要求没那么高,或者开发者还没遇到大规模扩容后的并发问题。如果业务要求严格的最终一致性,分布式锁+版本号的组合方案比较靠谱,既能防写入冲突,也能避免清理后旧值残留。
内容的提问来源于stack exchange,提问作者piotrd
相关产品推荐
相关产品推荐

