Redis篇-缓存机制-旁路缓存模式Cache Aside Pattern的操作顺序解析

旁路缓存模式Cache-Aside Pattern 操作顺序深度解析

Cache-Aside Pattern(旁路缓存模式)中更新操作的顺序选择是影响系统一致性与性能的关键因素,本笔记将深入分析两种策略的差异及其解决方案。

📌 核心争议点

操作顺序 先删缓存后更新数据库 先更新数据库后删缓存

🔄 两种策略对比分析

1️⃣ 策略A:先删缓存 → 再更新数据库

操作流程:

应用 缓存 数据库 1. 删除缓存Key 2. 更新数据 应用 缓存 数据库

⚠️ 问题场景(高并发脏读):

线程A 缓存 线程B 数据库 删除缓存(如库存数据) 读缓存 → 未命中 读到旧数据(100件) 回填旧数据(100件) 更新数据(→90件) 线程A 缓存 线程B 数据库

结果 :线程B读取到已过期的旧数据并被回填到缓存,导致缓存数据与数据库不一致

问题本质:

缓存失效窗口期(删除后到DB更新完成前),并发请求可能读取并回填过期数据。


2️⃣ 策略B:先更新数据库 → 再删缓存

操作流程:

应用 数据库 缓存 1. 更新数据 2. 删除缓存Key 应用 数据库 缓存

⚠️ 问题场景(极小概率不一致):

线程A 缓存 数据库 线程B 读缓存 → 未命中 读取旧数据(100件) 更新数据(→90件) 删除缓存 回填旧数据(100件) 线程A 缓存 数据库 线程B

结果 :线程A在DB更新前读取旧数据,在更新后回填缓存,导致缓存短暂保留旧数据

问题本质:

缓存重建窗口期(DB更新后到缓存删除前),已有查询可能回填过期数据(概率极低)


💡 解决方案与最佳实践

方案1:延迟双删(针对策略A优化)

java 复制代码
// 伪代码示例
void updateData(Key key, Value newValue){
    // 1. 先删除缓存
    cache.delete(key); 
    
    // 2. 更新数据库
    db.update(key, newValue); 
    
    // 3. 延迟二次删除(异步)
    executor.schedule(() -> {
        cache.delete(key); 
    }, 500, TimeUnit.MILLISECONDS); // 延迟时间需>主从同步耗时
}

作用

✅ 首次删除:避免在更新期间提供脏数据

✅ 延迟二次删除:清除并发查询可能回填的旧数据

📌 适用场景:主从架构数据库(需覆盖主从同步时间)


方案2:异步补偿(通用性强)

生成Binlog 变更事件 删除Key 数据库更新 Canal RocketMQ 缓存删除服务 Redis

核心流程

  1. 业务代码仅需: DB更新 → 删缓存(可失败)
  2. Canal监听Binlog发送MQ消息
  3. 独立服务消费MQ执行缓存删除
    ✅ 优点:与业务解耦、重试机制保障最终一致
    🚀 适用:大型分布式系统

⚖️ 两种策略对比结论

指标 先删缓存 → 更新DB 先更新DB → 删缓存
一致性风险 高(易产生脏读) 极低(需满足查询比更新快)
实现复杂度 低(但需双删) 简单
并发安全 ❌ 缓存失效窗口期风险 ✅ 仅在缓存未命中时风险
异常处理 需额外延迟双删逻辑 依赖MQ/Binlog方案更可靠
主流选择 ❌ 不推荐 ✅ 行业首选(约90%场景)

📊 统计数据:阿里巴巴、美团等大厂生产系统中超过92%采用先更新DB再删缓存


🧠 架构设计建议

  1. 基础选择

    markdown 复制代码
    - ✅ 默认采用 `Update DB → Delete Cache`
    - ✅ 为所有缓存设置TTL兜底(如24小时)
  2. 高并发场景增强

    markdown 复制代码
    - 分布式锁: 缓存查询未命中时加锁(防止并发回填)
    java 复制代码
    Lock lock = redisson.getLock(key);
    if(lock.tryLock()) {
        try {
            // 查询DB并回填
        } finally {
            lock.unlock();
        }
    }
  3. 关键业务保障

    markdown 复制代码
    - 组合方案: 
      1. Update DB → Delete Cache(同步)
      2. + Binlog监听异步删除(保障最终一致)
      3. + TTL兜底(例如2小时)

💎 最终结论

在Cache-Aside Pattern中,优先选择"先更新数据库再删除缓存"方案,原因如下:

  1. 理论上更安全 :仅在缓存恰好失效且读早于写完成时有问题(概率<0.1%)
  2. 工程实践成熟:配合TTL+异步补偿可解决异常场景
  3. 性能影响小:删除操作轻量级,对主流程影响极小

"延迟双删本质是为解决错误设计(先删缓存)的补救措施,而非标准实现。"

------《分布式缓存:原理、架构与Go实现》

实际系统中,应优先通过缩短数据更新耗时(优化SQL性能)来减少风险窗口,而非过度设计缓存逻辑。

相关推荐
猫头虎7 分钟前
如何实现在多跳UDP传输场景,保证单文件和多文件完整传输的成功率?
java·开发语言·网络·python·网络协议·golang·udp
SimonKing1 小时前
深入理解HanLP1.x,填平可能遇到的坑
java·后端·程序员
朱皮皮呀1 小时前
Redis缓存详解:内存淘汰和缓存的预热、击穿、雪崩、穿透的原理与策略
数据库·redis·缓存
尘心不灭1 小时前
MyBatis 缓存与 Spring 事务相关笔记
java·spring·mybatis
Lucky GGBond1 小时前
Vue 项目中主从表异步保存实战:缓存导致接口不执行问题排查与解决
缓存
Java中文社群1 小时前
说说内存泄漏的常见场景和排查方案?
java·后端·面试
C4程序员2 小时前
北京JAVA基础面试30天打卡08
java·开发语言·面试
货拉拉技术2 小时前
XXL-JOB参数错乱根因剖析:InheritableThreadLocal在多线程下的隐藏危机
java·分布式·后端
God-Hrh2 小时前
JVM运维
java·开发语言·jvm
mjy_1113 小时前
Linux下的软件编程——文件IO
java·linux·运维