很多缓存一致性问题最开始看起来都很简单。例如商品服务需要修改一个商品价格开发者通常会写出类似逻辑TransactionalpublicvoidupdateProductPrice(LongproductId,BigDecimalnewPrice){productRepository.updatePrice(productId,newPrice);redisTemplate.delete(product:price:productId);}这段代码很常见。数据库更新后删除缓存下一次读取时缓存未命中再从数据库加载新价格似乎没有问题。当你把“缓存更新不一致怎么办”交给 AI 时它也经常会给出这类答案更新数据库 ↓ 删除缓存 ↓ 下次查询重新写入缓存这套思路并不完全错。问题在于它只描述了理想顺序没有处理真实系统里的事务、并发读取、缓存重建和异常恢复。尤其是下面这个场景事务尚未提交缓存已经被删除另一个请求读取商品价格它发现缓存为空于是去数据库查询数据库事务还没有提交它读到旧价格旧价格重新写进缓存原事务提交成功缓存里却留下了旧值。这就是一种特别隐蔽的长期脏数据。数据库已经是新价格缓存却保留旧价格服务没有报错接口也能正常返回只是部分用户持续读到旧数据。一、最常见的错误把“更新库后删缓存”理解成原子操作很多人看到下面这段代码时会认为它天然有顺序TransactionalpublicvoidupdateProductPrice(LongproductId,BigDecimalnewPrice){productRepository.updatePrice(productId,newPrice);redisTemplate.delete(product:price:productId);}从代码执行顺序看确实先执行数据库更新再执行缓存删除。但从数据库事务的角度看updatePrice()只是把修改放进当前事务。在事务真正提交前其他请求是否能读到新数据取决于隔离级别、查询路径和当前连接状态。于是会出现这样的时序时间点写请求读请求T1更新数据库事务未提交T2删除缓存T3读取缓存未命中T4查询数据库读到旧价格T5把旧价格写回缓存T6数据库事务提交T7后续请求继续读缓存旧值问题不在于 Redis 出错也不在于 SQL 没执行。问题是缓存失效发生得太早。缓存删除应该和“事务已经确认提交”绑定而不是只跟随代码行的执行顺序。二、为什么 AI 很容易给出这个不完整方案AI 在处理这类问题时通常会根据大量常见代码模式给出一个“更新库 删缓存”的基础方案。这个方案适合回答修改数据后怎样让缓存尽快失效但它没有自动回答当前数据库事务什么时候提交缓存删除失败后如何恢复高并发下是否有人会在提交前重建旧缓存多个服务是否都能写同一份缓存重试任务是否会误删更新后的新缓存关键价格、库存、权益字段是否允许短暂读旧值。也就是说AI 可以迅速生成局部正确的实现但局部正确不等于整体一致。近期关于 AI 辅助开发的工程讨论也越来越强调把团队规则、验证方式和评审标准沉淀成可复用资产而不是只依赖某次提示词或某个开发者的即时判断。对缓存一致性来说真正需要沉淀的不是一句“更新后删缓存”而是完整规则数据库提交成功 ↓ 缓存失效 ↓ 读取方允许重建缓存 ↓ 缓存写入必须校验版本或时间 ↓ 删除失败必须进入补偿链路三、正确的第一步事务提交后再删除缓存在 Spring 场景下可以让缓存失效逻辑放到事务提交成功之后。例如publicrecordProductPriceChangedEvent(LongproductId,BigDecimalprice){}业务层只负责修改数据库和发布领域事件TransactionalpublicvoidupdateProductPrice(LongproductId,BigDecimalnewPrice){productRepository.updatePrice(productId,newPrice);applicationEventPublisher.publishEvent(newProductPriceChangedEvent(productId,newPrice));}缓存删除放在事务提交后ComponentpublicclassProductCacheInvalidationListener{TransactionalEventListener(phaseTransactionPhase.AFTER_COMMIT)publicvoidonPriceChanged(ProductPriceChangedEventevent){redisTemplate.delete(product:price:event.productId());}}这样做至少解决了一件事如果数据库事务最终回滚就不会提前删除缓存。同时读请求在数据库提交前仍然可以继续读取旧缓存不会因为缓存提前删除而把旧数据库值重新写回缓存。但要注意这不是所有问题的终点。四、事务提交后删缓存也可能失败假设数据库事务已经提交成功但 Redis 恰好出现网络异常TransactionalEventListener(phaseTransactionPhase.AFTER_COMMIT)publicvoidonPriceChanged(ProductPriceChangedEventevent){redisTemplate.delete(product:price:event.productId());}如果delete()失败数据库已经是新价格缓存仍然是旧价格。此时不能简单写一句try{redisTemplate.delete(key);}catch(Exceptione){log.error(delete cache failed,e);}因为日志并不会自动修复脏缓存。更可靠的做法是把“缓存失效失败”视为一个需要补偿的工程事件。例如记录待处理任务publicvoidinvalidateProductCache(LongproductId){Stringkeyproduct:price:productId;try{redisTemplate.delete(key);}catch(Exceptionex){cacheRetryRepository.save(CacheRetryTask.of(DELETE,key,PRODUCT_PRICE_CHANGED));throwex;}}然后由独立任务进行重试Scheduled(fixedDelay30000)publicvoidretryCacheInvalidation(){ListCacheRetryTasktaskscacheRetryRepository.findPendingTasks(100);for(CacheRetryTasktask:tasks){try{redisTemplate.delete(task.getCacheKey());cacheRetryRepository.markSuccess(task.getId());}catch(Exceptione){cacheRetryRepository.increaseRetryCount(task.getId());}}}这里要注意一个边界重试删除不能无限执行。如果旧重试任务在很久之后才执行它可能删除已经被新业务重新写入的缓存。因此重试任务需要带版本、事件时间或业务版本号而不是只保存一个 Redis Key。五、给缓存数据加版本避免旧事件干扰新数据例如缓存对象里增加业务版本publicrecordProductPriceCache(LongproductId,BigDecimalprice,Longversion,InstantcachedAt){}写入数据库时同步递增版本TransactionalpublicvoidupdateProductPrice(LongproductId,BigDecimalnewPrice){ProductproductproductRepository.findByIdForUpdate(productId);product.changePrice(newPrice);product.increaseVersion();productRepository.save(product);applicationEventPublisher.publishEvent(newProductPriceChangedEvent(productId,newPrice,product.getVersion()));}缓存补偿任务也记录版本publicrecordCacheRetryTask(Longid,StringcacheKey,LongeventVersion,Stringstatus){}这样补偿任务执行前可以判断待删除事件版本 当前数据库版本 ↓ 说明这是旧事件 ↓ 不能盲目执行删除或覆盖很多系统不需要一开始就引入复杂版本控制。但对于价格、库存、权益、审批状态、限流策略等关键数据至少要明确缓存是否允许短暂不一致旧事件能否影响新缓存补偿操作如何避免覆盖最新状态是否需要强制读取数据库确认。六、把 AI 从“给代码”变成“帮你列验证清单”让 AI 直接写缓存更新代码得到的通常是一段可运行实现。但更有价值的使用方式是让它先帮你列出缓存一致性风险。例如你是后端架构评审助手。 场景 商品价格修改后需要失效 Redis 缓存。 数据库使用事务缓存采用 Cache Aside 模式。 请不要直接只给“更新数据库后删除缓存”的代码。 请输出 1. 事务未提交时提前删缓存可能造成的并发时序 2. 缓存删除失败后的补偿方案 3. 重试任务可能误删新缓存的风险 4. 哪些字段适合允许短暂不一致 5. 最少 6 个并发与异常测试场景 6. 需要人工确认的业务边界。这样得到的输出通常更适合作为代码评审前的检查清单。对于已经把 ChatGPT Plus、GPT Plus 用在代码解释、设计讨论、测试补全和排障整理中的开发者来说长期使用的价值不在于每次都让工具直接写完代码而在于是否能把问题建模、约束条件和验证步骤逐步沉淀到自己的工作流里。对已经确认有 AI 工具长期使用需求的开发者来说工具准备不只是模型能力还包括使用周期、说明理解、边界意识和异常处理路径相关信息可按实际需要参考gpt985com七、这类缓存改造至少要测什么缓存一致性问题最怕只测“正常更新后能不能读到新值”。更应该覆盖这些场景测试场景预期结果数据库更新成功、缓存删除成功下次读取从数据库加载新值数据库更新回滚原缓存不应被删除缓存删除失败产生可重试补偿任务删除缓存后并发读取不应长期写入旧值补偿任务延迟执行不应误删新版本缓存两次快速更新同一商品最终缓存必须对应最新版本Redis 短暂不可用系统具备降级、告警和恢复路径例如可以测试“事务回滚时缓存不失效”TestvoidshouldNotEvictCacheWhenDatabaseTransactionRollsBack(){Stringkeyproduct:price:10001;redisTemplate.opsForValue().set(key,99.00);assertThrows(BusinessException.class,()-productApplicationService.updatePriceWithFailure(10001L,newBigDecimal(109.00)));assertEquals(99.00,redisTemplate.opsForValue().get(key));}再测试“事务提交后才触发缓存失效”TestvoidshouldEvictCacheOnlyAfterTransactionCommit(){Stringkeyproduct:price:10001;redisTemplate.opsForValue().set(key,99.00);productApplicationService.updatePrice(10001L,newBigDecimal(109.00));assertNull(redisTemplate.opsForValue().get(key));}这些测试的重点不是验证某个方法是否被调用而是验证在异常、并发和重试条件下最终读到的数据是否仍然符合业务预期。八、结语“更新数据库后删除缓存”不是错误方案。它只是一个还没有写完的方案。真正可靠的缓存一致性设计至少要回答删除缓存发生在事务提交前还是提交后删除失败后如何补偿延迟重试会不会影响新数据高并发读请求是否可能重建旧缓存哪些数据允许短暂不一致线上如何发现缓存和数据库已经出现偏差。AI 可以快速给出缓存更新代码也可以帮你补测试和整理时序。但能不能把缓存逻辑真正放进长期稳定的开发工作流取决于你是否把事务、版本、补偿和验证这些边界一起设计进去。缓存问题最怕的不是一次删除失败。而是系统已经读错数据却没有任何人知道它从什么时候开始错了。
AI 建议“更新数据库后删除缓存”,为什么仍可能造成长期脏数据
很多缓存一致性问题最开始看起来都很简单。例如商品服务需要修改一个商品价格开发者通常会写出类似逻辑TransactionalpublicvoidupdateProductPrice(LongproductId,BigDecimalnewPrice){productRepository.updatePrice(productId,newPrice);redisTemplate.delete(product:price:productId);}这段代码很常见。数据库更新后删除缓存下一次读取时缓存未命中再从数据库加载新价格似乎没有问题。当你把“缓存更新不一致怎么办”交给 AI 时它也经常会给出这类答案更新数据库 ↓ 删除缓存 ↓ 下次查询重新写入缓存这套思路并不完全错。问题在于它只描述了理想顺序没有处理真实系统里的事务、并发读取、缓存重建和异常恢复。尤其是下面这个场景事务尚未提交缓存已经被删除另一个请求读取商品价格它发现缓存为空于是去数据库查询数据库事务还没有提交它读到旧价格旧价格重新写进缓存原事务提交成功缓存里却留下了旧值。这就是一种特别隐蔽的长期脏数据。数据库已经是新价格缓存却保留旧价格服务没有报错接口也能正常返回只是部分用户持续读到旧数据。一、最常见的错误把“更新库后删缓存”理解成原子操作很多人看到下面这段代码时会认为它天然有顺序TransactionalpublicvoidupdateProductPrice(LongproductId,BigDecimalnewPrice){productRepository.updatePrice(productId,newPrice);redisTemplate.delete(product:price:productId);}从代码执行顺序看确实先执行数据库更新再执行缓存删除。但从数据库事务的角度看updatePrice()只是把修改放进当前事务。在事务真正提交前其他请求是否能读到新数据取决于隔离级别、查询路径和当前连接状态。于是会出现这样的时序时间点写请求读请求T1更新数据库事务未提交T2删除缓存T3读取缓存未命中T4查询数据库读到旧价格T5把旧价格写回缓存T6数据库事务提交T7后续请求继续读缓存旧值问题不在于 Redis 出错也不在于 SQL 没执行。问题是缓存失效发生得太早。缓存删除应该和“事务已经确认提交”绑定而不是只跟随代码行的执行顺序。二、为什么 AI 很容易给出这个不完整方案AI 在处理这类问题时通常会根据大量常见代码模式给出一个“更新库 删缓存”的基础方案。这个方案适合回答修改数据后怎样让缓存尽快失效但它没有自动回答当前数据库事务什么时候提交缓存删除失败后如何恢复高并发下是否有人会在提交前重建旧缓存多个服务是否都能写同一份缓存重试任务是否会误删更新后的新缓存关键价格、库存、权益字段是否允许短暂读旧值。也就是说AI 可以迅速生成局部正确的实现但局部正确不等于整体一致。近期关于 AI 辅助开发的工程讨论也越来越强调把团队规则、验证方式和评审标准沉淀成可复用资产而不是只依赖某次提示词或某个开发者的即时判断。对缓存一致性来说真正需要沉淀的不是一句“更新后删缓存”而是完整规则数据库提交成功 ↓ 缓存失效 ↓ 读取方允许重建缓存 ↓ 缓存写入必须校验版本或时间 ↓ 删除失败必须进入补偿链路三、正确的第一步事务提交后再删除缓存在 Spring 场景下可以让缓存失效逻辑放到事务提交成功之后。例如publicrecordProductPriceChangedEvent(LongproductId,BigDecimalprice){}业务层只负责修改数据库和发布领域事件TransactionalpublicvoidupdateProductPrice(LongproductId,BigDecimalnewPrice){productRepository.updatePrice(productId,newPrice);applicationEventPublisher.publishEvent(newProductPriceChangedEvent(productId,newPrice));}缓存删除放在事务提交后ComponentpublicclassProductCacheInvalidationListener{TransactionalEventListener(phaseTransactionPhase.AFTER_COMMIT)publicvoidonPriceChanged(ProductPriceChangedEventevent){redisTemplate.delete(product:price:event.productId());}}这样做至少解决了一件事如果数据库事务最终回滚就不会提前删除缓存。同时读请求在数据库提交前仍然可以继续读取旧缓存不会因为缓存提前删除而把旧数据库值重新写回缓存。但要注意这不是所有问题的终点。四、事务提交后删缓存也可能失败假设数据库事务已经提交成功但 Redis 恰好出现网络异常TransactionalEventListener(phaseTransactionPhase.AFTER_COMMIT)publicvoidonPriceChanged(ProductPriceChangedEventevent){redisTemplate.delete(product:price:event.productId());}如果delete()失败数据库已经是新价格缓存仍然是旧价格。此时不能简单写一句try{redisTemplate.delete(key);}catch(Exceptione){log.error(delete cache failed,e);}因为日志并不会自动修复脏缓存。更可靠的做法是把“缓存失效失败”视为一个需要补偿的工程事件。例如记录待处理任务publicvoidinvalidateProductCache(LongproductId){Stringkeyproduct:price:productId;try{redisTemplate.delete(key);}catch(Exceptionex){cacheRetryRepository.save(CacheRetryTask.of(DELETE,key,PRODUCT_PRICE_CHANGED));throwex;}}然后由独立任务进行重试Scheduled(fixedDelay30000)publicvoidretryCacheInvalidation(){ListCacheRetryTasktaskscacheRetryRepository.findPendingTasks(100);for(CacheRetryTasktask:tasks){try{redisTemplate.delete(task.getCacheKey());cacheRetryRepository.markSuccess(task.getId());}catch(Exceptione){cacheRetryRepository.increaseRetryCount(task.getId());}}}这里要注意一个边界重试删除不能无限执行。如果旧重试任务在很久之后才执行它可能删除已经被新业务重新写入的缓存。因此重试任务需要带版本、事件时间或业务版本号而不是只保存一个 Redis Key。五、给缓存数据加版本避免旧事件干扰新数据例如缓存对象里增加业务版本publicrecordProductPriceCache(LongproductId,BigDecimalprice,Longversion,InstantcachedAt){}写入数据库时同步递增版本TransactionalpublicvoidupdateProductPrice(LongproductId,BigDecimalnewPrice){ProductproductproductRepository.findByIdForUpdate(productId);product.changePrice(newPrice);product.increaseVersion();productRepository.save(product);applicationEventPublisher.publishEvent(newProductPriceChangedEvent(productId,newPrice,product.getVersion()));}缓存补偿任务也记录版本publicrecordCacheRetryTask(Longid,StringcacheKey,LongeventVersion,Stringstatus){}这样补偿任务执行前可以判断待删除事件版本 当前数据库版本 ↓ 说明这是旧事件 ↓ 不能盲目执行删除或覆盖很多系统不需要一开始就引入复杂版本控制。但对于价格、库存、权益、审批状态、限流策略等关键数据至少要明确缓存是否允许短暂不一致旧事件能否影响新缓存补偿操作如何避免覆盖最新状态是否需要强制读取数据库确认。六、把 AI 从“给代码”变成“帮你列验证清单”让 AI 直接写缓存更新代码得到的通常是一段可运行实现。但更有价值的使用方式是让它先帮你列出缓存一致性风险。例如你是后端架构评审助手。 场景 商品价格修改后需要失效 Redis 缓存。 数据库使用事务缓存采用 Cache Aside 模式。 请不要直接只给“更新数据库后删除缓存”的代码。 请输出 1. 事务未提交时提前删缓存可能造成的并发时序 2. 缓存删除失败后的补偿方案 3. 重试任务可能误删新缓存的风险 4. 哪些字段适合允许短暂不一致 5. 最少 6 个并发与异常测试场景 6. 需要人工确认的业务边界。这样得到的输出通常更适合作为代码评审前的检查清单。对于已经把 ChatGPT Plus、GPT Plus 用在代码解释、设计讨论、测试补全和排障整理中的开发者来说长期使用的价值不在于每次都让工具直接写完代码而在于是否能把问题建模、约束条件和验证步骤逐步沉淀到自己的工作流里。对已经确认有 AI 工具长期使用需求的开发者来说工具准备不只是模型能力还包括使用周期、说明理解、边界意识和异常处理路径相关信息可按实际需要参考gpt985com七、这类缓存改造至少要测什么缓存一致性问题最怕只测“正常更新后能不能读到新值”。更应该覆盖这些场景测试场景预期结果数据库更新成功、缓存删除成功下次读取从数据库加载新值数据库更新回滚原缓存不应被删除缓存删除失败产生可重试补偿任务删除缓存后并发读取不应长期写入旧值补偿任务延迟执行不应误删新版本缓存两次快速更新同一商品最终缓存必须对应最新版本Redis 短暂不可用系统具备降级、告警和恢复路径例如可以测试“事务回滚时缓存不失效”TestvoidshouldNotEvictCacheWhenDatabaseTransactionRollsBack(){Stringkeyproduct:price:10001;redisTemplate.opsForValue().set(key,99.00);assertThrows(BusinessException.class,()-productApplicationService.updatePriceWithFailure(10001L,newBigDecimal(109.00)));assertEquals(99.00,redisTemplate.opsForValue().get(key));}再测试“事务提交后才触发缓存失效”TestvoidshouldEvictCacheOnlyAfterTransactionCommit(){Stringkeyproduct:price:10001;redisTemplate.opsForValue().set(key,99.00);productApplicationService.updatePrice(10001L,newBigDecimal(109.00));assertNull(redisTemplate.opsForValue().get(key));}这些测试的重点不是验证某个方法是否被调用而是验证在异常、并发和重试条件下最终读到的数据是否仍然符合业务预期。八、结语“更新数据库后删除缓存”不是错误方案。它只是一个还没有写完的方案。真正可靠的缓存一致性设计至少要回答删除缓存发生在事务提交前还是提交后删除失败后如何补偿延迟重试会不会影响新数据高并发读请求是否可能重建旧缓存哪些数据允许短暂不一致线上如何发现缓存和数据库已经出现偏差。AI 可以快速给出缓存更新代码也可以帮你补测试和整理时序。但能不能把缓存逻辑真正放进长期稳定的开发工作流取决于你是否把事务、版本、补偿和验证这些边界一起设计进去。缓存问题最怕的不是一次删除失败。而是系统已经读错数据却没有任何人知道它从什么时候开始错了。