Redis 删除缓存失败怎么办?重试、死信、补偿的工程化方案
这篇聊一个很现实的问题:数据库已经改成功了,但缓存删除失败了,线上怎么办?
先给答案
如果你项目里只有一句redis.del(key),那一致性是靠运气。
一套更稳的做法是:
- 主流程里先写库再删缓存
- 删除失败立刻进入重试队列
- 超过重试上限进入死信队列
- 死信触发告警和人工/自动补偿
- 全链路打点,能看见“删失败率”和“补偿成功率”
一句话:删除缓存不是一个动作,而是一条可观测、可补偿的链路。
为什么“删缓存失败”必须单独设计
很多同学会说:“删失败就下次再读库呗。”
这句话在低并发时看起来没毛病,但线上高峰期会出事:
- 热点 key 还在,用户继续读到旧值
- 读流量越大,旧值传播越快
- 你又没有补偿机制,脏数据会“活很久”
最麻烦的是:这个问题不会立刻报错,而是以“偶发投诉”“数据不对”的形式出现,排查成本很高。
一个真实可落地的链路
你会发现,核心不是“怎么删”,而是“删不掉时怎么兜”。
代码示例:主流程 + 异步重试
1. 主流程(写库后删缓存)
@ServicepublicclassProductService{@ResourceprivateProductMapperproductMapper;@ResourceprivateStringRedisTemplateredisTemplate;@ResourceprivateCacheDeleteProducercacheDeleteProducer;@Transactional(rollbackFor=Exception.class)publicvoidupdateProduct(Productproduct){Stringkey="product:"+product.getId();// 1) 数据库是事实来源productMapper.updateById(product);// 2) 主流程删缓存,失败则入重试队列try{redisTemplate.delete(key);}catch(Exceptionex){cacheDeleteProducer.sendDeleteEvent(key,1);}}}2. 重试消费者(指数退避 + 最大次数)
@ComponentpublicclassCacheDeleteConsumer{privatestaticfinalintMAX_RETRY=5;@ResourceprivateStringRedisTemplateredisTemplate;@ResourceprivateCacheDeleteProducercacheDeleteProducer;@ResourceprivateDeadLetterProducerdeadLetterProducer;publicvoidonMessage(CacheDeleteEventevent){try{redisTemplate.delete(event.getCacheKey());// 打点:delete_success_total +1}catch(Exceptionex){intnextRetry=event.getRetryCount()+1;if(nextRetry>MAX_RETRY){deadLetterProducer.send(event.getCacheKey(),ex.getMessage());return;}longdelaySeconds=(long)Math.pow(2,nextRetry);// 2,4,8,16,32cacheDeleteProducer.sendDeleteEvent(event.getCacheKey(),nextRetry,delaySeconds);}}}3. 死信补偿任务(定时巡检)
@ComponentpublicclassCacheDeleteCompensationJob{@ResourceprivateDeadLetterRepositorydeadLetterRepository;@ResourceprivateStringRedisTemplateredisTemplate;// 每 5 分钟跑一次@Scheduled(cron="0 */5 * * * ?")publicvoidcompensate(){List<DeadLetterRecord>records=deadLetterRepository.queryUnresolved(200);for(DeadLetterRecordrecord:records){try{redisTemplate.delete(record.getCacheKey());deadLetterRepository.markResolved(record.getId());}catch(Exceptione){deadLetterRepository.increaseFailCount(record.getId(),e.getMessage());}}}}这 5 个细节,决定你方案能不能用
幂等性
删缓存天生幂等,删不存在 key 也算成功,别把它当异常。重试上限
不要无限重试,超过阈值必须死信,不然就是隐性消息堆积。退避策略
固定 1 秒重试容易打爆 Redis,用指数退避更稳。死信可见性
死信不等于丢弃,要有告警和处理面板。链路监控
至少要有这几个指标:
cache_delete_fail_totalcache_delete_retry_totalcache_delete_dlt_totalcache_delete_compensation_success_total
常见误区
误区 1:删失败概率很低,可以忽略
线上你总会遇到:网络抖动、Redis 短暂超时、连接池耗尽。
低概率 * 高频请求 = 可观事故数。
误区 2:有延迟双删就够了
延迟双删只能覆盖一部分并发窗口,无法替代失败重试链路。
误区 3:死信就是失败,人工看就行
只靠人工盯死信,夜里一定会漏。
最好是“告警 + 自动补偿 + 人工兜底”三层。
选型建议(按团队规模)
| 团队阶段 | 推荐方案 |
|---|---|
| 小团队、单体服务 | 写库后删缓存 + 本地重试(短期) |
| 中型团队、多服务 | 写库后删缓存 + MQ 重试 + 死信告警 |
| 大团队、高一致性要求 | 事件驱动一致性 + 死信平台 + 自动补偿任务 |
最后总结
“删除缓存失败”不是小概率边角料,它是缓存一致性的主战场。
真正能扛线上流量的方案,通常长这样:
- 主链路快:写库后删缓存
- 失败可恢复:异步重试
- 极端可兜底:死信补偿
- 整体可观测:指标和告警
把这四件事做到位,你的缓存一致性就不是“玄学”,而是工程能力。
