结论:没有绝对一致的方案,只有根据业务容忍度选择最合适的策略;缓存与数据库一致性本质是权衡问题而非技术实现问题;先删缓存再更新db会因并发导致旧数据回填,产生不可自愈的脏数据;先更新db再删缓存将不一致压缩至极短可自愈窗口,更稳妥;spring中需确保删除缓存在事务提交后执行;集群下@cacheevict失效因key分散于不同slot,应强制哈希或手动delete;本地+redis双写需通过redis pub/sub通知各节点清除caffeine;最终决策关键在于业务对不一致的容忍时长与降级策略。

直接说结论:没有绝对一致的方案,只有根据业务容忍度选择最合适的策略。缓存与数据库一致性本质是“权衡问题”,不是“技术实现问题”。
为什么先删缓存再更新DB会出错?
这是新手最容易踩的坑。现象是:线程A删除缓存 → 线程B查缓存未命中 → B从DB读旧值写入缓存 → A更新DB完成 → 缓存里还是旧数据。
- 根本原因是缓存删除和DB更新之间存在时间窗口,且无锁保护
- 哪怕加了
RedisTemplate.delete()和JdbcTemplate.update(),也无法保证原子性 - 在高并发或网络抖动时,这个窗口会被放大,出错概率显著上升
先更新DB再删缓存为什么更稳妥?
它把不一致的时间窗口压缩到“DB已更新但缓存尚未删除”的极短区间内,且后续读请求会自动回填新值,属于“可自愈”的不一致。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 关键点在于:删除缓存必须是
afterCommit级别操作,否则事务回滚后缓存被误删 - Spring中推荐用
@Transactional+TransactionSynchronizationManager.registerSynchronization()注册事务提交后的回调 - 避免直接在service方法末尾调用
redisTemplate.delete(),那可能在事务未提交时就执行了
集群环境下@CacheEvict为什么失效?
Spring Cache的@CacheEvict默认只发一条DEL命令,但在Redis Cluster中,如果key分布在不同slot,该命令可能只清掉部分节点上的缓存。
- 现象是:一个key在节点1上被删了,但相同逻辑生成的另一个key还在节点2上残留
- 解决方案不是禁用
@CacheEvict,而是确保被驱逐的key都落在同一slot——通过{xxx}语法强制哈希,比如user:{id} - 更彻底的做法是绕过Spring Cache抽象,改用
RedisTemplate手动调用delete(),并确认连接的是正确节点(需配合Lettuce的ClusterClientOptions)
本地缓存(Caffeine)+ Redis双写怎么同步?
L1本地缓存无法广播,所以“更新DB → 更新Redis → 清空本机Caffeine”这套流程里,其他节点的Caffeine不会自动失效。
- 不能依赖
CacheManager的全局通知机制,Caffeine本身不支持分布式事件 - 可行做法是:更新DB后,向Redis发布一个
cache:invalidate:user:123消息,所有节点订阅该channel,收到后主动清除本地CaffeineCache.asMap().remove("user:123") - 注意
publish/subscribe不是100%可靠,要加重试和幂等判断,比如用StringRedisTemplate.convertAndSend()配合唯一消息ID
真正难的不是代码怎么写,而是判断哪条数据能接受几秒不一致、哪些字段必须强一致、以及当缓存失败时是否允许降级读DB——这些决策比redisTemplate.opsForValue().set()重要得多。










