缓存删除三级补偿方案:延迟队列+消息队列+定时任务兜底

问题背景:
在 Cache-Aside 模式中,更新数据库后删除缓存失败会导致数据不一致。本文提供工业级三级补偿方案,实现最终一致性保障。

整体架构:

复制代码
更新操作触发 → 一级延迟队列 → 二级消息队列 → 三级定时任务

方案实现:

一、第一级补偿:延迟队列(快速重试)

核心代码:

java 复制代码
// 延迟队列初始化
@PostConstruct
public void init() {
    deleteQueue = redisson.getQueue("cache:delete:queue");
    delayedQueue = redisson.getDelayedQueue(deleteQueue);
}

// 更新操作切面
@Around("@annotation(cacheUpdate)")
public Object aroundUpdate(...) {
    // 首次删除缓存
    redisson.getBucket(key).delete();
    
    // 数据库操作
    Object result = joinPoint.proceed();
    
    // 加入延迟队列(1秒后二次删除)
    delayedQueue.offer(key, 1, TimeUnit.SECONDS); 
    return result;
}

// 消费延迟队列(独立线程)
@EventListener(ApplicationReadyEvent.class)
public void startDelayConsumer() {
    new Thread(() -> {
        while (true) {
            String key = deleteQueue.poll(10, TimeUnit.SECONDS);
            if (key != null) {
                redisson.getBucket(key).delete();
            }
        }
    }).start();
}

特点:

响应时间:秒级

适用场景:高频更新业务

防抖设计:单线程顺序消费

二、第二级补偿:消息队列(可靠重试)

RocketMQ 集成示例:

java 复制代码
// 消息监听器
@RocketMQMessageListener(topic = "CACHE_DELETE", consumerGroup = "cache-group")
public void handleDelete(String key) {
    try {
        if (!redisson.getBucket(key).delete()) {
            retryWithBackoff(key, 3); // 指数退避重试
        }
    } catch (Exception e) {
        // 进入死信队列
    }
}

// 退避策略实现
private void retryWithBackoff(String key, int retryCount) {
    for (int i = 1; i <= retryCount; i++) {
        Thread.sleep(1000 * i);
        if (redisson.getBucket(key).delete()) break;
    }
}

// 延迟队列异常处理
delayedQueue.offer(...).exceptionally(e -> {
    rocketMQTemplate.send("CACHE_DELETE", key); 
    return null;
});

特点:

可靠性:99.9%+ 送达保障

重试策略:3次指数退避

容错机制:死信队列隔离异常

三、第三级补偿:定时任务(全量兜底)

Spring Scheduler 实现:

java 复制代码
// 每天凌晨执行全量比对
@Scheduled(cron = "0 0 3 * * ?") 
public void scanAndFix() {
    redisson.getKeys()
           .getKeysByPattern("user:*")
           .forEach(key -> {
               Long userId = extractUserId(key);
               User dbUser = databaseService.getUserFromDB(userId);
               User cacheUser = (User) redisson.getBucket(key).get();
               
               // 判断是否需要删除
               if ((cacheUser != null && dbUser == null) || 
                   (dbUser != null && cacheUser.getVersion() < dbUser.getVersion())) {
                   redisson.getBucket(key).delete();
               }
           });
}

比对策略:

缓存存在但数据库已物理删除

数据版本号不一致

逻辑删除标记状态不一致

防抖容错设计

  1. 防重复删除机制
java 复制代码
String deleteFlagKey = key + ":deleting";
if (redis.setnx(deleteFlagKey, "1")) {
    redis.expire(deleteFlagKey, 30); // 30秒窗口期
    performDelete(key); 
}
  1. 监控报警体系

各级补偿对比

相关推荐
利威尔xu10 小时前
Go IM 源码实战(14):本地缓存快 3000 倍,失效却难 10 倍
后端·缓存·微服务·面试·golang
程序猿编码15 小时前
解决 LLM 算力浪费,一套统一 KV 管理层实现缓存复用
人工智能·神经网络·机器学习·缓存·llm·大模型推理
zrc00700719 小时前
steam无法登录 steam错误 登录时出现错误。请稍后再试。解决办法 steam无法登录解决办法 湖南地区steam DNS问题解决办法
windows·经验分享·游戏·缓存·游戏程序·创业创新·玩游戏
CCblandy20 小时前
缓存淘汰实测:一次全表扫描把 LRU 命中率从 46% 打到 38%,W-TinyLFU 守住 50% 却贵 11 倍
后端·python·缓存·性能优化·架构
Wang's Blog20 小时前
Java 项目实战: 交友项目 - 小视频模块实战:访客记录、FastDFS 存储与 SpringCache 缓存
java·缓存·交友
麻瓜code21 小时前
双写一致性:为什么是“删缓存“,而不是“改缓存“
java·缓存·mybatis
snow@li21 小时前
redis:RedisDesktopManager(RDM/RESP.app)Redis 数据可视化全景深度梳理
数据库·redis·缓存
你看我名字1 天前
Redis 高可用、持久化与故障切换:从缓存组件到云上基础设施
数据库·redis·缓存
二月夜1 天前
Redis 高频面试题全解:缓存三大问题、大 key 治理与分布式锁之争(2026 招聘 JD 真题复盘)
redis·分布式·后端·缓存
X1A0RAN1 天前
Jenkins CI 流水线优化实践:基于提交指纹的任务缓存
缓存·ci/cd·jenkins