保证缓存和数据库的一致性是一个经典难题主要因为两者独立更新且无法实现原子操作。在高并发场景下如果不加控制可能出现缓存与数据库数据不一致的问题。用Java 代码完整说明缓存与数据库一致性方案。所有示例代码均采用 JavaSpring Boot RedisTemplate 风格并保留了方案对比与避坑指南。一、最常用方案旁路缓存Cache Aside Pattern核心规则读先查缓存命中返回未命中查数据库再写入缓存。写先更新数据库然后删除缓存而不是更新缓存。为什么删除而不是更新避免并发写导致缓存脏数据更新缓存可能因并发写导致缓存与数据库值不一致且多次更新会浪费性能。删除操作幂等且简单。删除缓存则让下次读时重新加载简单可靠。Java 代码示例基于 Spring Boot RedisTemplateServicepublicclassUserService{AutowiredprivateRedisTemplateString,ObjectredisTemplate;AutowiredprivateUserRepositoryuserRepository;privatestaticfinalStringUSER_CACHE_PREFIXuser:;// 读操作旁路缓存publicUsergetUser(Longid){StringcacheKeyUSER_CACHE_PREFIXid;// 1. 查缓存Useruser(User)redisTemplate.opsForValue().get(cacheKey);if(user!null){returnuser;}// 2. 缓存未命中查数据库useruserRepository.findById(id).orElse(null);if(user!null){// 3. 写入缓存可设置合理过期时间redisTemplate.opsForValue().set(cacheKey,user,30,TimeUnit.MINUTES);}returnuser;}// 写操作先更新数据库再删除缓存TransactionalpublicvoidupdateUser(Useruser){// 1. 更新数据库userRepository.save(user);// 2. 删除缓存StringcacheKeyUSER_CACHE_PREFIXuser.getId();redisTemplate.delete(cacheKey);}}该方案的并发风险与“缓存双删”极低概率下会出现读线程读到旧数据后写线程删除了缓存但读线程又把旧数据写回缓存。解决方案延迟双删– 更新数据库后先删缓存等待一小段时间大于一次并发读写的耗时再次删除。TransactionalpublicvoidupdateUserWithDoubleDelete(Useruser){StringcacheKeyUSER_CACHE_PREFIXuser.getId();// 1. 第一次删除缓存redisTemplate.delete(cacheKey);// 2. 更新数据库userRepository.save(user);// 3. 延迟一段时间例如 500ms再次删除缓存newThread(()-{try{Thread.sleep(500);// 实际应使用更优雅的调度线程池redisTemplate.delete(cacheKey);}catch(InterruptedExceptione){Thread.currentThread().interrupt();}}).start();}生产建议不要直接用new Thread可使用ScheduledExecutorService或消息队列延时消息。二、解耦方案订阅数据库变更日志Canal MQ流程业务代码只操作数据库 → Canal 监听 binlog → 发 MQ → 消费者删除缓存。优点完全解耦即使缓存删除失败MQ 重试机制保证最终一致。Java 消费者示例Spring Boot RocketMQComponentRocketMQMessageListener(topicuser-update-topic,consumerGroupcache-consume-group)publicclassCacheDeleteConsumerimplementsRocketMQListenerString{AutowiredprivateRedisTemplateString,ObjectredisTemplate;OverridepublicvoidonMessage(StringuserIdStr){LonguserIdLong.valueOf(userIdStr);StringcacheKeyuser:userId;redisTemplate.delete(cacheKey);log.info(删除缓存成功key: {},cacheKey);}}对应业务代码只需更新数据库无需触碰缓存TransactionalpublicvoidupdateUser(Useruser){userRepository.save(user);// 可选发送一条MQ消息作为保险Canal 负责主要删除这里可省略// mqProducer.send(user-update-topic, String.valueOf(user.getId()));}Canal 配置方法MySQL binlog 开启 ROW 模式三、兜底方案设置缓存过期时间TTL无论用哪种方案永远给缓存设置一个合理的 TTL如 30 分钟。即使所有删除机制都失败数据最终也会自动从数据库重新加载。// 写入缓存时统一设置过期时间redisTemplate.opsForValue().set(cacheKey,user,30,TimeUnit.MINUTES);四、强一致性方案分布式锁读写串行化适用场景极少数对一致性要求极高、且并发量不高的场景如金融库存。代价吞吐量大幅下降。Java 示例Redisson 分布式锁AutowiredprivateRedissonClientredissonClient;publicUserupdateUserWithLock(Useruser){StringlockKeylock:user:user.getId();RLocklockredissonClient.getLock(lockKey);try{lock.lock(3,TimeUnit.SECONDS);// 1. 更新数据库userRepository.save(user);// 2. 删除缓存redisTemplate.delete(user:user.getId());}finally{if(lock.isHeldByCurrentThread()){lock.unlock();}}returnuser;}publicUsergetUserWithLock(Longid){StringlockKeylock:user:id;RLocklockredissonClient.getLock(lockKey);try{lock.lock(1,TimeUnit.SECONDS);// 1. 查缓存Useruser(User)redisTemplate.opsForValue().get(user:id);if(user!null)returnuser;// 2. 查数据库useruserRepository.findById(id).orElse(null);if(user!null){redisTemplate.opsForValue().set(user:id,user,30,TimeUnit.MINUTES);}returnuser;}finally{if(lock.isHeldByCurrentThread()){lock.unlock();}}}五、不同场景的方案选择决策表业务场景推荐方案可容忍不一致时间商品信息、用户资料读多写少旁路缓存 TTL几分钟库存、账户余额写较多延迟双删 / CanalMQ秒级排行榜、计数器允许误差只写缓存异步回写 DB分钟~小时支付、订单状态强要求直接读数据库不用缓存0 容忍六、避坑指南两个致命错误❌ 错误 1先删缓存再更新数据库// 错误示范redisTemplate.delete(cacheKey);userRepository.save(user);// 此时若另一个线程读并写回旧数据缓存永久脏后果高并发下缓存长时间为旧值直到 TTL 过期。❌ 错误 2写请求中直接更新缓存而非删除// 错误示范userRepository.save(user);redisTemplate.opsForValue().set(cacheKey,user);// 可能导致并发顺序错乱后果两个写请求乱序缓存与数据库值相反。七、最终推荐架构Java 生产实践// 1. 写操作延迟双删 事务注解TransactionalpublicvoidupdateProduct(Productproduct){StringcacheKeyproduct:product.getId();// 第一次删除redisTemplate.delete(cacheKey);// 更新数据库productRepository.save(product);// 延迟删除使用线程池scheduledExecutor.schedule(()-redisTemplate.delete(cacheKey),500,TimeUnit.MILLISECONDS);}// 2. 读操作旁路缓存 TTLpublicProductgetProduct(Longid){StringcacheKeyproduct:id;Productp(Product)redisTemplate.opsForValue().get(cacheKey);if(p!null)returnp;pproductRepository.findById(id).orElse(null);if(p!null){redisTemplate.opsForValue().set(cacheKey,p,10,TimeUnit.MINUTES);}returnp;}// 3. 全局兜底在配置类中为所有 Redis 缓存设置默认 TTLBeanpublicRedisCacheManagerBuilderCustomizercustomizer(){returnbuilder-builder.withDefaultCacheConfiguration(RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(30)));}总结核心Cache Aside先 DB 后删缓存。加强延迟双删 或 CanalMQ。底线永远设置 TTL。原则不追求绝对强一致性否则应放弃缓存。