缓存一致性实践:删除缓存不是银弹

发布时间:2026/7/3 2:15:14
缓存一致性实践:删除缓存不是银弹 缓存一致性实践删除缓存不是银弹一、缓存问题的本质是读写时序缓存能显著降低数据库压力但一致性问题也会随之出现。最常见的方案是“更新数据库后删除缓存”它比直接更新缓存更稳妥但并不是银弹。并发读写、删除失败、主从延迟、事务未提交、热点 key 重建都可能导致脏数据或缓存击穿。讨论缓存一致性时应先明确业务容忍度。账户余额、库存、订单状态对一致性要求高商品详情、文章统计、推荐列表可以接受短时间延迟。不同场景应该采用不同策略而不是把所有缓存都套进同一个模板。二、典型链路数据库和缓存不是一个事务sequenceDiagram participant U as 用户请求 participant S as Java 服务 participant C as Redis participant D as Database U-S: 更新数据 S-D: 提交事务 S-C: 删除缓存 U-S: 查询数据 S-C: 未命中 S-D: 读取新值 S-C: 回填缓存这个顺序看起来合理但有几个细节需要注意。删除缓存必须发生在数据库事务提交之后否则另一个线程可能在事务未提交时读到旧数据并回填缓存。删除失败要有重试或补偿否则旧缓存会持续存在。高并发热点 key 回填时要避免大量请求同时打到数据库。还要考虑主从延迟。如果写入主库后读请求从从库读取再回填缓存就可能把旧值放回 Redis。此时需要对关键读请求短时间走主库或者使用版本号校验避免旧数据覆盖新缓存。三、代码实现用事务后事件触发缓存删除下面示例展示一种较稳妥的做法业务事务提交后再发布缓存删除动作。Transactional public void updateProduct(ProductUpdateCommand command) { Product product productRepository.findById(command.id()) .orElseThrow(() - new IllegalArgumentException(product not found)); product.changePrice(command.price()); productRepository.save(product); eventPublisher.publishEvent(new ProductChangedEvent(command.id())); } TransactionalEventListener(phase TransactionPhase.AFTER_COMMIT) public void onProductChanged(ProductChangedEvent event) { cacheService.delete(product: event.productId()); }如果删除缓存失败不要只打印日志。可以把失败事件写入可靠消息队列或本地补偿表由后台任务重试。对于高价值数据还可以加入版本号数据库记录带version缓存值也带version回填前比较版本避免旧查询覆盖新值。热点 key 要配合互斥重建或逻辑过期。缓存失效瞬间如果大量请求同时回源数据库会形成击穿。可以用 Redis 分布式锁控制单线程回填其余请求短暂等待或返回旧值。逻辑过期适合读多写少场景用旧值换稳定性。四、工程取舍一致性、性能和复杂度要分层强一致缓存通常成本很高甚至违背缓存的初衷。大多数业务需要的是“可解释的最终一致”。例如商品价格更新后 1 秒内刷新可以接受库存扣减则不能依赖缓存作为最终依据。把数据按一致性等级分层才能避免过度设计。监控也很重要。缓存命中率、删除失败次数、回源耗时、热点 key、数据库 QPS 和脏读投诉都应该被观察。没有监控时缓存一致性问题往往只在用户反馈或对账中暴露定位成本很高。最后要给缓存 key 制定规范。包含业务前缀、对象 ID、版本或租户信息避免不同模块误删、覆盖或复用同一个 key。缓存不是散落在代码里的小优化而是系统架构的一部分。缓存预热和缓存雪崩也需要提前规划。预热应在服务启动或扩容时主动加载高频数据避免冷启动瞬间大量请求击穿到数据库。雪崩则来自批量 key 同时失效可以通过随机过期时间、热点 key 永不过期配合后台刷新、或熔断机制来缓解。对于核心链路建议准备本地缓存作兜底在 Redis 不可用或大量 key 失效时提供降级能力。五、总结缓存一致性要围绕读写时序设计更新数据库后删除缓存只是基础方案。事务后删除、失败补偿、版本校验、热点重建和一致性分层才是生产环境更完整的答案。把业务容忍度说清楚缓存策略才不会走偏。