Cache Aside 旁路缓存 + 延迟双删 完整原理与落地文档
1. Cache Aside(旁路缓存)概述
1.1 核心定义
Cache Aside 又称旁路缓存 / 缓存旁置,是业界最通用的缓存设计模式。
核心特征:
-
MySQL 为唯一权威数据源,Redis 仅为临时副本
-
读写缓存逻辑完全由业务代码手动控制
-
缓存与数据库无自动联动,解耦度高、性能最好、落地最简单
1.2 读流程(标准、无争议)
-
先查询 Redis 缓存
-
缓存命中 → 直接返回数据
-
缓存未命中 → 查询 MySQL,回写 Redis,再返回数据
读流程无并发一致性问题,所有业务通用。
1.3 写流程(两种方案对比)
❌ 方案一:先删缓存、再更新 DB(严禁使用)
流程:删缓存 → 更新数据库
致命缺陷:高并发下极易产生永久脏缓存
-
线程A删除缓存
-
线程B查询缓存失效,读取 DB 旧数据写入缓存
-
线程A才更新完数据库
结果:缓存长期是旧数据,严重数据不一致。
✅ 方案二:先更新 DB、再删缓存(标准基础写法)
流程:更新数据库 → 删除缓存
优点:绝大多数场景一致性良好
残留漏洞(核心痛点):极端并发会出现脏缓存
漏洞复现条件:
-
缓存刚好过期
-
读线程查询 DB 耗时非常长
-
写线程完成「更新DB + 删除缓存」
-
读线程最后将旧数据写入 Redis
结果:永久脏缓存,直到 Key 过期。该问题引出延迟双删方案。
2. 延迟双删 原理与解决方案
2.1 核心作用
解决「先更新DB后删缓存」模式下,慢查询并发导致的旧数据回填脏缓存问题。
2.2 最终标准流程(生产推荐)
-
更新 MySQL 数据库
-
立即删除一次缓存(即时清理)
-
异步延迟等待(根据业务最慢 SQL 耗时配置)
-
二次删除缓存(兜底清理滞后回填的旧数据)
2.3 为什么能解决脏数据?
慢查询线程滞后写入的旧缓存,会被延迟第二次删除直接清掉,彻底杜绝永久脏缓存。
2.4 延迟时间配置规范
-
普通主键查询:500ms
-
复杂查询/慢SQL:1s~2s
原则:延迟时间 > 业务最大单次DB查询耗时。
2.5 两种落地实现方式
方式1:本地定时任务(单机使用)
通过线程池延迟执行删除。
缺点:集群部署、服务重启会丢失延迟任务。
方式2:延迟MQ消息(企业生产最优)
更新完DB+立即删缓存 → 发送延迟消息 → 消费者执行二次删除。
优点:持久化、不丢任务、支持分布式集群。
3. 优缺点总结
3.1 优点
-
彻底解决高并发极端脏缓存问题
-
代码侵入极小,性能损耗极低
-
无需分布式锁,吞吐量高
3.2 缺点
-
属于最终一致性,延迟窗口内存在短暂脏读
-
本地延迟任务不适合分布式集群
4. 生产兜底策略(必加)
所有缓存 Key 必须配置过期时间 TTL
作用:即使极端情况漏删缓存,脏数据也会自动过期,形成:
延迟双删主动清理 + TTL 过期兜底 双保险机制
5. 适用场景
✅ 适合 CacheAside + 延迟双删
高读低写、允许短暂不一致:
- 商品、设备信息、用户资料、配置项、首页数据
❌ 不适合(强一致业务)
库存、订单、金额、账务等强一致性场景,需要:
-
分布式锁 / 事务
-
Canal Binlog 异步更新缓存
6. 核心伪代码
Plain
// 1. 更新数据库
updateDB(data)
// 2. 第一次立即删除缓存
redis.del(cacheKey)
// 3. 异步延迟二次删除(推荐 MQ 延迟消息)
sendDelayDeleteMsg(cacheKey, delayTime = 1s)